The SD card comes back before the rocket's story does. Everything downstream of that is arithmetic — raw gyro counts and pressure counts in, a damping ratio and a drag area out. Six stages, fixed order, each one checkable on its own before you trust the next.
No dashboards, no live telemetry, no guessing. Just what actually happened on the way down, reconstructed from what the vehicle logged.
python -m descent.main flight.csv \
--mass 0.35 --riser 0.50 --diameter 0.30 \
--bias-file bias_calibration.csv \
--design-cda 0.15 --out results
python -m descent.main --selftest # synthetic data, known answers
python -m descent.main --selftest --coning| # | module | does |
|---|---|---|
| 1 | load.py |
CSV → typed DataFrame, integrity checks |
| 2 | preprocess.py |
time base → gyro bias → zero-phase filter |
| 3 | oscillation.py |
PCA → motion class → damping identification |
| 4 | aerodynamics.py |
density → altitude → velocity → C_D·A → β |
| 5 | assessment.py |
design intent / consistency / stability |
| 6 | plot.py |
six figures |
| 7 | report.py |
assembles it all into one report.html |
config.py holds constants and the run configuration. synth.py generates
self-test data.
Stage 4 consumes stage 3's output. The C_D·A window is snapped to a whole number of oscillation periods, and C_D·A is corrected for the oscillation bias using the measured modulation. Running the two branches independently is incorrect.
Each stage runs on its own — isolate a bad number before it propagates:
python -m descent.load flight.csv
python -m descent.preprocess flight.csv --bias-file bias.csv --out pre.csv
python -m descent.oscillation flight.csv --bias-file bias.csv
python -m descent.aerodynamics flight.csv --no-oscillation--deploy-ms <int> overrides the deployment instant (a raw t_ms value) on
any of main.py, preprocess.py, oscillation.py, or aerodynamics.py,
instead of inferring it from the accelerometer. It must fall within the
file's t_ms range, snaps to the nearest logged sample, and warns (without
stopping) if it leaves under 20% of the record. How deployment was
determined — inferred or manual, with any snap distance — is recorded in
deployment_method in results.json.
Halt the pipeline mid-way, or keep every stage's output:
python -m descent.main flight.csv --stop-after preprocess
python -m descent.main flight.csv --checkpoint debug/--checkpoint writes stage_1_load.json … stage_5_assessment.json plus the
preprocessed CSV, so a failure in a late stage leaves the earlier ones on disk.
Flight CSV — required columns:
t_ms, gx_dps, gy_dps, gz_dps, pressure_pa, temp_c, rh_pct
optional: ax_ms2, ay_ms2, az_ms2, gps_alt_m
t_ms is milliseconds since system boot, not since deployment — it is what
--deploy-ms is given in terms of.
Raw pressure_pa is required. Air density cannot be reconstructed from an
onboard-derived altitude, and the onboard conversion embeds a fixed
standard-atmosphere temperature that this pipeline replaces with the measured
one — worth ~8% in C_D·A.
A NaN run up to max_nan_gap_samples (config.py, default 15) is bridged by
linear interpolation before anything touches it; longer gaps fail loudly
instead of being fed to the filter. Tunable per-run with --max-nan-gap on
python -m descent.load standalone. See load.py.
Bias calibration CSV — a few seconds of gyro output with the vehicle still,
recorded before flight: gx_dps, gy_dps, gz_dps, temp_c
Every run writes report.html into the output directory, next to the six
figures and results.json — one page, no network calls, opens straight in a
browser off the SD card if that's all you've got at the site. Regenerate the
styling without rerunning the analysis:
python -m descent.report results/Layout lives in report.py, all styling in report.css — plain CSS, no
build step.
Generates a flight whose coefficients are chosen in advance and checks what the pipeline returns:
| quantity | truth | recovered | error |
|---|---|---|---|
| damping ratio ζ | 0.0300 | 0.0309 | 2.9% |
| quadratic coefficient c | 0.2000 | 0.2144 | 7.2% |
| natural frequency | 0.6200 Hz | 0.6202 Hz | 0.04% |
| terminal speed | 6.2996 m/s | 6.2980 m/s | 0.03% |
| drag area C_D·A | 0.1500 m² | 0.1498 m² | 0.16% |
Run it after any change. If Sanctus Bovis's numbers ever look wrong on a real flight, this is the first thing to run — it tells you whether the pipeline broke or the flight was actually weird.
--design-cdafrom the drogue sizing (criterion 1 is skipped without it)- bias calibration recorded and its temperature noted
--mass,--riser,--diametermeasured on the recovered vehicle
Rotational energy is always reported as specific energy (J/(kg·m²)), never
absolute joules — a single scalar moment of inertia can't correctly
represent a non-axisymmetric fuselage, and the swing axis PCA finds is only
known after the flight, not a fixed body axis. See oscillation.rotational_energy.


