@@ -75,5 +75,66 @@ def optimal_alignment_in_chordal_metric(Ra, Rb, t=None):
75
75
return mean_rotor_in_chordal_metric (Ra / Rb , t )
76
76
77
77
78
+ def optimal_alignment_in_Euclidean_metric (a⃗ , b⃗ , t = None ):
79
+ """Return rotor R such that R*b⃗*R̄ is as close to a⃗ as possible
80
+
81
+ As in the `optimal_alignment_in_chordal_metric` function, the `t` argument is
82
+ optional. If it is present, the times are used to weight the corresponding
83
+ integral. If it is not present, a simple sum is used instead (which may be
84
+ slightly faster).
85
+
86
+ The task of finding `R` is called "Wahba's problem"
87
+ <https://en.wikipedia.org/wiki/Wahba%27s_problem>, and has a simple solution
88
+ using eigenvectors. In their book "Fundamentals of Spacecraft Attitude
89
+ Determination and Control" (2014), Markley and Crassidis say that "Davenport’s
90
+ method remains the best method for solving Wahba’s problem". This constructs a
91
+ simple matrix from a sum over the input vectors, and extracts the optimal rotor
92
+ as the dominant eigenvector (the one with the largest eigenvalue).
93
+
94
+ """
95
+ import numpy as np
96
+ from scipy .linalg import eigh
97
+ from scipy .interpolate import InterpolatedUnivariateSpline as spline
98
+ from . import quaternion
99
+
100
+ a⃗ = np .asarray (a⃗ , dtype = float )
101
+ b⃗ = np .asarray (b⃗ , dtype = float )
102
+ if a⃗ .shape != b⃗ .shape :
103
+ raise ValueError (f"Input vectors must have same shape; a⃗.shape={ a⃗ .shape } , b⃗.shape={ b⃗ .shape } " )
104
+ if a⃗ .shape [- 1 ] != 3 :
105
+ raise ValueError (f"Final dimension of a⃗ and b⃗ must have size 3; it is { a⃗ .shape [- 1 ]} " )
106
+ if t is not None :
107
+ if a⃗ .ndim != 2 :
108
+ raise ValueError (f"If t is not None, a⃗ and b⃗ must have exactly 2 dimensions; they have { a⃗ .ndim } " )
109
+ t = np .asarray (t , dtype = float )
110
+ if a⃗ .shape [0 ] != len (t ):
111
+ raise ValueError (f"Input time must have same length as first dimension of vectors; len(t)={ len (t )} " )
112
+
113
+ # This constructs the matrix given by Eq. (5.11) of Markley and Crassidis
114
+ S = np .empty ((3 , 3 ))
115
+ for i in range (3 ):
116
+ for j in range (3 ):
117
+ if t is None :
118
+ S [i , j ] = np .sum (a⃗ [..., i ] * b⃗ [..., j ])
119
+ else :
120
+ S [i , j ] = spline (t , a⃗ [:, i ] * b⃗ [:, j ]).integral (t [0 ], t [- 1 ])
121
+
122
+ # This is Eq. (5.17) from Markley and Crassidis, modified to suit our
123
+ # conventions by flipping the sign of ``z``, and moving the final dimension
124
+ # to the first dimension.
125
+ M = [
126
+ [S [0 ,0 ]+ S [1 ,1 ]+ S [2 ,2 ], S [2 ,1 ]- S [1 ,2 ], S [0 ,2 ]- S [2 ,0 ], S [1 ,0 ]- S [0 ,1 ], ],
127
+ [ S [2 ,1 ]- S [1 ,2 ], S [0 ,0 ]- S [1 ,1 ]- S [2 ,2 ], S [0 ,1 ]+ S [1 ,0 ], S [0 ,2 ]+ S [2 ,0 ], ],
128
+ [ S [0 ,2 ]- S [2 ,0 ], S [0 ,1 ]+ S [1 ,0 ], - S [0 ,0 ]+ S [1 ,1 ]- S [2 ,2 ], S [1 ,2 ]+ S [2 ,1 ], ],
129
+ [ S [1 ,0 ]- S [0 ,1 ], S [0 ,2 ]+ S [2 ,0 ], S [1 ,2 ]+ S [2 ,1 ], - S [0 ,0 ]- S [1 ,1 ]+ S [2 ,2 ],],
130
+ ]
131
+
132
+ # This extracts the dominant eigenvector, and interprets it as a rotor. In
133
+ # particular, note that the *last* eigenvector output by `eigh` (the 3rd)
134
+ # has the largest eigenvalue.
135
+ eigenvector = eigh (M , subset_by_index = (3 , 3 ))[1 ][:, 0 ]
136
+ return quaternion (* eigenvector )
137
+
138
+
78
139
def mean_rotor_in_intrinsic_metric (R , t = None ):
79
140
raise NotImplementedError ()
0 commit comments