Skip to content

Commit 09cec81

Browse files
committed
fix tests
1 parent 986e313 commit 09cec81

2 files changed

Lines changed: 309 additions & 1 deletion

File tree

src/smsfusion/_ins/_ahrs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def _measurement_matrix_init(q_nb: NDArray[np.float64]) -> NDArray[np.float64]:
155155
156156
Returns
157157
-------
158-
ndarray, shape (4, 6)
158+
ndarray, shape (4, 9)
159159
Linearized measurement matrix.
160160
"""
161161
dhdx = np.zeros((4, 9))

tests/test_ins_/test_ahrs.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import numpy as np
2+
import pytest
3+
from scipy.signal import resample_poly
4+
5+
import smsfusion as sf
6+
from smsfusion._ins._ahrs import AHRS, _state_transition_matrix_init, _state_transition_matrix_update, _measurement_matrix_init, _reset, _process_noise_covariance_matrix
7+
from smsfusion.benchmark import (
8+
benchmark_pure_attitude_beat_202311A,
9+
benchmark_pure_attitude_chirp_202311A
10+
)
11+
12+
13+
def test_state_transition_matrix_init():
14+
dt = 0.1
15+
dvel = np.ones(3) * 0.01
16+
dtheta = np.ones(3) * 0.02
17+
R_nb = np.eye(3)
18+
gbc = 0.01
19+
20+
phi_out = _state_transition_matrix_init(dt, dvel, dtheta, R_nb, gbc)
21+
phi_expected = np.array([
22+
[1.0, 0.0, 0.0, 0.0, 0.01, -0.01, 0.0, 0.0, 0.0],
23+
[0.0, 1.0, 0.0, -0.01, 0.0, 0.01, 0.0, 0.0, 0.0],
24+
[0.0, 0.0, 1.0, 0.01, -0.01, 0.0, 0.0, 0.0, 0.0],
25+
[0.0, 0.0, 0.0, 1.0, 0.02, -0.02, -dt, 0.0, 0.0],
26+
[0.0, 0.0, 0.0, -0.02, 1.0, 0.02, 0.0, -dt, 0.0],
27+
[0.0, 0.0, 0.0, 0.02, -0.02, 1.0, 0.0, 0.0, -dt],
28+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 - dt / gbc, 0.0, 0.0],
29+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 - dt / gbc, 0.0],
30+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 - dt / gbc],
31+
])
32+
33+
np.testing.assert_almost_equal(phi_out, phi_expected)
34+
35+
36+
def test_state_transition_matrix_update():
37+
dt = 0.1
38+
dvel = np.ones(3) * 0.01
39+
dtheta = np.ones(3) * 0.02
40+
R_nb = np.eye(3)
41+
gbc = 0.01
42+
43+
phi_init = _state_transition_matrix_init(dt, dvel, dtheta, R_nb, gbc)
44+
45+
dtheta_update = np.ones(3) * 0.01
46+
dvel_update = np.ones(3) * 0.1
47+
phi_out = _state_transition_matrix_update(
48+
phi_init,
49+
dvel=dvel_update,
50+
dtheta=dtheta_update,
51+
R_nb=R_nb
52+
)
53+
54+
phi_expected = np.array([
55+
[1.0, 0.0, 0.0, 0.0, 0.1, -0.1, 0.0, 0.0, 0.0],
56+
[0.0, 1.0, 0.0, -0.1, 0.0, 0.1, 0.0, 0.0, 0.0],
57+
[0.0, 0.0, 1.0, 0.1, -0.1, 0.0, 0.0, 0.0, 0.0],
58+
[0.0, 0.0, 0.0, 1.0, 0.01, -0.01, -dt, 0.0, 0.0],
59+
[0.0, 0.0, 0.0, -0.01, 1.0, 0.01, 0.0, -dt, 0.0],
60+
[0.0, 0.0, 0.0, 0.01, -0.01, 1.0, 0.0, 0.0, -dt],
61+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 - dt / gbc, 0.0, 0.0],
62+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 - dt / gbc, 0.0],
63+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 - dt / gbc],
64+
])
65+
66+
np.testing.assert_almost_equal(phi_out, phi_expected)
67+
68+
69+
def test_measurement_matrix_init():
70+
q_nb = np.array([1.0, 0.0, 0.0, 0.0])
71+
72+
expect = np.zeros((4, 9))
73+
expect[0:3, 0:3] = np.eye(3)
74+
expect[3, 3:6] = np.array([0.0, 0.0, 1.0])
75+
# kappa -> zero due to unit quat
76+
77+
np.testing.assert_array_equal(_measurement_matrix_init(q_nb), expect)
78+
79+
80+
def test_process_noise_covariance_matrix():
81+
dt = 0.1
82+
vrw = 0.0005
83+
arw = 0.00005
84+
gbs = 0.00005
85+
gbc = 50.0
86+
Q_out = _process_noise_covariance_matrix(dt, vrw, arw, gbs, gbc)
87+
Q_expect = np.array([
88+
[dt * vrw**2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
89+
[0.0, dt * vrw**2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
90+
[0.0, 0.0, dt * vrw**2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
91+
[0.0, 0.0, 0.0, dt * arw**2, 0.0, 0.0, 0.0, 0.0, 0.0],
92+
[0.0, 0.0, 0.0, 0.0, dt * arw**2, 0.0, 0.0, 0.0, 0.0],
93+
[0.0, 0.0, 0.0, 0.0, 0.0, dt * arw**2, 0.0, 0.0, 0.0],
94+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, dt * (2.0 * gbs**2 / gbc), 0.0, 0.0],
95+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, dt * (2.0 * gbs**2 / gbc), 0.0],
96+
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, dt * (2.0 * gbs**2 / gbc)],
97+
])
98+
99+
np.testing.assert_allclose(Q_out, Q_expect)
100+
101+
def test_reset():
102+
v_n = np.array([0.0, 0.1, 0.0])
103+
q_nb = np.array([1.0, 0.0, 0.0, 0.0])
104+
bg_b = np.zeros(3)
105+
dx = np.array([0.1, 0.0, 0.0, 0.01, 0.0, 0.0, 0.1, -0.1, 0.2])
106+
107+
dx, v_n, q_nb, bg_b = _reset(dx, v_n, q_nb, bg_b)
108+
109+
np.testing.assert_allclose(dx, np.zeros_like(dx))
110+
np.testing.assert_allclose(v_n, np.array([0.1, 0.1, 0.0]))
111+
np.testing.assert_allclose(bg_b, np.array([0.1, -0.1, 0.2]))
112+
np.testing.assert_allclose(q_nb, np.array([np.cos(0.01/2), np.sin(0.01/2), 0.0, 0.0]), atol=1e-6)
113+
114+
115+
def test_ahrs_init():
116+
mekf = AHRS(
117+
10.0
118+
)
119+
np.testing.assert_allclose(mekf.velocity(), np.zeros(3))
120+
np.testing.assert_allclose(mekf.quaternion(), np.array([1.0, 0.0, 0.0, 0.0]))
121+
np.testing.assert_allclose(mekf.bias_gyro(), np.zeros(3))
122+
np.testing.assert_allclose(mekf.P, np.array(sf._ins._ahrs.P0))
123+
assert mekf._g == 9.80665
124+
assert mekf._nav_frame == "ned"
125+
126+
127+
@pytest.mark.parametrize("nav_frame, scale", (["NED", 1.0], ["ENU", -1.0]))
128+
def test_ahrs_nav_frame(nav_frame, scale):
129+
mekf = AHRS(
130+
10.0,
131+
nav_frame=nav_frame
132+
)
133+
134+
assert mekf._nav_frame == nav_frame.lower()
135+
np.testing.assert_allclose(mekf._g_n, np.array([0.0, 0.0, mekf._g * scale]))
136+
137+
138+
def test_ahrs_methods():
139+
vel_init = np.array([0.0, 0.1, -0.2])
140+
euler_init = np.array([10.0, 20.0, 30.0])
141+
quaternion_init = sf.quaternion_from_euler(euler_init, degrees=True)
142+
bg_init = np.array([0.01, -0.01, 0.02])
143+
144+
mekf = AHRS(
145+
10.0,
146+
v=vel_init,
147+
q=quaternion_init,
148+
bg=bg_init
149+
)
150+
151+
np.testing.assert_allclose(mekf.velocity(), vel_init)
152+
np.testing.assert_allclose(mekf.euler(), np.radians(euler_init))
153+
np.testing.assert_allclose(mekf.euler(degrees=True), euler_init)
154+
np.testing.assert_allclose(mekf.quaternion(), quaternion_init)
155+
np.testing.assert_allclose(mekf.bias_gyro(), bg_init)
156+
np.testing.assert_allclose(mekf.bias_gyro(degrees=True), np.degrees(bg_init))
157+
158+
159+
@pytest.mark.parametrize(
160+
"benchmark_gen, degrees",
161+
[(benchmark_pure_attitude_beat_202311A, False), (benchmark_pure_attitude_chirp_202311A, True)],
162+
)
163+
def test_ahrs_no_head_aiding_benchmark(benchmark_gen, degrees):
164+
fs_imu = 100.0
165+
warmup = int(fs_imu * 600.0) # truncate 600 seconds from the beginning
166+
167+
# Reference signals (without noise)
168+
t, euler_ref, acc_ref, gyro_ref = benchmark_gen(fs_imu)
169+
170+
# IMU measurements (with noise)
171+
bg = np.array([0.01, -0.02, 0.0])
172+
noise_model = sf.noise.IMUNoise(
173+
err_acc=sf.constants.ERR_ACC_MOTION2,
174+
err_gyro=sf.constants.ERR_GYRO_MOTION2,
175+
seed=0,
176+
)
177+
imu_noise = noise_model(fs_imu, len(t))
178+
acc_noise = acc_ref + imu_noise[:, :3]
179+
gyro_noise = gyro_ref + imu_noise[:, 3:] + bg
180+
181+
if degrees:
182+
gyro_noise = np.degrees(gyro_noise)
183+
184+
# MEKF
185+
q0 = sf.quaternion_from_euler(euler_ref[0], degrees=False)
186+
mekf = AHRS(
187+
fs_imu,
188+
q=q0,
189+
gyro_noise_density=sf.constants.ERR_GYRO_MOTION2["N"],
190+
gyro_bias_stability=sf.constants.ERR_GYRO_MOTION2["B"],
191+
gyro_bias_corr_time=sf.constants.ERR_GYRO_MOTION2["tau_cb"],
192+
)
193+
194+
# Apply filter
195+
euler_out, bias_gyro_out = [], []
196+
for i, (f_i, w_i) in enumerate(
197+
zip(acc_noise, gyro_noise)
198+
):
199+
200+
dvel = f_i / fs_imu
201+
dtheta = w_i / fs_imu
202+
203+
mekf.update(
204+
dvel,
205+
dtheta,
206+
degrees=degrees,
207+
)
208+
209+
euler_out.append(mekf.euler(degrees=False))
210+
bias_gyro_out.append(mekf.bias_gyro(degrees=False))
211+
212+
euler_out = np.array(euler_out)
213+
bias_gyro_out = np.array(bias_gyro_out)
214+
215+
# Half-sample shift (compensates for the delay introduced by Euler integration)
216+
euler_out = resample_poly(euler_out, 2, 1)[1:-1:2]
217+
euler_ref = euler_ref[:-1, :]
218+
219+
roll_rms, pitch_rms, yaw_rms = np.std((euler_out - euler_ref)[warmup:], axis=0)
220+
bias_gyro_x_rms, bias_gyro_y_rms, bias_gyro_z_rms = np.std(
221+
(bias_gyro_out - bg)[warmup:], axis=0
222+
)
223+
224+
assert np.degrees(roll_rms) <= 0.1
225+
assert np.degrees(pitch_rms) <= 0.1
226+
assert np.degrees(bias_gyro_x_rms) <= 0.005
227+
assert np.degrees(bias_gyro_y_rms) <= 0.005
228+
229+
230+
@pytest.mark.parametrize(
231+
"benchmark_gen, degrees",
232+
[(benchmark_pure_attitude_beat_202311A, False), (benchmark_pure_attitude_chirp_202311A, True)],
233+
)
234+
def test_ahrs_benchmark(benchmark_gen, degrees):
235+
fs_imu = 100.0
236+
warmup = int(fs_imu * 600.0) # truncate 600 seconds from the beginning
237+
238+
# Reference signals (without noise)
239+
t, euler_ref, acc_ref, gyro_ref = benchmark_gen(fs_imu)
240+
241+
# IMU measurements (with noise)
242+
bg = np.array([0.01, -0.02, 0.0])
243+
noise_model = sf.noise.IMUNoise(
244+
err_acc=sf.constants.ERR_ACC_MOTION2,
245+
err_gyro=sf.constants.ERR_GYRO_MOTION2,
246+
seed=0,
247+
)
248+
imu_noise = noise_model(fs_imu, len(t))
249+
acc_noise = acc_ref + imu_noise[:, :3]
250+
gyro_noise = gyro_ref + imu_noise[:, 3:] + bg
251+
252+
head_std = np.radians(1.0)
253+
head_noise = euler_ref[:, -1] + np.random.normal(0., head_std, len(euler_ref))
254+
255+
if degrees:
256+
gyro_noise = np.degrees(gyro_noise)
257+
head_noise = np.degrees(head_noise)
258+
head_std = np.degrees(head_std)
259+
260+
# MEKF
261+
q0 = sf.quaternion_from_euler(euler_ref[0], degrees=False)
262+
mekf = AHRS(
263+
fs_imu,
264+
q=q0,
265+
gyro_noise_density=sf.constants.ERR_GYRO_MOTION2["N"],
266+
gyro_bias_stability=sf.constants.ERR_GYRO_MOTION2["B"],
267+
gyro_bias_corr_time=sf.constants.ERR_GYRO_MOTION2["tau_cb"],
268+
)
269+
270+
# Apply filter
271+
euler_out, bias_gyro_out = [], []
272+
for i, (f_i, w_i, head_i) in enumerate(
273+
zip(acc_noise, gyro_noise, head_noise)
274+
):
275+
276+
dvel = f_i / fs_imu
277+
dtheta = w_i / fs_imu
278+
279+
mekf.update(
280+
dvel,
281+
dtheta,
282+
degrees=degrees,
283+
head=head_i,
284+
head_degrees=degrees,
285+
head_var=head_std**2,
286+
)
287+
288+
euler_out.append(mekf.euler(degrees=False))
289+
bias_gyro_out.append(mekf.bias_gyro(degrees=False))
290+
291+
euler_out = np.array(euler_out)
292+
bias_gyro_out = np.array(bias_gyro_out)
293+
294+
# Half-sample shift (compensates for the delay introduced by Euler integration)
295+
euler_out = resample_poly(euler_out, 2, 1)[1:-1:2]
296+
euler_ref = euler_ref[:-1, :]
297+
298+
roll_rms, pitch_rms, yaw_rms = np.std((euler_out - euler_ref)[warmup:], axis=0)
299+
bias_gyro_x_rms, bias_gyro_y_rms, bias_gyro_z_rms = np.std(
300+
(bias_gyro_out - bg)[warmup:], axis=0
301+
)
302+
303+
assert np.degrees(roll_rms) <= 0.1
304+
assert np.degrees(pitch_rms) <= 0.1
305+
assert np.degrees(yaw_rms) <= 0.1
306+
assert np.degrees(bias_gyro_x_rms) <= 0.005
307+
assert np.degrees(bias_gyro_y_rms) <= 0.005
308+
assert np.degrees(bias_gyro_z_rms) <= 0.005

0 commit comments

Comments
 (0)