A 3U CubeSat attitude simulator that takes the spacecraft from a 10 deg/s tumble to nadir pointing: B-dot detumbling with magnetorquers, TRIAD initialisation, a multiplicative EKF with gyro-bias estimation, and quaternion-feedback PD on reaction wheels with magnetic momentum dumping. About 1100 lines of NumPy/SciPy. In the last orbit of the nominal run the pointing error is 0.55 deg at p95 (0.32 deg in sunlight), and every number in this README comes from a test or a script in the repo.
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -s # ~1 min, prints the numbers
python scripts/run_mission.py # nominal 4-orbit mission -> docs/figures, docs/results.json
python scripts/monte_carlo.py 50 # sequential Monte Carlo -> docs/monte_carlo.json (~8 min)
python scripts/monte_carlo.py --plot-only # redraw from results/mc_runs.npzWith pip install -e ".[igrf]" and SimConfig(field_model="igrf") the truth flies through the full IGRF-14 field while the on-board model stays a dipole, which is useful for studying model mismatch.
The truth model (rigid body, wheels, orbit, environment) is integrated with RK4 at 4 Hz. Sensors and flight software run at 1 Hz, as on most CubeSat OBCs. DETUMBLE runs B-dot until the gyro-measured rate stays below 0.5 deg/s for 120 s and the Sun is visible; TRIAD then seeds the MEKF and the spacecraft switches to POINTING (nadir/LVLH). With no Sun there is no switch, because TRIAD needs two non-parallel vectors.
These checks live in tests/:
| Check | Result |
|---|---|
| Torque-free energy / inertial angular momentum, 1 orbit, RK4 @ 0.25 s | rel. drift 7.4e-9 / 2.3e-8 |
| RK4 global order (step halving) | error ratio 15.98, 16.00 (theory: 16) |
| Quaternion norm | |q|−1 ≤ 2.2e-16 |
| B-dot from 8.8 deg/s | < 0.5 deg/s in 1.19 orbits |
| MEKF consistency (8-run open-loop MC) | ≥ 98 % inside ±3σ, ANEES ≈ n = 6 |
| Pointing, last orbit (truth vs LVLH) | p95 0.55 deg (0.32 deg sunlit) |
Conventions follow Markley & Crassidis (2014) throughout: scalar-last quaternion
The bus carries three orthogonal reaction wheels with momentum
The quaternion is re-projected onto
The orbit is circular, 500 km,
Sensors: magnetometer at σ = 100 nT/axis; Sun sensor at σ = 0.5°/axis with full-sky FOV, unavailable in eclipse; a Farrenkopf gyro with ARW σ_v = 8.7e-5 rad/√s (0.30 °/√h), RRW σ_u = 1e-6 rad/s^1.5 and β₀ ~ 0.05 °/s/axis. The gyro is sampled as a delta-angle sensor (rate averaged over the 1 s interval) with the exact discrete noise model of M&C eq. 6.90, which is what the filter's discrete propagation assumes.
B-dot uses only the magnetometer, with the near-optimal gain of Avanzini & Giulietti (2012),
The dipole saturates at 0.2 A m² per axis by scaling the whole vector, so its direction is kept. Pointing uses quaternion feedback PD with gyroscopic compensation (Wie 2008) toward the LVLH frame:
with
The estimator is the MEKF of Markley & Crassidis, sec. 6.2. Its error state is
a Joseph-form covariance update and
Nominal run from scripts/run_mission.py: seed 1, 4 orbits, initial rate (6, −5, 4) deg/s, random initial attitude. Grey bands are eclipses.
The rate falls from 8.8 deg/s to the 0.5 deg/s exit threshold in 1.19 orbits. B-dot on its own cannot go below about
After MEKF initialisation the PD slews ~130° to nadir in ~80 s (< 1° after 77 s). From then on pointing error follows knowledge error almost exactly, so attitude determination sets the limit and control does not. In sunlight both are ~0.1–0.3°. In eclipse the magnetometer is the only vector sensor, rotation about the field line is held only by gyro propagation, and the error grows to 1–3° until sunrise.
The ±3σ bounds open in shadow and collapse at sunrise, tracking the error. Attitude and bias errors stay inside ±3σ 98.4–99.9 % of the time, mean NEES 5.90 (expected 6).
Wheel momentum peaks at 2.1 mN m s during the acquisition slew (21 % of capacity). Afterwards dumping holds
The Monte Carlo runs 50 dispersed cases of 3 orbits each:
| Per run, over 50 runs | median | p95 | max |
|---|---|---|---|
| Detumble time to mode switch [orbits] (mean 1.08) | – | 1.69 | 1.75 |
| MEKF init to < 1° pointing [s] | 79 | 206 | 255 |
| Pointing error p95, last orbit, incl. eclipse [deg] | 0.70 | 1.33 | 1.41 |
| Pointing error p95, last orbit, sunlit [deg] | 0.27 | 0.45 | 0.50 |
| Knowledge error RMS, last orbit [deg] | 0.29 | 0.48 | 0.53 |
| Peak wheel momentum [mN m s] (capacity 10) | 1.87 | 2.88 | 3.12 |
All 50 runs detumbled and acquired nadir. All meet < 0.5° (p95) in sunlight. A stricter < 1° target including eclipse (the tested requirement is < 2°) is missed by 8 of 50 runs, max 1.41°, which is the single-vector eclipse limit described above.
A 4-orbit closed-loop run takes ~13 s in plain NumPy on a laptop. There is no numba or multiprocessing, and the Monte Carlo is sequential to keep memory flat.
- Field model is a tilted dipole. Against IGRF-14 on this orbit the direction error is 6° median / 35° max and
$\lVert\mathbf B\rVert$ is off by up to −42 % (South Atlantic Anomaly). The IGRF frame conversion matches the dipole within 0.4° at degree 1 (tests/test_environment.py). Withfield_model="igrf"the mismatch is not in$R$ , so the filter is expected to go inconsistent. - No magnetometer/magnetorquer interference or duty-cycling.
- Ideal actuators: no wheel friction, jitter, speed-dependent limits, MTQ dynamics or quantisation; no wheel redundancy.
- Circular two-body orbit (no
$J_2$ , no decay), no solar activity, no SRP, no albedo, no penumbra. - Sun sensor has full-sky FOV; no sensor misalignment, scale factor or magnetometer bias estimation.
- Eclipse knowledge error reaches 1–3° with one vector sensor; a star tracker or Earth sensor would fix it.
- Not flight software: no FDIR, no safe mode, no fixed-point or timing concerns.
Code is in src/cubeadcs/ (dynamics.py, environment.py, sensors.py, control.py, estimation.py, sim.py), tests in tests/, entry points in scripts/.
- F. L. Markley, J. L. Crassidis, Fundamentals of Spacecraft Attitude Determination and Control, Springer, 2014. doi:10.1007/978-1-4939-0802-8
- B. Wie, Space Vehicle Dynamics and Control, 2nd ed., AIAA Education Series, 2008.
- G. Avanzini, F. Giulietti, "Magnetic Detumbling of a Rigid Spacecraft", Journal of Guidance, Control, and Dynamics, 35(4), 2012.
- H. D. Black, "A Passive System for Determining the Attitude of a Satellite", AIAA Journal, 2(7), 1964.
- R. L. Farrenkopf, "Analytic Steady-State Accuracy Solutions for Two Common Spacecraft Attitude Estimators", Journal of Guidance and Control, 1(4), 1978.
- D. A. Vallado, Fundamentals of Astrodynamics and Applications, 4th ed., Microcosm Press, 2013.
- E. J. Lefferts, F. L. Markley, M. D. Shuster, "Kalman Filtering for Spacecraft Attitude Estimation", Journal of Guidance, Control, and Dynamics, 5(5), 1982.
- P. Alken et al., "International Geomagnetic Reference Field: the thirteenth generation", Earth, Planets and Space, 73:49, 2021.
- Y. Bar-Shalom, X. R. Li, T. Kirubarajan, Estimation with Applications to Tracking and Navigation, Wiley, 2001.
MIT © 2026 Tommaso Maggiolo. See LICENSE and CITATION.cff.





