Complementary Filter (gnc-autonomy/estimation-filtering/complementary-filter)
Use when the task is an explicit (Mahony-style) complementary filter
attitude observer on the special orthogonal group SO(3): fusing the
high-rate rate-gyro angular rates (which drift) with low-rate absolute
vector measurements (sun sensor and magnetometer, which do not) into a
continuous attitude estimate with online gyro bias estimation. This leaf
implements the Mahony explicit complementary filter structure in pure
Python, stdlib only: per vector pair the cross-product innovation drives
a proportional correction and an integral bias update, and the corrected
rate propagates the unit attitude quaternion by RK4 with renormalization.
It pairs with the static two-vector attitude determination leaf in
space-systems/adcs for single-shot attitude from a vector pair, with the
stochastic estimation leaves of this pack for full navigation filters,
and with attitude-dynamics for propagation only. The frequency-domain
intuition is a high-pass gyro channel (fast but drifting) fused with a
low-pass vector channel (absolute but slow): the filter passes the gyro
through at high frequency and the vector correction through at low
frequency, so the estimate is both agile and drift-free.
Domain quick reference
- State: the unit attitude quaternion q (scalar first, [w, x, y, z]) and
the gyro bias vector b in rad/s. Measured rate: omega_m = omega_true +
b + noise. Reference-frame vector r_i, body measurement
m_i = R(q_true)^T r_i + noise.
- Convention (documented, used everywhere): R(q) is the active rotation
matrix, so the filter predicts the body-frame direction of a reference
vector as the inverse rotation v_i = R(q_est)^T r_i, which reproduces
the body measurement exactly when the estimate is perfect.
- Vector innovation: e = sum_i (m_i x v_i), cross product of the body
measurement with the predicted body vector; the small-angle attitude
error in the body frame, summed over the pairs available at the step.
- Corrected rate: omega_c = omega_m - b_est + k_p * e, with the
proportional gain k_p in 1/s (module default 2.0).
- Bias dynamics: b_dot = -k_i * e, explicit step
b_new = b_est - k_i * e * dt, integral gain k_i in 1/s^2 (module
default 0.4). The bias estimate converges to the true gyro bias, which
removes the drift source of the gyro channel.
- Kinematics: q_dot = 0.5 * q (x) [0, omega_c], the quaternion
multiplication form of the body-rate equation; integrated over the
step by RK4 with the corrected rate held constant, then q is
renormalized to unit norm.
- When no vector measurement is present at a step, e = 0 and the filter
degrades to gyro-only propagation of the kinematics.
- The innovation norm per step is the magnitude of the total cross
product error; the steady-state verdict compares the mean of the last
ten steps against a tolerance.
- SI units throughout: rad, rad/s, rad/s^2, s.
- ARP4754A frames the onboard function development context; the
relations above are standard engineering methodology, summary-only.
Workflow
- Initialize the unit quaternion q0 (normalized when within 1e-6 of
unit norm) and the bias estimate b0, and fix the gains k_p, k_i and
the step dt.
- For each step take the gyro sample omega_m and the vector
measurements available: each pair carries the reference-frame
direction r_i and the body measurement m_i.
- Predict each reference direction in the body frame with
rotate_reference_vector and form the per-pair error with
vector_measurement_error; sum the pair errors (zero when no
measurement is present).
- Form the rate correction with correction_step and the compensated
rate with gyro_compensated_rate, then integrate one step with
propagate_attitude: RK4 on quat_kinematics with renormalization,
plus the explicit bias update via bias_update.
- Loop over the whole sample sequence with run_complementary_filter,
which returns the (q, b) estimate and the innovation norm per step.
- Judge convergence with steady_state_verdict on the innovation norm
sequence at the chosen tolerance.
- Confirm the deterministic checks with the contract test
scripts/test_complementary_filter.py.
Worked example
Spacecraft at a constant true body rate omega_true = [0.01, 0.02,
0.005] rad/s for 100 s at dt = 0.01 s (10000 steps), true gyro bias
b_true = [0.001, -0.0008, 0.0006] rad/s (|b_true| = 1.414e-3 rad/s), so
the gyro reads omega_true + b_true. Reference vectors: sun r1 = [1, 0,
0] and a magnetometer-style reference r2 = [0.6, -0.8, 0], each with a
perfect noiseless body measurement at every step.
- Pure gyro propagation (no measurements): the attitude drifts with the
uncompensated bias. At t = 100 s the attitude error is 0.113 rad, in
the expected band 0.10 to 0.20 rad around the integrated bias angle
|b_true| * 100 s = 0.141 rad; at t = 10 s the error is 0.01411 rad
against |b_true| * 10 s = 0.01414 rad, within 0.5 percent.
- Filtered run with k_p = 2.0 1/s and k_i = 0.4 1/s^2: the innovation
norm stays below 1e-3 rad over the whole run (measured maximum 5.1e-4
rad at t = 2.3 s), reads 4.7e-6 rad at t = 20 s, and decays to about
3e-14 rad at t = 100 s; the steady-state verdict on the 1e-3 rad
tolerance passes.
- Bias convergence: the estimate reaches within 10 percent of b_true by
t = 9.9 s and ends at [0.0010000, -0.0008000, 0.0006000], a relative
error of about 4e-11, so the drift source is removed.
- Attitude tracking: the estimate follows the rotating truth within the
one-step sampling lag |omega_true| * dt = 2.29e-4 rad, because the
measurement at step k reflects the true attitude at step k.
- A single reference vector leaves rotations about the reference sun
line unobservable, so the bias estimate cannot fully converge with one
vector alone; two non-parallel references (as above) make the attitude
and bias observable, which is why the fusion loop takes vector pairs.
Verification
- Confirm quaternion normalization and multiplication identities, and
that quat_to_rotation_matrix gives R R^T = I within 1e-9 and maps a
90 degree rotation correctly.
- Confirm rotate_reference_vector returns R(q)^T r and that the
predicted body direction matches the measurement when the estimate is
perfect.
- Confirm with zero measurement error and zero bias that the corrected
rate equals omega_m and that one propagated step matches the closed
form axis-angle rotation quaternion within 1e-6, norm preserved.
- Confirm the drift anchors: gyro-only attitude error 0.113 rad at
100 s (band 0.10 to 0.20) and 0.01411 rad at 10 s (within 0.5 percent
of the integrated bias angle).
- Confirm the filter anchors: innovation norm below 1e-3 rad for the
whole filtered run, steady-state verdict true, bias estimate within
10 percent of b_true by about 10 s.
- Confirm every non-positive dt, negative gain, non-finite input,
malformed vector, zero-norm quaternion, and initial quaternion outside
the 1e-6 unit-norm tolerance raises ValueError.
- Run the contract test offline: python3
scripts/test_complementary_filter.py (30 tests, deterministic).
Pitfalls
- Running the filter with a single reference vector: one reference leaves
rotations about that vector unobservable, so the bias estimate cannot
fully converge; the fusion loop needs two non-parallel references (the
worked example uses sun plus magnetometer-style vectors).
- Reading the innovation norm as attitude error during no-measurement steps:
when no vector measurement is present e = 0 and the filter degrades to
gyro-only propagation - the innovation norm says nothing about drift on
those steps.
- Starting off the unit quaternion: q0 is normalized only when within 1e-6
of unit norm and an initial quaternion outside that tolerance raises
ValueError; renormalize before every step is part of propagate_attitude.
- Expecting the estimate to lead the truth: the estimate follows the
rotating truth within the one-step sampling lag |omega_true|*dt (2.29e-4
rad in the worked example) because the measurement at step k reflects the
attitude at step k.
- Gyro-only drift anchors are the failure bound: without measurements the
attitude error grows as the integrated bias (0.113 rad at 100 s against
|b_true|*100 s = 0.141 rad), which is the drift the bias estimate removes.
- Non-positive dt, negative gains, non-finite inputs, malformed vectors and
zero-norm quaternions raise ValueError.
Related leaves
- gnc-autonomy/estimation-filtering/alpha-beta-filter: the discrete
tracking-filter relative for position and velocity smoothing.
- gnc-autonomy/space/attitude-dynamics: pure quaternion kinematics
propagation, the gyro channel without the vector correction.
- space-systems/adcs/attitude-determination-triad: static single-shot
attitude from two vector observations, the alternative to continuous
filtering.
- gnc-autonomy/estimation-filtering/extended-kalman-filter: the
stochastic filtering family this pack offers for noise-covariance
aware estimation.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_complementary_filter.py
The test covers the quaternion algebra and rotation matrix identities,
the body-frame reference-vector prediction, the cross-product
innovation, the proportional correction and integral bias update, the
compensated rate, RK4 kinematics against the closed-form axis-angle
step, the worked-example anchors (gyro-only drift of 0.113 rad over
100 s and 0.01411 rad over 10 s, filtered innovation below 1e-3 rad
throughout, bias within 10 percent of true by about 10 s, unit norm
preserved at every step, one-step sampling lag bound), measurement gaps
that fall back to gyro propagation, the pair and list measurement
formats, and ValueError rejection of non-physical inputs.
Compliance
- Standards referenced, not reproduced: ARP4754A is cited as the
development-process frame for onboard attitude functions; the
complementary filter relations above are standard published
engineering methodology, summary-only per standards-map.yaml.
- compliance: STANDARDS-REF, gated: false.
1---2name: complementary-filter3description: Use when you must run a Mahony-style explicit complementary filter attitude observer on so3 to fuse a spacecraft or air-vehicle rate gyro with sun sensor and magnetometer vector measurements into a continuous, drift-free attitude quaternion estimate with online gyro bias estimation: form the cross-product innovation from each body measurement against the reference vector rotated by the estimated attitude, drive the proportional and integral correction gains, update the bias estimate, and integrate the quaternion kinematics with RK4 and renormalization. Produces the attitude and bias time histories, the per-step innovation norm, and the steady-state convergence verdict that gate the pointing and navigation assessment. Trigger: complementary-filter, mahony, gyro-bias-estimation, vector-measurement-fusion, so3-attitude-observer, attitude-quaternion, innovation-norm, rate-gyro.4license: Apache-2.05---67# Complementary Filter (gnc-autonomy/estimation-filtering/complementary-filter)89Use when the task is an explicit (Mahony-style) complementary filter10attitude observer on the special orthogonal group SO(3): fusing the11high-rate rate-gyro angular rates (which drift) with low-rate absolute12vector measurements (sun sensor and magnetometer, which do not) into a13continuous attitude estimate with online gyro bias estimation. This leaf14implements the Mahony explicit complementary filter structure in pure15Python, stdlib only: per vector pair the cross-product innovation drives16a proportional correction and an integral bias update, and the corrected17rate propagates the unit attitude quaternion by RK4 with renormalization.18It pairs with the static two-vector attitude determination leaf in19space-systems/adcs for single-shot attitude from a vector pair, with the20stochastic estimation leaves of this pack for full navigation filters,21and with attitude-dynamics for propagation only. The frequency-domain22intuition is a high-pass gyro channel (fast but drifting) fused with a23low-pass vector channel (absolute but slow): the filter passes the gyro24through at high frequency and the vector correction through at low25frequency, so the estimate is both agile and drift-free.2627## Domain quick reference2829- State: the unit attitude quaternion q (scalar first, [w, x, y, z]) and30 the gyro bias vector b in rad/s. Measured rate: omega_m = omega_true +31 b + noise. Reference-frame vector r_i, body measurement32 m_i = R(q_true)^T r_i + noise.33- Convention (documented, used everywhere): R(q) is the active rotation34 matrix, so the filter predicts the body-frame direction of a reference35 vector as the inverse rotation v_i = R(q_est)^T r_i, which reproduces36 the body measurement exactly when the estimate is perfect.37- Vector innovation: e = sum_i (m_i x v_i), cross product of the body38 measurement with the predicted body vector; the small-angle attitude39 error in the body frame, summed over the pairs available at the step.40- Corrected rate: omega_c = omega_m - b_est + k_p * e, with the41 proportional gain k_p in 1/s (module default 2.0).42- Bias dynamics: b_dot = -k_i * e, explicit step43 b_new = b_est - k_i * e * dt, integral gain k_i in 1/s^2 (module44 default 0.4). The bias estimate converges to the true gyro bias, which45 removes the drift source of the gyro channel.46- Kinematics: q_dot = 0.5 * q (x) [0, omega_c], the quaternion47 multiplication form of the body-rate equation; integrated over the48 step by RK4 with the corrected rate held constant, then q is49 renormalized to unit norm.50- When no vector measurement is present at a step, e = 0 and the filter51 degrades to gyro-only propagation of the kinematics.52- The innovation norm per step is the magnitude of the total cross53 product error; the steady-state verdict compares the mean of the last54 ten steps against a tolerance.55- SI units throughout: rad, rad/s, rad/s^2, s.56- ARP4754A frames the onboard function development context; the57 relations above are standard engineering methodology, summary-only.5859## Workflow60611. Initialize the unit quaternion q0 (normalized when within 1e-6 of62 unit norm) and the bias estimate b0, and fix the gains k_p, k_i and63 the step dt.642. For each step take the gyro sample omega_m and the vector65 measurements available: each pair carries the reference-frame66 direction r_i and the body measurement m_i.673. Predict each reference direction in the body frame with68 rotate_reference_vector and form the per-pair error with69 vector_measurement_error; sum the pair errors (zero when no70 measurement is present).714. Form the rate correction with correction_step and the compensated72 rate with gyro_compensated_rate, then integrate one step with73 propagate_attitude: RK4 on quat_kinematics with renormalization,74 plus the explicit bias update via bias_update.755. Loop over the whole sample sequence with run_complementary_filter,76 which returns the (q, b) estimate and the innovation norm per step.776. Judge convergence with steady_state_verdict on the innovation norm78 sequence at the chosen tolerance.797. Confirm the deterministic checks with the contract test80 scripts/test_complementary_filter.py.8182## Worked example8384Spacecraft at a constant true body rate omega_true = [0.01, 0.02,850.005] rad/s for 100 s at dt = 0.01 s (10000 steps), true gyro bias86b_true = [0.001, -0.0008, 0.0006] rad/s (|b_true| = 1.414e-3 rad/s), so87the gyro reads omega_true + b_true. Reference vectors: sun r1 = [1, 0,880] and a magnetometer-style reference r2 = [0.6, -0.8, 0], each with a89perfect noiseless body measurement at every step.9091- Pure gyro propagation (no measurements): the attitude drifts with the92 uncompensated bias. At t = 100 s the attitude error is 0.113 rad, in93 the expected band 0.10 to 0.20 rad around the integrated bias angle94 |b_true| * 100 s = 0.141 rad; at t = 10 s the error is 0.01411 rad95 against |b_true| * 10 s = 0.01414 rad, within 0.5 percent.96- Filtered run with k_p = 2.0 1/s and k_i = 0.4 1/s^2: the innovation97 norm stays below 1e-3 rad over the whole run (measured maximum 5.1e-498 rad at t = 2.3 s), reads 4.7e-6 rad at t = 20 s, and decays to about99 3e-14 rad at t = 100 s; the steady-state verdict on the 1e-3 rad100 tolerance passes.101- Bias convergence: the estimate reaches within 10 percent of b_true by102 t = 9.9 s and ends at [0.0010000, -0.0008000, 0.0006000], a relative103 error of about 4e-11, so the drift source is removed.104- Attitude tracking: the estimate follows the rotating truth within the105 one-step sampling lag |omega_true| * dt = 2.29e-4 rad, because the106 measurement at step k reflects the true attitude at step k.107- A single reference vector leaves rotations about the reference sun108 line unobservable, so the bias estimate cannot fully converge with one109 vector alone; two non-parallel references (as above) make the attitude110 and bias observable, which is why the fusion loop takes vector pairs.111112## Verification113114- Confirm quaternion normalization and multiplication identities, and115 that quat_to_rotation_matrix gives R R^T = I within 1e-9 and maps a116 90 degree rotation correctly.117- Confirm rotate_reference_vector returns R(q)^T r and that the118 predicted body direction matches the measurement when the estimate is119 perfect.120- Confirm with zero measurement error and zero bias that the corrected121 rate equals omega_m and that one propagated step matches the closed122 form axis-angle rotation quaternion within 1e-6, norm preserved.123- Confirm the drift anchors: gyro-only attitude error 0.113 rad at124 100 s (band 0.10 to 0.20) and 0.01411 rad at 10 s (within 0.5 percent125 of the integrated bias angle).126- Confirm the filter anchors: innovation norm below 1e-3 rad for the127 whole filtered run, steady-state verdict true, bias estimate within128 10 percent of b_true by about 10 s.129- Confirm every non-positive dt, negative gain, non-finite input,130 malformed vector, zero-norm quaternion, and initial quaternion outside131 the 1e-6 unit-norm tolerance raises ValueError.132- Run the contract test offline: python3133 scripts/test_complementary_filter.py (30 tests, deterministic).134135## Pitfalls136137- Running the filter with a single reference vector: one reference leaves138 rotations about that vector unobservable, so the bias estimate cannot139 fully converge; the fusion loop needs two non-parallel references (the140 worked example uses sun plus magnetometer-style vectors).141- Reading the innovation norm as attitude error during no-measurement steps:142 when no vector measurement is present e = 0 and the filter degrades to143 gyro-only propagation - the innovation norm says nothing about drift on144 those steps.145- Starting off the unit quaternion: q0 is normalized only when within 1e-6146 of unit norm and an initial quaternion outside that tolerance raises147 ValueError; renormalize before every step is part of propagate_attitude.148- Expecting the estimate to lead the truth: the estimate follows the149 rotating truth within the one-step sampling lag |omega_true|*dt (2.29e-4150 rad in the worked example) because the measurement at step k reflects the151 attitude at step k.152- Gyro-only drift anchors are the failure bound: without measurements the153 attitude error grows as the integrated bias (0.113 rad at 100 s against154 |b_true|*100 s = 0.141 rad), which is the drift the bias estimate removes.155- Non-positive dt, negative gains, non-finite inputs, malformed vectors and156 zero-norm quaternions raise ValueError.157158## Related leaves159160- gnc-autonomy/estimation-filtering/alpha-beta-filter: the discrete161 tracking-filter relative for position and velocity smoothing.162- gnc-autonomy/space/attitude-dynamics: pure quaternion kinematics163 propagation, the gyro channel without the vector correction.164- space-systems/adcs/attitude-determination-triad: static single-shot165 attitude from two vector observations, the alternative to continuous166 filtering.167- gnc-autonomy/estimation-filtering/extended-kalman-filter: the168 stochastic filtering family this pack offers for noise-covariance169 aware estimation.170171## Behavior contract (gate 3)172173Run the deterministic contract test (stdlib unittest, offline):174175 python3 scripts/test_complementary_filter.py176177The test covers the quaternion algebra and rotation matrix identities,178the body-frame reference-vector prediction, the cross-product179innovation, the proportional correction and integral bias update, the180compensated rate, RK4 kinematics against the closed-form axis-angle181step, the worked-example anchors (gyro-only drift of 0.113 rad over182100 s and 0.01411 rad over 10 s, filtered innovation below 1e-3 rad183throughout, bias within 10 percent of true by about 10 s, unit norm184preserved at every step, one-step sampling lag bound), measurement gaps185that fall back to gyro propagation, the pair and list measurement186formats, and ValueError rejection of non-physical inputs.187188## Compliance189190- Standards referenced, not reproduced: ARP4754A is cited as the191 development-process frame for onboard attitude functions; the192 complementary filter relations above are standard published193 engineering methodology, summary-only per standards-map.yaml.194- compliance: STANDARDS-REF, gated: false.