Skip to content

A field-model error the plant sees and the filter does not - #236

Open
patrickmckeen wants to merge 5 commits into
feat/sensor-bias-instabilityfrom
feat/magnetometer-field-model-error
Open

patrickmckeen wants to merge 5 commits into
feat/sensor-bias-instabilityfrom
feat/magnetometer-field-model-error

Conversation

@patrickmckeen

@patrickmckeen patrickmckeen commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

The problem

The simulation's "true" magnetic field and the estimator's field model are the same IGRF evaluation, so a magnetometer in a simulation is only ever wrong by its own noise and bias, and a torquer always produces exactly the torque the model predicts. On a real spacecraft the model itself is wrong: IGRF leaves out the crustal and external fields, and the difference at low Earth orbit is typically 100 to 300 nT, changing slowly as the spacecraft moves. That error is in the environment, so it is the same for every magnetometer on board and the torquers act on it too; what is off is the filter's prediction of both. Any study of magnetometer-based attitude accuracy or magnetic control without it is optimistic.

What this adds

FieldModelError(std, correlation_time): an error field in the inertial frame, one independent first-order Gauss-Markov process per axis, with the given standard deviation in tesla and correlation time in seconds. It starts in its stationary distribution, decays towards zero over the correlation time and is topped up each step so its standard deviation stays where configured, for any step length. Reasonable values for low Earth orbit are a few hundred nanotesla and a few hundred seconds (the time to move a few hundred kilometres), and the docstring says so. This is a stand-in for a model error, not a model of it: it has the right size and the right slowness, nothing more.

The satellite owns it: Satellite(field_error=FieldModelError(...)). It is stepped once per plant step with the other error models (update_errors), and the whole plant sees the same realisation:

  • every magnetometer reads the model field plus the error (rotated into the body frame), in its clean reading too, since this is the environment and not the device, with the Jacobian following;
  • the magnetorquers and the magnetic disturbances (act_torque, dist_torques, and so the dynamics) act on the model field plus the error. The plant's orbital state is replaced by a copy with the offset field inside those two methods, so the caller's orbital state is never touched.

The estimator's clone of the satellite has no error field, on the satellite or its sensors, because the model error is by definition what the model does not have. A magnetometer can still be given a FieldModelError directly when used on its own.

This stacks on #235.

Tests

testing/test_sensors/test_field_model_error.py, 6 tests: the error field is stationary with the configured standard deviation and lag-one correlation and its axes are independent (20 000 steps); three magnetometers sharing one instance read one consistent error at one epoch and it steps once when the epoch changes; the analytic Jacobian matches a central-difference Jacobian with the error and a misalignment in place; the estimator's clone has no error field; the validation; and, at satellite level, that the readings, the torquer torque, the magnetic disturbance torque and the dynamics all use the same true field while the estimator's clone uses the model field, with the caller's orbital state untouched and one realisation per step. The full suite passes.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Test Results

1 604 tests  +6   1 602 ✅ +6   3m 49s ⏱️ + 1m 58s
    1 suites ±0       2 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit cd2ab4d. ± Comparison against base commit 2be7599.

♻️ This comment has been updated with latest results.

@patrickmckeen patrickmckeen changed the title A field-model error the plant's magnetometers see and the filter does not A field-model error the plant sees and the filter does not Oct 16, 2026
@nscheuer

Copy link
Copy Markdown
Owner

I would prefer different field models for the estimator and the environment rather than introducing a new class that is hyperspecific for this problem

@patrickmckeen
patrickmckeen force-pushed the feat/sensor-bias-instability branch from a82a3e9 to e1bb83d Compare October 22, 2026 14:54
@patrickmckeen
patrickmckeen force-pushed the feat/magnetometer-field-model-error branch from 1100f6a to 930a134 Compare October 22, 2026 14:54
t and others added 5 commits October 26, 2026 12:49
… sample rate

Noise gains an optional density, in reading units per root hertz (the
angle random walk of a gyro, the noise density of a magnetometer data
sheet). A reading averaged over the sensor's sample_time then has standard
deviation sqrt(std_noise^2 + density^2 / sample_time), so the noise no
longer silently changes when the simulation step changes. The sensor
passes its sample time both when it draws and when the estimator asks for
the measurement covariance, so both sides agree; the warm start uses the
same standard deviation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every sensor takes a scale_factor (its reading is that factor times a
perfect device's) and a misalignment (a rotation vector from the nominal
sensor frame to the actual one). A single-axis sensor reads along its
sensitive axis as mounted; a vector sensor reports in its frame as
mounted; the quaternion star tracker reports the attitude of its own
frame. The sensors' Jacobians follow the device as built. The estimator's
clone of a satellite is reset to the nominal device, since these errors
are exactly what the filter does not know.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bias keeps three parts and their sum: the constant offset the device was
built with, a random walk of rate std_bias that now starts at zero rather
than at the offset, and a bias instability modelled as a first-order
Gauss-Markov process (instability_std, instability_time), the slow wander
an Allan-variance plot shows as its floor. The estimator's random-walk
rate covers the instability with the usual 2 sigma^2 / tau term. A bias
built the old way produces the same sequence as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the filter does not

FieldModelError is a slowly varying error field in the inertial frame
(one first-order Gauss-Markov process per axis, std in tesla, correlation
time in seconds), shared by a spacecraft's magnetometers, standing in for
the difference between the real geomagnetic field and the IGRF model that
the simulation's truth and the estimator otherwise share exactly. A
magnetometer given one reads the model field plus the error, its Jacobian
follows, and the estimator's clone of the satellite drops it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the torquers act on it too

The satellite owns the FieldModelError (Satellite(field_error=...)): it is
stepped once per plant step with the other error models, every
magnetometer reads it, and the magnetorquers and magnetic disturbances
act on the model field plus the error (a copy of the orbital state with
the offset field is used inside act_torque and dist_torques, so the
caller's state is untouched). The estimator's clone has none of it. Until
now only the readings saw the error while the torque used the model
field, which understated the effect of model error on magnetic control.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@patrickmckeen
patrickmckeen force-pushed the feat/magnetometer-field-model-error branch 2 times, most recently from 930a134 to cd2ab4d Compare October 26, 2026 20:08
@patrickmckeen
patrickmckeen force-pushed the feat/sensor-bias-instability branch 2 times, most recently from 2be7599 to 683cab8 Compare October 27, 2026 17:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants