Skip to content

Commit 5fcc5b6

Browse files
authored
Solve Wahba's problem (#182)
1 parent 6a8dde4 commit 5fcc5b6

File tree

4 files changed

+94
-3
lines changed

4 files changed

+94
-3
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
# Set this first for easier replacement
20-
version = "2021.8.30.10.33.11"
20+
version = "2021.10.7.23.40.37"
2121

2222
if "win" in platform.lower() and not "darwin" in platform.lower():
2323
extra_compile_args = ["/O2"]

src/quaternion/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Copyright (c) 2021, Michael Boyle
44
# See LICENSE file for details: <https://github.com/moble/quaternion/blob/main/LICENSE>
55

6-
__version__ = "2021.8.30.10.33.11"
6+
__version__ = "2021.10.7.23.40.37"
77
__doc_title__ = "Quaternion dtype for NumPy"
88
__doc__ = "Adds a quaternion dtype to NumPy."
99
__all__ = ['quaternion',
@@ -40,7 +40,11 @@
4040
from .calculus import spline
4141
except:
4242
pass
43-
from .means import mean_rotor_in_chordal_metric, optimal_alignment_in_chordal_metric
43+
from .means import (
44+
mean_rotor_in_chordal_metric,
45+
optimal_alignment_in_chordal_metric,
46+
optimal_alignment_in_Euclidean_metric
47+
)
4448

4549
np.quaternion = quaternion
4650
np.sctypeDict['quaternion'] = np.dtype(quaternion)

src/quaternion/means.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,66 @@ def optimal_alignment_in_chordal_metric(Ra, Rb, t=None):
7575
return mean_rotor_in_chordal_metric(Ra / Rb, t)
7676

7777

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+
78139
def mean_rotor_in_intrinsic_metric(R, t=None):
79140
raise NotImplementedError()

tests/test_quaternion.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,32 @@ def test_mean_rotor_in_chordal_metric():
15921592
assert np.abs(q-mean2) < 1e-15, (q, mean2, length)
15931593

15941594

1595+
@pytest.mark.skipif(not has_scipy, reason="Scipy is not installed")
1596+
def test_optimal_alignment_in_Euclidean_metric():
1597+
N = 10
1598+
a⃗ = np.random.normal(size=(N, 3))
1599+
R = quaternion.quaternion(*np.random.normal(size=(4))).normalized()
1600+
1601+
# Test the exact result
1602+
b⃗ = quaternion.rotate_vectors(R, a⃗)
1603+
Rprm = quaternion.optimal_alignment_in_Euclidean_metric(a⃗, b⃗)
1604+
assert quaternion.rotation_intrinsic_distance(R, np.conjugate(Rprm)) < 25*eps
1605+
assert np.max(np.abs(a⃗ - quaternion.rotate_vectors(Rprm, b⃗))) < 40*eps
1606+
1607+
# Uniform time steps
1608+
t = np.linspace(-1.2, 3.4, num=N)
1609+
Rprmprm = quaternion.optimal_alignment_in_Euclidean_metric(a⃗, b⃗, t)
1610+
assert quaternion.rotation_intrinsic_distance(R, np.conjugate(Rprmprm)) < 25*eps
1611+
assert np.max(np.abs(a⃗ - quaternion.rotate_vectors(Rprmprm, b⃗))) < 40*eps
1612+
1613+
# Perturb b⃗ slightly
1614+
δ = np.sqrt(eps)
1615+
b⃗prmprmprm = [b⃗[i] + (2*(np.random.rand(3) - 0.5) * δ/np.sqrt(3)) for i in range(N)]
1616+
Rprmprmprm = quaternion.optimal_alignment_in_Euclidean_metric(a⃗, b⃗prmprmprm)
1617+
assert quaternion.rotation_intrinsic_distance(R, np.conjugate(Rprmprmprm)) < 25*δ
1618+
assert np.max(np.abs(a⃗ - quaternion.rotate_vectors(Rprmprmprm, b⃗prmprmprm))) < 40*δ
1619+
1620+
15951621
def test_numpy_save_and_load():
15961622
import tempfile
15971623
a = quaternion.as_quat_array(np.random.rand(5,3,4))

0 commit comments

Comments
 (0)