11import numpy as np
22from 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
77class 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
0 commit comments