Skip to content

Commit 0a47d38

Browse files
feat: enhance coning and sculling algorithm allowing optimized calibration (DLAB-123) (#103)
1 parent 491aa06 commit 0a47d38

5 files changed

Lines changed: 276 additions & 71 deletions

File tree

src/smsfusion/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from . import benchmark, calibrate, constants, noise
2-
from ._coning_sculling import ConingScullingAlg
2+
from ._coning_sculling import ConingScullingAlg, ConingScullingAlgCalibrated
33
from ._ins import AHRS, VRU, AidedINS, FixedNED, StrapdownINS, gravity
44
from ._smoothing import FixedIntervalSmoother
55
from ._transforms import quaternion_from_euler
@@ -18,4 +18,5 @@
1818
"VRU",
1919
"quaternion_from_euler",
2020
"ConingScullingAlg",
21+
"ConingScullingAlgCalibrated",
2122
]

src/smsfusion/_coning_sculling.py

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numpy as np
22
from numpy.typing import ArrayLike
33

4-
from smsfusion._vectorops import _cross
4+
from smsfusion._vectorops import _adjugate_and_det_3_by_3, _cross
55

66

77
class ConingScullingAlg:
@@ -13,8 +13,8 @@ class ConingScullingAlg:
1313
1414
Can be used in a strapdown algorithm as:
1515
16-
vel[m+1] = vel[m] + R(q[m]) @ dvel[m] + dvel_corr
17-
q[m+1] = q[m] ⊗ dq(dtheta[m])
16+
vel[m+1] = vel[m] + R(q[m]) @ dvel[m+1] + dvel_corr
17+
q[m+1] = q[m] ⊗ dq(dtheta[m+1])
1818
1919
where,
2020
@@ -23,11 +23,11 @@ class ConingScullingAlg:
2323
2424
and,
2525
26-
- dvel[m] is the sculling integral, i.e., the velocity vector change (no gravity
26+
- dvel[m+1] is the sculling integral, i.e., the velocity vector change (no gravity
2727
correction) from time step m to m+1.
28-
- dtheta[m] is the coning integral, i.e., the rotation vector change from time
28+
- dtheta[m+1] is the coning integral, i.e., the rotation vector change from time
2929
step m to m+1.
30-
- dq(dtheta[m]) is the unit quaternion representation of the rotation increment
30+
- dq(dtheta[m+1]) is the unit quaternion representation of the rotation increment
3131
over the interval [m, m+1].
3232
- R(q[m]) is the rotation matrix (body-to-nav) corresponding to the attitude
3333
quaternion q[m].
@@ -106,32 +106,20 @@ def update(self, f: ArrayLike, w: ArrayLike, degrees: bool = False):
106106
dv_prev[:] = dv
107107
dtheta_prev[:] = dtheta
108108

109-
def dtheta(self, degrees=False):
110-
"""
111-
Peek at the accumulated 'body attitude change' vector. I.e., the rotation
112-
vector describing the total rotation over all samples since initialization
113-
(or last reset).
114-
115-
Parameters
116-
----------
117-
degrees : bool, default False
118-
Specifies whether the returned rotation vector should be in degrees
119-
or radians (default).
120-
"""
121-
dtheta = self._theta + self._dtheta_con
122-
return np.degrees(dtheta) if degrees else dtheta
123-
124109
@property
125110
def _dvel_rot(self):
126111
return 0.5 * _cross(self._theta, self._vel)
127112

128-
def dvel(self):
113+
def _calc_dtheta_dvel(self, degrees=False):
129114
"""
130-
Peek at the accumulated specific force velocity change vector. I.e.,
131-
the total change in velocity (no gravity correction) over all samples since
132-
initialization (or last reset).
115+
Calculate the coning and sculling corrected dtheta and dvel.
133116
"""
134-
return self._vel + self._dvel_rot + self._dvel_scul
117+
dtheta = self._theta + self._dtheta_con
118+
dtheta = np.degrees(dtheta) if degrees else dtheta
119+
# Equation (7.2.2.2-23) in ref [2]_
120+
dvel = self._vel + self._dvel_rot + self._dvel_scul
121+
122+
return dtheta, dvel
135123

136124
def flush(self, degrees=False):
137125
"""
@@ -148,15 +136,93 @@ def flush(self, degrees=False):
148136
Returns
149137
-------
150138
dtheta : ndarray, shape (3,)
151-
The accumulated 'body attitude change' vector, see :meth:`dtheta`.
139+
The accumulated 'body attitude change' vector. I.e., the rotation vector
140+
describing the total rotation over all samples since initialization (or
141+
last reset).
152142
dvel : ndarray, shape (3,)
153-
The accumulated specific force velocity change vector, see :meth:`dvel`.
143+
The accumulated specific force velocity change vector. I.e., the total change
144+
in velocity (no gravity correction) over all samples since initialization
145+
(or last reset).
154146
"""
155-
dtheta = self.dtheta(degrees=degrees)
156-
dvel = self.dvel()
147+
dtheta, dvel = self._calc_dtheta_dvel(degrees)
157148

158149
self._theta[:] = np.zeros(3, dtype=float)
159150
self._dtheta_con[:] = np.zeros(3, dtype=float)
160151
self._dvel_scul[:] = np.zeros(3, dtype=float)
161152
self._vel[:] = np.zeros(3, dtype=float)
162153
return dtheta, dvel
154+
155+
156+
class ConingScullingAlgCalibrated(ConingScullingAlg):
157+
"""Extension of :class:`ConingScullingAlg` that applies a calibration matrix and
158+
bias correction to the measurements while minimizing the number of operations. See
159+
:class:`ConingScullingAlg` for full API and more algorithm details.
160+
161+
Parameters
162+
----------
163+
fs : float
164+
Sampling frequency of the measurements (Hz).
165+
W_w : array-like, shape (3, 3), optional
166+
Gyroscope calibration matrix (default: identity).
167+
W_f : array-like, shape (3, 3), optional
168+
Accelerometer calibration matrix (default: identity).
169+
b_w : array-like, shape (3,), optional
170+
Gyroscope bias vector (default: zero).
171+
b_f : array-like, shape (3,), optional
172+
Accelerometer bias vector (default: zero).
173+
bias_alt : bool, default False
174+
If set to ``True``, the bias definition of the alternative calibration model
175+
is returned. See Notes.
176+
177+
Notes
178+
-----
179+
The calibration model is defined as::
180+
181+
xyz_ref = W @ xyz + bias
182+
183+
The alternative calibration model where biases are added first is defined as::
184+
185+
xyz_ref = W @ (xyz + bias)
186+
187+
The alternative model is enabled by setting ``bias_alt=True``.
188+
"""
189+
190+
def __init__(
191+
self,
192+
fs,
193+
W_w: np.ndarray = np.eye(3),
194+
W_f: np.ndarray = np.eye(3),
195+
b_w: np.ndarray = np.zeros(3),
196+
b_f: np.ndarray = np.zeros(3),
197+
bias_alt: bool = False,
198+
):
199+
adj_W_w, W_w_det = _adjugate_and_det_3_by_3(W_w)
200+
if W_w_det == 0:
201+
raise ValueError("W_w must be invertible")
202+
W_w_inv = adj_W_w / W_w_det
203+
self.cof_W = W_w_inv.T * W_w_det
204+
self.W_star = W_w_inv @ W_f
205+
if bias_alt:
206+
self.b_f_star = b_f
207+
self.b_w_star = b_w
208+
else:
209+
adj_W_f, W_f_det = _adjugate_and_det_3_by_3(W_f)
210+
if W_f_det == 0:
211+
raise ValueError("W_f must be invertible.")
212+
W_f_inv = adj_W_f / W_f_det
213+
self.b_f_star = W_f_inv @ b_f
214+
self.b_w_star = W_w_inv @ b_w
215+
self.W_w = W_w
216+
super().__init__(fs)
217+
218+
def update(self, f, w, degrees=False):
219+
f_adjusted = self.W_star @ (f + self.b_f_star)
220+
w_adjusted = w + self.b_w_star
221+
super().update(f_adjusted, w_adjusted, degrees)
222+
223+
def _calc_dtheta_dvel(self, degrees=False):
224+
dtheta = self.W_w @ self._theta + self.cof_W @ self._dtheta_con
225+
dtheta = np.degrees(dtheta) if degrees else dtheta
226+
227+
dvel = self.W_w @ self._vel + self.cof_W @ (self._dvel_rot + self._dvel_scul)
228+
return dtheta, dvel

src/smsfusion/_vectorops.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,47 @@ def _skew_symmetric(a: NDArray[np.float64]) -> NDArray[np.float64]:
9090
Skew symmetric matrix.
9191
"""
9292
return np.array([[0.0, -a[2], a[1]], [a[2], 0.0, -a[0]], [-a[1], a[0], 0.0]])
93+
94+
95+
@njit # type: ignore[misc]
96+
def _adjugate_and_det_3_by_3(
97+
m: NDArray[np.float64],
98+
) -> tuple[NDArray[np.float64], float]:
99+
"""
100+
Calculates and returns the adjugate matrix and determinant of the matrix
101+
```python
102+
m = [[a, b, c],
103+
[d, e, f],
104+
[g, h, i]]
105+
```
106+
If the determinant is non-zero, one can calculate the inverse of m as
107+
``inv(m) = adj(m) / det(m)``.
108+
109+
Parameters
110+
----------
111+
m : numpy.ndarray, shape (3, 3)
112+
The matrix for which to calculate the adjugate and determinant.
113+
114+
Returns
115+
-------
116+
adj_m : numpy.ndarray, shape (3, 3)
117+
The adjugate of the input matrix m.
118+
det_m : float
119+
The determinant of the input matrix m.
120+
"""
121+
a, b, c = m[0]
122+
d, e, f = m[1]
123+
g, h, i = m[2]
124+
A = e * i - f * h
125+
B = -(d * i - f * g)
126+
C = d * h - e * g
127+
D = -(b * i - c * h)
128+
E = a * i - c * g
129+
F = -(a * h - b * g)
130+
G = b * f - c * e
131+
H = -(a * f - c * d)
132+
I_ = a * e - b * d
133+
# Equations fetched from https://en.wikipedia.org/wiki/Invertible_matrix#Inversion_of_3_%C3%97_3_matrices
134+
adj_m = np.array([[A, D, G], [B, E, H], [C, F, I_]])
135+
det_m = a * A + b * B + c * C
136+
return adj_m, det_m

0 commit comments

Comments
 (0)