01: System Requirements Specification¶
Document ID: PM-SRS-001
Status: Baseline for review
Applies to: navkit 21-state error-state Kalman filter and navkit.fdir
Date: 2026-09
0. How to read the verdicts in this document¶
Every requirement below carries a verification verdict, and the verdicts are not decoration. Of the three operational user needs in §1, exactly one passes in full: one passes only on its primary leg and fails its calibration leg, and one is unverified and partly contradicted by the design as it stands. A requirements document whose traceability column read "met" across the board would be describing a different system from this one.
This document is one of three in docs/product_management/:
| Document | ID | Contents |
|---|---|---|
01_system_requirements_spec.md |
PM-SRS-001 | this document: OUNs, requirements, verdicts |
02_swapc_tradeoff_matrix.md |
PM-SWAPC-002 | platform cost study and the ESKF/pose-graph decision rule |
03_fdir_and_spoofing_strategy.md |
PM-FDIR-003 | threat taxonomy, two-stage architecture, operator telemetry |
| Verdict | Meaning |
|---|---|
| MET | Verified against a committed, reproducible check. |
| PARTIAL | The primary leg passes and a stated secondary leg does not. |
| NOT MET | Verified to fail. |
| NOT VERIFIED | No check exists. Absence of evidence, recorded as absence. |
| CONTRADICTED | The requirement as written is incompatible with the current design. |
Provenance is marked on every number, because the distinction decides what a reader may do with it:
- [M] Measured: produced by
scripts/run_benchmark.pyfrom a known-answer synthetic fixture and written toresults/benchmark.json(generated and gitignored, not committed). - [D] Derived: computed from measured or configured values in this repository, with the arithmetic shown.
- [C] Configured: a default in the source, i.e. a choice, not a result.
- [P] Proposed: a target for a future revision. Not a claim about today.
- [E] Estimate: external planning judgement, not from this repository.
The benchmark artifact carries its own disclaimer, reproduced here because it governs every [M] number in this document:
"All numbers are from a known-answer synthetic fixture. They are not field measurements and must not be presented as real-sensor performance."
No requirement in §1 is verified against flight hardware, a real IMU, or a real optical front end. Ground truth is the analytic trajectory the synthetic generator is built from, which is exact but is not the world.
1. Operational User Needs¶
OUN-01: GNSS Denial Tolerance¶
Maintain continuous tactical position estimation during a 15 s total GNSS outage with maximum ATE < 3.0 m.
Rationale. A contested receiver does not fail cleanly. It goes quiet, and the filter must carry position through the gap and re-converge on return without discarding the healthy fixes that arrive at the end of it.
Verification verdict: PARTIAL.
| Leg | Criterion | Result | Verdict |
|---|---|---|---|
| Accuracy through the gap | ATE RMSE < 3.0 m over outage_visual |
2.322 m [M] | MET |
| Accuracy, degraded front end | ATE RMSE < 3.0 m over outage_visual_degraded_camera |
2.008 m [M] | MET |
| Calibration through the gap | 2σ coverage ≥ 95% | 20.5% / 17.8% [M] | NOT MET |
| Calibration, bulk | mean NEES within tolerance of expected 3 | 286.2 / 203.7 [M] | NOT MET |
The accuracy leg passes on both cases. The calibration leg fails by a factor of
roughly five, and this is the single most important thing in this document. The
filter reports 0.162 m of 1σ uncertainty while being 2.322 m wrong. It
converges, it runs, and it is confidently wrong. This is CONSTRAINTS.md
blocker B1, still open, and it is not fixed by ADR-0005 or ADR-0006.
A range of ±3.0 m that the system can hit only while understating its own error by 16× is not a satisfied requirement. It is a half-satisfied one, and the half that is missing is the half a tactical user depends on.
OUN-02: Spoofing Resilience¶
Detect and reject step-bias and subtle drift spoofing attacks (1–100 m) without corrupting state estimates.
Verification verdict: MET, with a documented and non-trivial cost.
Verified by an injected step-bias probe applied to a 30-fix block on a live 5 Hz channel, sweeping the offset across the full required range:
| Offset | Inflation grants | Peak position error | Channel state |
|---|---|---|---|
| 1 m | 0 | 3.430 m | escalated to SENSOR_FAULT |
| 2 m | 0 | 5.441 m | escalated to SENSOR_FAULT |
| 3 m | 0 | 1.998 m | rejected, no fault |
| 5 m | 0 | 2.200 m | rejected, no fault |
| 10 m | 0 | 2.200 m | rejected, no fault |
| 20 m | 0 | 2.200 m | rejected, no fault |
| 40 m | 0 | 2.200 m | rejected, no fault |
| 100 m | 0 | 2.200 m | rejected, no fault |
The property that matters is the flat zero. An attacker cannot buy trust with
magnitude: the re-acquisition budget is a function of silence, and a
continuously streaming channel has none. Pinned by
TestInflationLimits::test_a_large_sustained_offset_is_not_followed and
test_a_spoof_small_enough_to_drift_toward_is_still_isolated; the second
exists because the first version of this code was inverted over 1–3 m (see
§4, TR-26).
The cost, stated plainly. At 1 m and 2 m the system refuses the sensor and declares a fault, then coasts on the inertial solution, reaching 3.430 m and 5.441 m of error. That is a large transient. It is the correct trade (refusing a plausible-looking receiver beats following it), but a user who expected a 1 m spoof to cost 1 m of error is wrong, and the operator interface in PM-FDIR-003 must say so.
Two further honest limits on this verdict:
- The probe is step-bias on position only. Gradual false-lock pulling (TR-13) is architecturally identified in PM-FDIR-003 §1.3 and is not verified here.
- The probe runs on a streamed channel. A spoof that first suppresses the genuine signal for enough seconds to earn a large drift budget, and only then injects, is a different attack and is not covered by this table.
OUN-03: Embedded SWaP-C Execution¶
Execute deterministically on embedded ARM Cortex-A53 / micro-controllers without third-party C++/compiled libraries or neural network dependencies.
Verification verdict: NOT VERIFIED, and partly CONTRADICTED.
| Leg | TR | Criterion | Evidence | Verdict |
|---|---|---|---|---|
| No neural networks | TR-11 | none in dependency graph | pyproject.toml [C] |
MET |
| No SciPy | TR-11 | chi-square and quantiles hand-rolled | fdir/gating.py, eval/statistics.py [M] |
MET |
| No third-party compiled libraries | TR-11 | n/a | numpy is a compiled C extension and a hard runtime dependency [C] |
CONTRADICTED |
| Flight image excludes eval tooling | TR-12 | n/a | matplotlib and pyyaml are unconditional runtime deps [C] |
NOT MET |
| Runs on Cortex-A53 | TR-32 | n/a | no ARM build exists | NOT VERIFIED |
| Deterministic execution | TR-32 | n/a | no timing, WCET or jitter measurement exists | NOT VERIFIED |
| Fits the stated power envelope | TR-32 | n/a | no power measurement exists | NOT VERIFIED |
The design intent is genuinely met in the ways that mattered to the authors: the chi-square thresholds are tabulated specifically so the filter's inner loop never runs a bisection with an unbounded iteration count, and there is no learned component anywhere. That work is worth something and is retained.
But the requirement as written says no third-party compiled libraries, and
numpy is one. It is also unavoidable in practice: the filter is dense linear
algebra over 21 states, and reimplementing that in pure Python to satisfy a
prose requirement would make the product worse. The requirement needs to be
rewritten, not the code. Proposed revision in TR-11.
Recommendation: do not present OUN-03 as a capability. Present it as "flight software is dependency-light and has no learned components; target-port work is unstarted." The single most load-bearing unmeasured claim in this document is that any of this is deterministic on the target.
2. Technical Requirements Traceability Matrix¶
Requirement IDs are stable. TR-nn rows carry their own verification verdict,
and the OUN roll-up in §2.1 does not average them away.
2.1 Roll-up¶
The counts below are computed from the verdict column and checked by
scripts/check_srs_trace.py (MET counts as met, NOT MET as not met, NOT VERIFIED and
CONTRADICTED as not verified or contradicted). That script also checks that every requirement
and criterion has a test, a checked configuration value or a stated gap in
docs/product_management/srs_trace.csv. It is structural: it does not re-measure a number or
read prose.
TR-nn IDs are grouped by discipline (IMU, optical, dependency, gating,
compute), not by operational user need. An OUN is therefore supported by a
non-contiguous set of requirements, and the mapping is given explicitly
rather than as an ID range.
| OUN | Supporting requirements | Met | Not met | Not verified / contradicted |
|---|---|---|---|---|
| OUN-01 denial tolerance | TR-20…TR-25, TR-28, TR-29, TR-31 (support); TR-30 | 9 | 0 | 1 (TR-30) |
| OUN-02 spoofing resilience | TR-14, TR-23, TR-26, TR-27; TR-13 | 4 | 0 | 1 (TR-13) |
| OUN-03 embedded SWaP-C | TR-11 (contradicted), TR-12 | 0 | 1 | 1 (TR-11) |
Four of the thirty-two requirements are not verified, three are not met and one is
contradicted, and the distribution matters more than the count. None of the OUN-01 gaps are
detection gaps: the mechanism that carries OUN-01 (TR-23) is verified and
working. The OUN-01 failure is calibration, which is carried by the OUN
statement itself and measured by AC-03/AC-04 rather than by any TR-nn row.
That asymmetry is the finding: this subsystem is better at refusing bad data
than at describing its own error, and AC-03/AC-04 are the criteria that say so.
A reader should not infer from this table that OUN-01 is nearly satisfied.
It is not. Its accuracy leg passes, its calibration leg fails by a factor of
roughly five, and the roll-up row above cannot show that because the failure is
not in a TR-nn row. The per-OUN verdicts in §1 govern.
2.2 IMU requirements¶
The noise model in the validated benchmark configuration is
ImuNoiseModel(2e-4, 2e-3, 2e-6, 1e-4, 1e-5, 2e-3) in the units
src/navkit/io/imu.py documents: rad/s/√Hz, m/s²/√Hz for the noise densities,
the same units for the bias random walks, and rad/s, m/s² for the bias sigmas
[C]. Converting to the industry units a requirement is usually written in
[D]:
| ID | Parameter | Budget | Validated config | Ratio | Verdict |
|---|---|---|---|---|---|
| TR-01 | Angular random walk | ≤ 0.05 °/√hr | 0.688 °/√hr | 13.8× over | NOT MET |
| TR-02 | Velocity random walk | ≤ 0.1 m/s/√hr | 0.120 m/s/√hr | 1.2× over | NOT MET |
| TR-03 | Gyro bias random walk | ≤ 0.01 °/√hr [P] | 0.0069 °/√hr | 0.69× | MET |
| TR-04 | Accel bias random walk | ≤ 0.01 m/s/√hr [P] | 0.0060 m/s/√hr | 0.60× | MET |
| TR-05 | Gyro bias 1σ | ≤ 0.001 °/s [P] | 0.00057 °/s | 0.57× | MET |
Conversion, shown so it can be checked or disputed [D]:
ARW [°/√hr] = gyro_noise_density [rad/s/√Hz] × √3600 × (180/π)
= 2.0e-4 × 60 × 57.29578 = 0.6875
VRW [m/s/√hr] = accel_noise_density [m/s²/√Hz] × √3600
= 2.0e-3 × 60 = 0.1200
This is the most consequential finding in the document, and it is not a failure of the filter. The published results are achieved on a consumer-grade MEMS IMU that is 13.8× worse on angular random walk than the budget in the requirement. Read one way that is good news about the algorithm: it works with hardware 14× worse than specified. Read the other way (and this is the reading that governs a requirements document): the requirement-to-hardware mapping has never been exercised. No result in this repository was produced on hardware that meets TR-01, so the accredited performance of a compliant system is unknown. A tactical MEMS IMU meeting 0.05 °/√hr will drift far less over 15 s, which should make OUN-01 easier, but "should" is not a verification and this document does not record it as one.
TR-03…TR-05 budgets are [P]: this repository proposes them, it does not inherit them from a customer requirement, and they should be treated as placeholders pending a real sensor datasheet.
2.3 Optical front-end requirements¶
The validated operating point comes from configs/benchmark.yaml [C]. There is
no optical hardware in this project, so only the noise and rate figures exist as
requirements; everything about the physical sensor is [P].
| ID | Parameter | Budget | Validated config | Verdict |
|---|---|---|---|---|
| TR-06 | Frame rate | ≥ 10 Hz [P] | 20.0 Hz [C] | MET |
| TR-07 | Rotation noise, per frame | ≤ 0.35 ° 1σ [P] | 0.35 ° 1σ [C] | MET (at budget) |
| TR-08 | Translation noise, per frame | ≤ 0.05 m 1σ [P] | 0.05 m 1σ [C] | MET (at budget) |
| TR-09 | Inter-frame drop rate | tolerates 30% burst loss [P] | 30%, 1 s burst [C] | MET |
| TR-10 | Resolution, optical distortion, hardware sync | n/a | no hardware exists | NOT VERIFIED |
TR-07 and TR-08 sit exactly at budget, which means the configuration has no
margin at all. Any front end worse than the synthetic one degrades OUN-01
directly, and outage_visual_degraded_camera is a frame-rate test, not an
accuracy-degradation test. TR-10 is where a real integration programme starts:
resolution sets the usable translation-noise floor, distortion sets a systematic
bias that no amount of filter tuning removes, and inter-camera sync error is a
bias that lands directly in the rotation block.
2.5 Dependency and threat-coverage requirements¶
| ID | Parameter | Requirement | Evidence | Verdict |
|---|---|---|---|---|
| TR-11 | Runtime dependency policy | no learned components; no SciPy; third-party compiled libraries permitted only under a declared policy | pyproject.toml [C], fdir/gating.py, eval/statistics.py [M] |
CONTRADICTED as worded |
| TR-12 | Flight image excludes eval and plotting tooling | eval-only dependencies not required at runtime | matplotlib and pyyaml are unconditional runtime deps [C] |
NOT MET |
| TR-13 | Gradual false-lock pulling detected | offset introduced below the gate threshold, undetected by a per-update gate | architecturally identified only; fdir_<ch>_mean_nis is logged but nothing acts on it |
NOT VERIFIED |
TR-11 decomposes into three sub-legs, and the aggregate verdict is the worst of them:
| Sub-leg | Evidence | Verdict |
|---|---|---|
| No neural networks in the dependency graph | pyproject.toml [C] |
MET |
| No SciPy | chi-square and quantiles hand-rolled in fdir/gating.py, eval/statistics.py [M] |
MET |
| No third-party compiled libraries | numpy is a compiled C extension and a hard runtime dependency [C] |
CONTRADICTED |
TR-11 needs rewriting, not re-implementing. The filter is dense linear algebra over 21 states; reimplementing that in pure Python to satisfy a prose requirement would make the product worse and slower on the target. The requirement is the defect, not the code. Proposed revision: no third-party C++ libraries, no neural-network dependencies, and no dependency outside a declared allowlist. Under that wording all three sub-legs pass and the clause becomes checkable by a dependency audit rather than by argument.
TR-13 is the most consequential unverified row in this table and the only one that is architecturally identified as a gap rather than missing. A gradual pull that keeps every innovation under the α = 0.001 threshold never trips Stage 1, never accumulates the trailing run Stage 2 requires, and so is invisible to the current architecture. That it is the hardest threat in the taxonomy and the one the design cannot see is the most important qualification on OUN-02's MET verdict, which is scored on step bias alone. See PM-FDIR-003 §1.3.
2.6 Gating and re-acquisition requirements¶
All values are [C] (defaults in FdirConfig) and are choices, not results.
Their justification is ADR-0005 and ADR-0006.
| ID | Parameter | Value | Basis | Verdict |
|---|---|---|---|---|
| TR-14 | False-alarm rate α | 0.001 | 0/1212 healthy updates rejected [M] | MET |
| TR-15 | Gate threshold, dof 1 | 10.828 | χ² table, exact-verified to 5e-4 [M] | MET |
| TR-16 | Gate threshold, dof 2 | 13.816 | as above | MET |
| TR-17 | Gate threshold, dof 3 | 16.266 | as above | MET |
| TR-18 | Gate threshold, dof 6 | 22.458 | as above | MET |
| TR-19 | dof coverage | exact ≤ 8, Wilson-Hilferty above | relative error −0.33% at m=4 to −0.005% at m=60 [M] | MET |
| TR-20 | Persistence threshold | 3 consecutive | ADR-0006: 2 separable from multipath bursts, 4 too late | MET |
| TR-21 | Re-acq window | 5 samples | reporting and mean_nis; the decision reads a trailing run only |
MET |
| TR-22 | Inflation factor cap | 100× | bounds a single grant to 30 m 1σ | MET |
| TR-23 | Drift bound, σ per second of silence | 0.5 m/s | the bound that does the real work | MET |
| TR-24 | Re-gate headroom | ≤ 25% of threshold | marginal passes are large-gain steps | MET |
| TR-25 | Grants per episode | 1 | a filter that can re-inflate forever follows anything | MET |
| TR-26 | Eligible sample class | genuine outlier only | the fault-hold ambiguity, §4 and PM-FDIR-003 §2.4 | MET |
| TR-27 | Inflation scope | GNSS position block only | dimensional and evidential | MET |
The velocity × silence grant cap (TR-23) deserves restating because it is the requirement that carries OUN-02, and because it is a bound rather than a tuning value:
At 5 Hz a healthy channel has T = 0.2 s and a 0.1 m granted σ. After the 15 s denial that validated OUN-01, T = 15 s and a 7.5 m granted σ, enough to admit the 3.4 m displacement the case actually produced, and nowhere near a 60 m offset. A constant budget cannot draw that line, because the correct limit depends on how long the filter was blind. That dependence is the requirement.
The granted σ is not the admitted offset, and the distinction is
load-bearing. TR-23 bounds the increment the filter adds to the position
block. The filter then re-gates the same update against the inflated
covariance and accepts anything up to reaccept_margin × χ²₃(0.001) =
0.25 × 16.266 = 4.0665 in the inflated metric, which is √4.0665 ≈ 2.02
granted σ of reach beyond the drift bound. In the S → 0 limit the admitted
offset is therefore 7.5 × (1 + 2.02) ≈ 22.6 m, not 7.5 m.
Measured directly [M]: constant-x offset applied to every fix returning after
the validated 15 s denial, outage_visual geometry:
| Offset | Verdict | Offset | Verdict |
|---|---|---|---|
| 7.5 m | admitted | 20.0 m | admitted |
| 12.0 m | admitted | 21.0 m | admitted |
| 16.0 m | admitted | 22.0 m | refused, fault declared |
| 18.0 m | admitted | 25.0 m | refused |
The measured envelope (~21 m) is 2.8× the granted σ (7.5 m). The code is
correct and self-aware: nis_monitor.py says the drift bound "is the
difference between bounding the increment and bounding the total", so this is a
specification error, not an implementation error: TR-23 as written is true about
the increment and misleading as a security envelope. AC-05 and AC-06 are scored
against offsets of 1–100 m, which straddle this boundary rather than probing it,
so their PASS verdicts survive; but the OUN-02 "0 grants" claim should be read as
"0 grants at the offsets tested", not as a bound on admitted error.
2.7 Compute and platform requirements¶
| ID | Parameter | Budget | Evidence | Verdict |
|---|---|---|---|---|
| TR-28 | State dimension | 21 | _N_STATES = 21 [C] |
MET |
| TR-29 | Filter update rate | n/a | 200 Hz over 30 s = 6001 IMU epochs [M] | MET |
| TR-30 | Per-run compute | n/a | 0.897 s estimator runtime / 2.811 s wall, heaviest case, x86 CPython [M] | NOT VERIFIED |
| TR-31 | Rejected-update cost | n/a | returns before gain formation; P⁺ = P⁻ [M] |
MET |
| TR-32 | Target-port determinism | n/a | n/a | NOT VERIFIED |
TR-30 is the number most likely to be quoted out of context, so: 0.897 s of estimator runtime (2.811 s wall, the difference being fixture setup and metrics) for a 30 s scenario is not a Cortex-A53 budget. It tells you the algorithm is not pathologically expensive (300 µs of x86 per epoch of dense 21×21 algebra) and nothing else. An A53 figure requires a native build, and TR-32 cannot be written at all until one exists. The compute/FR requirements are costed per profile in PM-SWAPC-002 §1, which reaches the opposite conclusion from the obvious one: at these sizes the filter is under 2% of the compute budget on all three platform tiers, so it is not the reason to choose a tier.
3. Verification and Acceptance Criteria¶
3.1 Criterion set¶
| ID | Criterion | Method | Threshold |
|---|---|---|---|
| AC-01 | Position accuracy through denial | outage_visual ATE RMSE |
< 3.0 m |
| AC-02 | Accuracy, degraded front end | outage_visual_degraded_camera ATE RMSE |
< 3.0 m |
| AC-03 | Bulk calibration | mean NEES / expected, outage_visual |
ratio ≤ 2.0 |
| AC-04 | Coverage calibration | 2σ ellipsoid coverage, all vision-aided cases | ≥ 95% |
| AC-05 | Spoof rejection, large | grants across 3–100 m step bias delivered continuously | 0 grants |
| AC-06 | Spoof rejection, small | grants across 1–2 m step bias delivered continuously | 0 grants |
| AC-07 | Rejection leaves state untouched | P⁺ == P⁻ on rejection |
exact |
| AC-08 | False alarms on healthy data | rejected / total, 1212 healthy fixes | 0 |
| AC-09 | Documentation integrity | scripts/check_doc_tables.py |
exit 0 |
| AC-10 | Regression floor | pytest |
0 failures |
| AC-11 | Coverage floor | scripts/coverage_report.py |
≥ 75% (script floor) |
3.2 Current acceptance status¶
| AC | Threshold | Measured | Verdict |
|---|---|---|---|
| AC-01 | < 3.0 m | 2.322 m | PASS |
| AC-02 | < 3.0 m | 2.008 m | PASS |
| AC-03 | ratio ≤ 2.0 | 95.4 (286.2 / 3) | FAIL |
| AC-04 | ≥ 95% | 20.5%, 17.8%, 0.7%, 16.3% | FAIL |
| AC-05 | 0 grants | 0 grants at 3/5/10/20/40/100 m; 21 m admitted after a 15 s denial | PASS at the tested offsets |
| AC-06 | 0 grants | 0 grants at 1/2 m | PASS |
| AC-07 | exact | holds | PASS |
| AC-08 | 0 | 0 of 1212 | PASS, but see note |
| AC-09 | exit 0 | matches results/benchmark.json |
PASS |
| AC-10 | 0 failures | 0 failures; the current counts are in CONSTRAINTS.md |
PASS |
| AC-11 | ≥ 75% (script floor) | above the floor; the current value is in CONSTRAINTS.md |
PASS |
AC-03 and AC-04 are not seed artefacts. Both were re-measured after this table
was first written, under two independent sweeps: 10 noise realisations of the
benchmark scene (scripts/seed_sweep.py) and 8 scenes with the trajectory
geometry varied over a 4.65x path-length range (scripts/scene_sweep.py). All 7
case verdicts are identical in all 8 scenes, and outage_visual is overconfident
at every noise seed. The verdicts are therefore robust; only the magnitudes are
single draws. This strengthens the FAILs; it does not soften them.
AC-08 is the one PASS here whose measurement no test enforces. A related test
(test_a_clean_run_produces_zero_false_rejections) checks a clean run of more than 50 fixes, not
the 1212 the criterion quotes. The 0-of-1212 result is
recorded in the fdir/gating.py module comment, which also documents the
contrast that justifies the default: at alpha = 0.01 the same 1212 healthy
fixes produce 13 rejections (1.07%, matching the nominal rate), and at
alpha = 0.001 they produce zero while a genuinely broken configuration still
rejects 68 of 101 and trips a fault. The measurement is sound and the reasoning
is sound, but it is a comment, and a comment does not fail a build.
By this document's own argument in §3.3, a number in a document is a claim
about a measurement, and the check keeps the claim attached to the
measurement, AC-08 is the one acceptance criterion that should be promoted to
an enforced test. It is the criterion that protects the false-alarm rate the
entire FDIR subsystem depends on, and a future refactor of gating.py could
raise the nuisance rate without turning anything red.
9 pass, 2 fail. The two acceptance criteria that define "safe to fly",
AC-03 and AC-04, both fail, and they fail for one root cause: the visual
anchor error is folded into filter state but is not a state the filter models
completely. A single anchor cannot represent correlated visual drift. The fix is
a pose graph over visual keyframes, which is CONSTRAINTS.md B1 and the top
item in ROADMAP.md. It is not an FDIR problem and no threshold tuning will
move AC-04.
3.3 The documentation checker as an acceptance gate¶
AC-09 deserves a note, because it is the cheapest gate in the set and the one most often skipped.
tests/test_doc_tables.py::test_the_real_documents_match_the_committed_benchmark
re-parses results/benchmark.json and compares every published figure in
README.md and docs/results.md against the generated value, failing on any
disagreement. It is not a formatting check. It exists because during ADR-0006
the benchmark was regenerated repeatedly with --only flags, which silently
reduced the artifact to 2 of 7 cases, and the first symptom was a documentation
test failing rather than a benchmark test.
A number in a document is a claim about a measurement, and this is the check that keeps the claim attached to the measurement. Any product document that quotes a benchmark figure must sit behind the same gate, which is why the figures in this SRS are [M]-tagged with the case name, so a future reader can find the source row.
4. Traceability to defects and decisions¶
| Req | Implementation | Record | Residual |
|---|---|---|---|
| TR-01…TR-05 | io/imu.py, configs/benchmark.yaml |
this document, PM-SWAPC-002 §2.2 | never exercised on compliant hardware |
| TR-06…TR-10 | configs/benchmark.yaml, eval/scenarios.py |
this document, PM-SWAPC-002 §2.3 | no optical hardware; budgets [P] |
| TR-11 | pyproject.toml |
this document §2.5 | needs rewriting, not re-implementing |
| TR-12 | pyproject.toml |
ROADMAP | eval deps are unconditional |
| TR-13 | fdir/nis_monitor.py (statistic only) |
ROADMAP, PM-FDIR-003 §1.3 | no detector exists |
| TR-14…TR-19 | fdir/gating.py |
ADR-0005 | tabulated; dof > 8 is approximate |
| TR-20…TR-27 | fdir/nis_monitor.py, fdir/fdir_manager.py |
ADR-0006 | GNSS position only |
| TR-28…TR-32 | estimators/eskf.py |
this document, PM-SWAPC-002 §1 | no target-port build |
| OUN-01 accuracy | estimators/eskf.py |
ADR-0006 | AC-03/AC-04 still fail |
| OUN-02 | fdir/ |
ADR-0006, PM-FDIR-003 | small offsets cost a fault and a coast |
| OUN-03 | pyproject.toml |
n/a | unstarted |
| AC-04 | n/a | B1, ROADMAP stage 3 | pose graph, not a filter change |
The inversion, recorded because specs should carry their own postmortems¶
ADR-0006's first implementation passed AC-05 and AC-06 differently than intended: 1 m and 2 m step biases obtained a re-acquisition grant while 3 m and above did not. The mechanism was backwards across the most attacker-relevant range, and the benchmark never showed it because the benchmark has no spoof case.
The cause was a genuine ambiguity in the design, not a typo. A channel is
excluded for one of two reasons, and both return accepted = False: the
innovation was an outlier, or the numbers were fine and a declared fault is
holding the channel out anyway. The second pass could not tell them apart, so a
1 m spoof (which the filter is pulled toward until the residual falls inside
the threshold) was "relieved" by spending the drift budget on a fault it
should have respected.
The fix is GatingDecision.outlier, and the requirement it encodes is TR-26.
AC-06 exists specifically so that a large-offset-only test cannot be mistaken
for a passing spoof test. Both halves of the range are now asserted together,
and both fail against the broken code.
This is the argument for keeping AC-06 as a separate criterion from AC-05 rather than folding them into one "spoof resilience" pass: the vulnerability was in the small case, and a single aggregate criterion would have reported health.
5. Open items for the next revision¶
| # | Item | Blocks | Area |
|---|---|---|---|
| 1 | Pose graph for visual anchor error | AC-03, AC-04, B1 | Estimation |
| 2 | Native ARM build, then WCET and jitter measurement | TR-30, TR-32, OUN-03 | Platform |
| 3 | Rewrite OUN-03 to a dependency policy that numpy can satisfy |
TR-11, OUN-03 | Product |
| 4 | Split matplotlib / pyyaml into an eval extra |
TR-12, TR-30, flight image | Platform |
| 5 | Real IMU on a bench, then re-run the benchmark | TR-01, TR-02 | Validation |
| 6 | Optical front-end spec: resolution, distortion, sync | TR-10 | Vision |
| 7 | Gradual false-lock detector; step bias is not the only threat | TR-13, OUN-02 | FDIR |
| 8 | Spoof-after-suppression probe (earn silence, then inject) | OUN-02 | FDIR |
| 9 | Derive max_drift_sigma_mps per IMU grade from fitted bias instability |
TR-23, OUN-02 | FDIR |
| 10 | Add a 60 s benchmark case; 15 s is a data limit, not a physical one | TR-29, OUN-01 | Estimation |
| 11 | Promote the 0-of-1212 false-alarm result to an enforced test | AC-08, TR-14 | FDIR |
| 12 | Probe AC-05/AC-06 inside the 7.5–22 m admitted envelope; the current offsets straddle the boundary rather than testing it | TR-23, OUN-02 | FDIR |
| 13 | Bound admitted offset, not just granted increment: either cap the re-gate reach in metres or add cross-modal validation | TR-23, OUN-02 | FDIR |
Items 2, 5 and 6 are the ones that decide whether this is a research artifact or a flight-candidate baseline. Items 7–10 are cheap, are unblocked by any platform decision, and each closes a specific named gap rather than a general one. Until they are done, the honest summary of this specification is: the contested-navigation problem is well posed and partly solved, and the remaining gap is honest bookkeeping rather than a missing idea.