diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26e0e06..a5ef7b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,11 +54,18 @@ jobs: - name: Fill README.md marker blocks (tools/readme/fill_readme_numbers.py) run: python3 tools/readme/fill_readme_numbers.py . - - name: Reproducibility gate (committed generated+evidence+README == regenerated, SHA-stable) + # WP15 (R16): fills every DOCS-*-START/END marker block across + # docs/*.md from the committed CSVs they cite -- the docs/ sibling of + # the README step above, same "separate step before the reproducibility + # gate" convention (docs/ is not under generated/ or evidence/ either). + - name: Fill docs/ marker blocks (tools/docs/fill_docs_numbers.py) + run: python3 tools/docs/fill_docs_numbers.py . + + - name: Reproducibility gate (committed generated+evidence+README+docs == regenerated, SHA-stable) run: | - git diff --exit-code -- generated/ evidence/ README.md \ - || (echo "::error::generated/, evidence/ or README.md drifted from committed bytes (R6)"; \ - git --no-pager diff --stat -- generated/ evidence/ README.md; exit 1) + git diff --exit-code -- generated/ evidence/ README.md docs/ \ + || (echo "::error::generated/, evidence/, README.md or docs/ drifted from committed bytes (R6)"; \ + git --no-pager diff --stat -- generated/ evidence/ README.md docs/; exit 1) # if: always() -- upload the regenerated bytes even when the gate fails, # so an R15 re-baselining PR (a legitimate, documented pin change) can diff --git a/CMakeLists.txt b/CMakeLists.txt index 175df12..a7d623b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,6 +196,14 @@ if(PYTHON3_EXECUTABLE) add_test(NAME readme_numbers COMMAND ${PYTHON3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/readme/test_readme_numbers.py ${CMAKE_CURRENT_SOURCE_DIR}) + # WP15 docs/ marker-block filler (R16 mechanism): regenerates every + # DOCS-*-START/END marker block across docs/*.md from the committed CSVs + # they cite (the docs/ sibling of readme_numbers above) and checks + # --check mode passes against the committed files, catches a corrupted + # number, and is deterministic/idempotent. + add_test(NAME docs_numbers COMMAND ${PYTHON3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/docs/test_docs_numbers.py + ${CMAKE_CURRENT_SOURCE_DIR}) else() - message(WARNING "python3 not found; WP7a viz, WP8 compliance, WP7 evidence, WP10c forensics and WP14 prioritization tests not registered") + message(WARNING "python3 not found; WP7a viz, WP8 compliance, WP7 evidence, WP10c forensics, WP14 prioritization and WP15 docs_numbers tests not registered") endif() diff --git a/README.md b/README.md index acd03a1..bd3d772 100644 --- a/README.md +++ b/README.md @@ -1,667 +1,138 @@ # ADSC — Active Debris Self-Cleanup -> **The product:** [**evidence/adsc_evidence_pack.md**](evidence/adsc_evidence_pack.md) -> — the generated English evidence pack (executive summary, installer argument, -> safety case, honest negatives, campaign/cost statistics, limitations, -> regulatory precheck, reproduction instructions). Every number in it is -> machine-read from committed artifacts; `bash tools/regenerate_all.sh build` -> regenerates all of it byte-for-byte. +> ADSC is not a debris collector. It is a low-cost concept for +> Kessler-precursor removal: a small servicer installs a passive deorbit kit +> on high-risk massive derelicts and departs, preventing future fragment +> clouds before they form — maximizing cleanup value per unit cost with a +> passively-safe approach design. + +**What this is:** an open, reproducible **evidence package** for +installer-type active debris removal — a C++17 GNC simulator plus generated, +claim-audited artifacts. **The product is +[evidence/adsc_evidence_pack.md](evidence/adsc_evidence_pack.md)**: every +number in it (and in this README) is machine-read from committed artifacts and +regenerates byte-for-byte from a clean clone. This is **not flight software and +not a mission proposal** (TRL 4, GNC software element, simulation environment). + +**Why:** the collisional cascade's fuel is the population of massive derelict +upper stages in congested bands — fragments are the symptom. The servicer +targets the objects that would become the next fragment clouds, attaches a +passive deorbit kit (drag sail or electrodynamic tether), and departs. What an +agency already has — funded capture/tug programs (ClearSpace-1, ADRAS-J2) and +published kit physics — does not include an open, honesty-audited **trade for +the installer architecture**; that is the niche this package fills +([docs/concept.md](docs/concept.md)). + +**The placeholder-independent core:** an installer never carries per-target +deorbit Δv and never tows or detumbles a multi-tonne stage; batch amortization +drives cost-per-removal to **28.5 % of the single-target baseline** at N = 4 +kits (Δv-limited, measured — see Key results). This is a mass-ratio scaling +argument that survives every open parameter. + +## What is implemented / not implemented + +Closed-loop GNC (sliding-mode attitude control, translation EKF + attitude +MEKF, guided approach with reachability-screened aborts), campaign +Monte-Carlo (N = 500/catalog, Wilson CIs), a WP12 fidelity ladder (CW → +J2 → ++drag; estimate-driven guidance; minimum-impulse-bit actuator), kit decay +trades (sail + WP13 EDT physics band), a relative-unit cost model with WP14 +absolute-cost ranges, a regulatory precheck (not legal advice), and the +generated evidence pack. Module-by-module detail: +[docs/technical_architecture.md](docs/technical_architecture.md) · +[docs/gnc.md](docs/gnc.md). + +Deliberately **not** implemented: flight-qualified code (WP9 reserved — the +only path to TRL 5), inertia identification, plane-change optimization, +homebrew breakup models, IRI plasma, absolute point-value costs, legal +determinations. Full honest scope: [docs/limitations.md](docs/limitations.md) +and the evidence pack's PLACEHOLDER inventory (140 marks, 56 decision-critical +— one glance away, by design). + +## Key results -A C++17 GNC **numerical simulator** for an installer-type active-debris-remediation -(ADR) servicer, framed as **Kessler-precursor removal**: the collisional cascade's -fuel is the population of massive derelict upper stages in congested orbital bands — -fragments are the symptom — so the servicer targets the objects that would become -the next fragment clouds, attaches a passive deorbit kit, and departs, using a -**passively-safe approach design** with clearance-verified aborts (WP11: -keep-out violations 0 of 500 per catalog at L0, ds-v1, Wilson 95% upper bound -0.0076, re-verified at L1 (+J2) and L2 (+drag) with zero violations -[ds-v1/ds-v2]). ADSC is an **open, reproducible evidence package** for that architecture (spec: -`adsc-specification-v5.md`), not flight software and not a mission proposal. It -assumes the operator is, or is contracted/consented by, the launching state of the -target; nothing in this repo assumes or enables unconsented approach to another -state's object. Governing value: **claims must match implementation** — every -number in this README is regenerable by running committed code. - -**Status (work-package based):** WP1 (relative orbital motion + passive -safety), the F1/F2 honesty follow-ups, WP2 (tumble synchronization), WP3 -(attach event + kit decay trades, mission reflowed into -approach→sync→attach→depart phases), WP4 (estimator + sensor models — the -control loop now runs on estimates, not truth), WP5 (campaign Monte-Carlo -under dispersions, with stable machine-readable CSV outputs), WP6 (parametric -cost model + figure of merit, consuming the WP5 CSV), and WP7a (a Python 3 -stdlib-only Visualization Pack of static SVGs over the committed CSVs) are -implemented on top of the v2.0 GNC core, together with WP8 (Compliance Matrix -Generator — a research-grade **regulatory precheck**, explicitly **not legal -advice**) and WP7 (the **Evidence Pack** — the project's actual product, -generated at `evidence/adsc_evidence_pack.md`). **All work packages WP1–WP8 are -complete; WP9 (processor-in-the-loop / flight-software track) is reserved and -not started.** WP7a draws **static** figures only (no interactive dashboard); -WP8 produces a precheck/evidence matrix, **never a legal conformity -determination**; WP6 predicts **no absolute cost** (relative units only). None -of this changes the TRL (still 4, element-scoped). - -**What changed from v1.21 → v2.0:** the v1.21 README named an SR-UKF and a -sliding-mode DACS, but the source only declared unused state and printed status -lines — no estimator, no control law, no dynamics loop. v2.0 replaced the prints -with an actual closed-loop simulation and stated plainly what is and isn't -implemented; v3 continues that discipline. - -## What is actually implemented - -- **TMR fuel store** (`fuel_store`): three redundant copies guarded by a real - CRC32 (IEEE, over the full 8-byte value via `memcpy`), majority voting, and - scrubbing. Recovers single- and double-copy SEU flips and flags an - unrecoverable triple failure instead of returning a silent bad value. Unit - tested (`tests/test_fuel_store.cpp`). -- **Rigid-body attitude dynamics** (`dynamics`): quaternion kinematics + Euler's - equation with a full inertia tensor, integrated with fixed-step RK4. -- **Sliding-mode attitude controller, tracking form (WP2)** (`controller`): a - quaternion sliding surface on the *relative* motion, `s = w_e + - λ·sign(q_e0)·q_ev` with `w_e = w − C(q_e)ᵀ·w_t`, boundary layer (saturation - instead of `sign` to suppress chatter), per-axis DACS deadband and torque - clamp, and target-motion feedforward: the target's torque-free acceleration - `ẇ_t = I_t⁻¹(−w_t × I_t w_t)` plus the transport term `−w_e × C(q_e)ᵀw_t`. - Regulation (v2 detumble) is the `w_t = 0` special case and its code path is - kept verbatim so the quoted regression numbers cannot drift; the equivalence - is asserted in `tests/test_sync.cpp`. -- **Tumble synchronization (WP2)** (`run_tumble_sync`): closed-loop sync - against a torque-free tumbling target with deliberately asymmetric inertia - (so the rate vector precesses — an isotropic target would tumble at constant - rate and make tracking trivially easy). Acceptance — |w_rel| < 0.1 deg/s and - attitude error < 2° held 30 s — is asserted in `tests/test_sync.cpp`, plus a - **feedforward-honesty test** that disables the reaching term (k = 0) so SMC - robustness cannot mask a wrong feedforward: on a perfectly synchronized start - the sliding variable must stay parked near zero with nothing to rescue it. -- **Point-mass inertia update** on capture (parallel-axis), with numerical - regularization before inversion. -- **Relative orbital motion** (`relmotion`, WP1): Clohessy-Wiltshire (Hill) - linear dynamics about a circular target orbit in the LVLH frame, with **both** - an analytic state-transition matrix and a fixed-step RK4 integrator - (cross-validated against each other). Includes drift-free "safety ellipse" - construction and a passively-safe approach-corridor generator. Unit-tested, - including a ≥100-point thrust-off coast that stays outside the keep-out sphere - over ≥2 orbital periods (D5 passive safety). -- **CW safe-abort with capped-impulse honesty (F1)**: `compute_safe_abort` - returns a Clohessy-Wiltshire impulse toward a drift-free relative orbit (a - bounded safety ellipse) through the current position, capped at the thruster - budget. The result carries a `Clean`/`Capped` status plus the **verified** - minimum range of the propagated post-burn coast — when the cap binds, the - bounded-orbit guarantee is lost and the code reports the actual coast range - instead of implying safety. Unit-tested, including a capped case. -- **Clearing-abort law + reachability screen + closed-loop guidance (WP11)** - (`compute_clearing_abort` / `clearing_abort_for`, `guidance`): a 3-stage - escalation ladder that accepts an abort only when the analytic post-burn - ellipse (`bounded_coast_min_range`, closed form) clears keep-out plus a - design margin — drift-null baseline, bounded radial reshape, two-impulse - retreat hop — with the legacy `Capped` honesty preserved past the Δv budget. - The campaign keep-out screen runs this law (D13); a truth-fed L0 - guided-approach demo (far approach → hold → sync → final approach → contact - → retreat) screens abort feasibility before every committed impulse and - produces the contact speed by a glideslope-with-floor profile - (`generated/wp11_guidance_modes.md`). Unit-tested (`tests/test_guidance.cpp`) - plus the pinned forensic-14 regression (`tests/test_forensic14.cpp`): the - legacy law reproduces all 14 WP10c violations analytically, the clearing law - clears every one. -- **Fidelity ladder (WP12)** (`propagation`, `adsc_ladder`): runtime-selectable - **L0/L1/L2** propagation on **one code path** (no fork) — L0 is the original - WP1 Clohessy-Wiltshire linearization (every committed WP5 number stays - byte-identical), L1 differences a full inertial two-body+J2 RK4 propagation - of both craft into the target's instantaneous LVLH frame, L2 adds a - per-craft free-molecular drag term under an **independent** - ballistic-coefficient dispersion stream (`ds-v2`; the committed campaign's - own `ds-v1` stream is untouched). The mandatory cross-validation is tested - (`tests/test_ladder.cpp`): with J2 forced off, L1 reproduces the CW closed - form within a stated **2.0 m** linearization bound (the CW model's own - error, measured at worst 0.52 m across the forensic-14 states); with drag - forced off, L2 reproduces L1 **bit-for-bit**. Re-verifies every committed - campaign abort event and the pinned forensic-14 states at L1/L2 with - **zero** keep-out violations [L1: ds-v1; L2: ds-v2; Wilson 95% upper bound - 0.0130 over the 292 (catalog A) / 291 (catalog B) re-verified abort events], and - measures safety-ellipse margin decay over repeated orbits - (`generated/wp12_ladder.{csv,md}`). Adds **L4** - estimate-driven guidance (translation EKF in the loop, under measurement - dropout and an unestimated range-bias walk) and **L5** a delta-sigma - minimum-impulse-bit actuator model (command delay + single-axis fault), - re-demonstrating tumble sync under real actuator quantization. Unit-tested - (`tests/test_ladder.cpp`). Regenerate with `./build/adsc_ladder` (ladder) / - `./build/sim_metrics` (L4/L5 rows of `generated/reference_metrics.csv`). -- **Installer mission flow (WP3)** (`run_installer_mission`): the mission runs - as approach → sync → attach → depart. Approach verifies the passively-safe - corridor; sync gates on WP2; **attach** clamps at the gated closing speed and - hands the deorbit kit over (the servicer loses the kit mass, the target gains - kit mass + sail area, changing its A/m); depart transfers to a bounded, - keep-out-clearing relative orbit. The attach reports the **contact-energy - budget** ½·m·v² at `max_v_rel` (≈ 0.33 J for the 29.6 kg servicer at - 0.15 m/s) — the no-fragmentation-at-contact argument for a geometry-keyed - clamp with mechanical compliance. (Mass basis: dry + kit, propellant-empty; - with maximum residual propellant the worst case is ≈ 0.41 J = ½·36.8 kg· - (0.15 m/s)² — the gate speed is the conservative term, the mass term is - not.) Unit-tested (`tests/test_mission.cpp`). The v2 - `post_capture_stabilization` is retained only as the detumble regression - reference; the installer paradigm does not detumble the multi-tonne target. -- **Kit deorbit-decay trades (WP3)** (`decay`): a quasi-circular drag-decay - integrator for `da/dt = −ρ(h)·C_d·(A/m)·√(μa)` over a Vallado exponential - atmosphere (cross-checked against its constant-density closed form), plus a - first-order EDT parametric knob. Produces the sail-area × altitude × - solar-activity → decay-years trade table for the catalog presets, always as a - solar-min..max **range** (T4), with the sail area required for the 25-year - guideline. The honest result — sail-only on a ~9 t stage at ~840 km needs an - impractically large sail while the lighter/lower class can close — is the - deliverable that motivates trade T1. Unit-tested (`tests/test_decay.cpp`). - Note: 25 years is the **IADC guideline** figure; under a US FCC license the - current rule (FCC 22-74, adopted 2022) requires **5-year** post-mission - disposal for LEO space stations — the sail-only negative for the heavy class - only gets harder under the 5-year standard. Both rules are carried separately - in the WP8 compliance rulepacks; the trade tables keep the IADC 25-year - reference line and no quoted number changes. -- **Estimator + sensor models (WP4)** (`estimator`): the sync control loop now - consumes **estimates, not truth** — enforced structurally: the controller is - handed an `EstimatedState` only, and truth is read solely by the sensor - models and the error-statistics recorder. Two decoupled filters (assumptions - documented in the header): a **translation EKF** on the 6-state LVLH relative - motion (prediction = the analytic WP1 CW state-transition matrix; 10 Hz range - + line-of-sight **unit-vector** measurements — az/el is avoided because its - Jacobian is singular at boresight while `d(r/|r|)/dr = (I−r̂r̂ᵀ)/|r|` is - regular everywhere), and a **multiplicative attitude EKF** (3-component - small-angle error states, covariance 3×3/6×6; an additive quaternion EKF is - deliberately not used) with gyro-propagated own attitude + star-tracker - updates and a 2 Hz vision relative-pose channel estimating `q_rel` and the - target rate `w_t`. Covariance updates are Joseph-form + symmetrized; the - tests check P symmetry and positive definiteness over a full run, and a - **NEES/NIS consistency watchdog** (fixed seed, χ² bands) rejects the - classic fake of inflating Q/R until acceptance passes. All randomness is one - fixed-seed `mt19937` + explicit Box-Muller (bit-stable across platforms). -- **Campaign Monte-Carlo (WP5)** (`campaign`): one mission processes N - class-level catalog targets sequentially in one plane (batch amortization), - tracking Δv budget and kit inventory and ending on Δv exhaustion, kit - exhaustion, all-targets-processed, or a terminal keep-out violation. - Inter-target phasing is a parameterized Δv/time cost (PLACEHOLDER; no - plane-change optimization). Dispersions (all PLACEHOLDER, centralized in - `CampaignConfig`) cover initial relative state, tumble rate/axis, servicer - initial attitude, sensor noise/bias, actuator torque-scale (~±10%) and - misalignment, and solar activity; every per-run seed is a deterministic - `SplitMix64(master_seed, run_index)`. Outputs per catalog preset — success / - abort / keep-out-violation **rates with Wilson 95% CIs** (all three reported - even at zero), and Δv-used / kits-used / removals / sync-time **percentiles - (p05/p50/p95)**, plus the failure classification counts — are written as - stable machine-readable CSV (`generated/wp5_campaign_*.csv`, schema - `1.0`, documented in `generated/wp5_campaign_schema.md`) intended for future - WP6/WP7/WP8 consumption. Unit-tested (`tests/test_campaign.cpp`: seed - determinism, Wilson edge cases, percentiles, termination, schema stability, - the guardrail that no artifact emits legal-approval language). Regenerate with - `./build/adsc_campaign`. -- **Parametric cost model + figure of merit (WP6)** (`cost`): the spec's - `C_campaign = C_dev + C_bus(m_dry) + N·C_kit + C_launch(m_wet, band) + - C_ops(T)` in **relative cost units (CU)** — **no point-value dollar figure is - ever emitted** (R6/D10); the CU→currency anchor is a PLACEHOLDER cited range - left for WP7. Consumes the WP5 campaign (schema 1.0): the **amortization - curve** (cost/removal vs kits carried N) re-runs the WP5 engine across a kit - sweep at the fixed master seed — the baseline reproduces - `wp5_campaign_runs.csv` exactly — and falls until the Δv budget (not the kit - count) bounds removals, which is the quantitative installer/batch argument. - Cost, cost/removal and **FoM = Σ mᵢ·w(hᵢ)/C_campaign** are propagated **per - run** (p05/p50/p95, never mean-only). FoM is reported under **two** normalized - congestion weightings (spatial-density + criticality-style, both PLACEHOLDER); - they disagree on band priority — open trade **T5**. A machine-readable - **tornado** ranks each cost parameter's ± impact. Outputs - `generated/wp6_cost_summary.{md,csv}` + `wp6_cost_schema.md` (schema `1.0`). - Unit-tested (`tests/test_cost.cpp`). Regenerate with `./build/adsc_cost`. -- **Visualization Pack (WP7a)** (`tools/viz/make_viz.py`, `decay_trade`): a - **Python 3 standard-library-only** generator (spec v4.2 R9 tooling exception — - no plotting library, no JS/CDN/external fonts; SVG is written directly) that - turns the committed WP5/WP6/WP3 CSVs into nine static **SVG** figures plus a - static report page. It draws: WP5 mission-outcome proportions (zeros shown), - Δv / removals / sync-time **p05/p50/p95** ranges (never mean-only), the - keep-out-violation **rate + Wilson 95% CI + N** (a rate panel, *not* a fake - trajectory), the WP6 **amortization curve** with the Δv-limited N=4 turn, the - **tornado**, the **FoM two-weighting T5 band-flip**, and the WP3 sail-decay - band (solar min..max, 25-yr line, catalog A vs B). It reads **every number - from a committed CSV** (nothing hand-written); each caption carries the source - CSV, master seed and schema_version; **no timestamps/run-times are embedded** - (byte-reproducible — a CI gate asserts regeneration matches the committed - bytes). It does **not** draw charts from any live/streaming source, embed real - imagery, or make interactive dashboards, and the keep-out figure states it is - a *simplified research visualization, not a flight safety certificate*. The - `decay_trade` target re-emits the existing WP3 decay numbers as CSV (no - physics change). Outputs `generated/viz/*.svg` + `wp5_dashboard.html` (a static - report page); tested via `tools/viz/test_viz.py` (ctest `viz`). Regenerate - with `python3 tools/viz/make_viz.py . generated/viz`. -- **Compliance Matrix Generator / Regulatory Precheck (WP8)** - (`tools/compliance/`): a Python 3 **standard-library-only** checker (spec v4.2 - R9; no `jsonschema` — validation is hand-written and dual-maintained with the - documented `mission_profile.schema.json`) that evaluates a declared mission - profile against **versioned rulepacks** (UN treaties, US FCC/FAA/NOAA, ITU, - ESA reference, ADSC internal policy; primary sources only) and emits - `generated/compliance_findings.json` + `evidence/compliance_matrix.{md,csv}` - (schema `1.0`, deterministic, no timestamps). Every finding separates - **binding law / non-binding guideline / agency guidance / ADSC policy / - placeholder research assumption**; *unknown is safer than a false PASS* - (missing inputs are UNKNOWN or fail, never pass); missing mandatory evidence - → WARN/BLOCK; unconsented active interference with a registered object → - **BLOCK** (ADSC policy). It also cross-checks the WP5 campaign compliance - metadata columns against the profile. **This is a precheck, not legal - advice**, and the rulepacks are versioned research artifacts that **can go - stale** as regulations change. Coverage today: UN treaties + US + ESA - reference only — Russian/Chinese/Japanese national law is **not** yet - covered (future work). Tested via `tools/compliance/test_compliance.py` - (ctest `compliance`). -- **Evidence Pack (WP7 — the actual product)** (`tools/evidence/`): a Python 3 - stdlib-only generator writes `evidence/adsc_evidence_pack.md` — executive - summary, the quantitative installer argument + cascade source-term framing - (full primary citations where confident, `[CITATION NEEDED — PLACEHOLDER]` - where not, never fabricated), safety case (WP1 coast statistics, F1 - clean+capped abort coverage, WP5 keep-out rate with Wilson CI, contact-energy - budget), the honest decay negatives, campaign/cost/FoM statistics with the T5 - disagreement kept open, a plain-language limitations section, the regulatory- - precheck summary ("This is not legal advice." in the body), the flight- - software migration annex (deliberately NOT implemented; WP9 reserved), - one-command reproduction instructions, and an **auto-collected PLACEHOLDER - inventory** of every unvalidated parameter in the tree. **Zero hand-written - numbers**: everything is machine-read from the committed CSVs (a new - `sim_metrics` target re-emits the pinned WP1/F1/WP2/WP3/WP4 demo numbers as - `generated/reference_metrics.csv`; no physics change). A **claim-audit test** - (ctest `evidence`) rejects forbidden overclaims (flight-ready/-proven, - TRL 5/6 claims, "guaranteed", legal-conclusion words), verifies required - honesty sections, cross-checks every quoted headline number against its - source CSV, and enforces determinism + committed==regenerated. - `tools/regenerate_all.sh` is the single regeneration entry point (CI runs - exactly it). -- **First-order PCM thermal budget** integrated over the control loop. -- **Deorbit gating**: autonomous by default, human-in-the-loop only on the - emergency path, blocked below the fuel reserve. - -The simulation driver (`adsc_sim`) runs a real post-capture detumble: it seeds a -tumble, closes the control loop at dt = 0.01 s, and reports the settling time and -final body rate — so "stable at dt = 0.01 s" is now something you can run and -verify rather than a claim in prose. + +| campaign (N = 500/catalog) | SL-16 class | SL-8 class | +|---|---|---| +| productive-end rate | **0.556** [0.512, 0.599] | **0.542** [0.498, 0.585] | +| keep-out violations | **0.000** [0.000, 0.008] [L0, ds-v1] | **0.000** [0.000, 0.008] [L0, ds-v1] | +| Δv used p50 [m/s] | 124 | 124 | +| removals/mission p50 | 4 | 4 | -## What is explicitly NOT implemented (honest scope) +Full tables + failure taxonomy: [docs/safety.md](docs/safety.md). + -- **Estimator scope is deliberately narrow (WP4).** The target inertia is - assumed **known** for the torque-free feedforward and the MEKF model — exact - in this simulation, but a real mission needs inertia identification. Sensor - models are Gaussian abstractions (no outliers, no dropouts, no star-tracker - occlusion, no vision pose ambiguity); gyro/rangefinder biases exist as - Config knobs but are **not estimated** (the consistency tests assume the - zero defaults). The translation state is estimated, consistency-tested and — - since WP12 L4 — **used for control**: the guided approach flies on the - translation EKF's estimate under measurement dropout and an unestimated - range-bias walk (see the closed-loop guidance bullet below for the measured - numbers, including the honest NEES inconsistency that unestimated bias - produces). Attitude sync in the campaign remains the truth-driven WP2 - primitive (documented WP5 simplification). -- **Continuous-torque DACS approximation — retired at L5 (WP12).** The v2–v4 - controller assumed continuous torque, with sync-hold relying on that - approximation (the fine firing deadband, 3×10⁻⁴ on the sliding variable, - bounded the sync-hold error only under continuous torque). WP12 L5 - re-demonstrates sync under a delta-sigma minimum-impulse-bit actuator model - (MIB 2×10⁻⁴ N·m·s PLACEHOLDER, 1-step command delay, single-axis 50% fault) - [L5: MIB delta-sigma, 1-step delay, single-axis fault]: sync at **16.28 s** - vs **16.87 s** continuous (`wp12_l5_sync_time_s` = 16.280000, - `wp2_sync_time_s` = 16.870000, `generated/reference_metrics.csv`). Honest - remainder: the MIB magnitude is PLACEHOLDER and ideal duty-cycling (no - thruster hardware model, no minimum on-time) is still assumed. The - 16.28 s < 16.87 s ordering is a metric artifact — the first-crossing-then- - dwell criterion lets a bang-bang limit cycle graze the tolerance box - slightly earlier under quantization — not a physical speed-up from - degradation; the claim is only that sync still completes under MIB + - delay + fault. -- **Thermal model is a single lumped PCM bucket** — no eclipse/sunlight - radiative balance, no per-node conduction. -- **Closed-loop rendezvous guidance: L0 truth-fed (WP11) plus L4 - estimate-driven (WP12).** A deterministic, truth-fed L0 guided-approach mode - machine (far_approach → hold → sync_hold → final_approach → contact → - retreat, with a reachability screen evaluated before every committed - impulse — screen held at every step in the demo: yes) still flies the - approach to contact truth-fed (`generated/wp11_guidance_modes.md`). The - contact speed is PRODUCED by a glideslope-with-floor profile (0.10 m/s - design) rather than merely gated, closing the long-standing "gate, not - guidance" limit at L0; the v_rel ≤ 0.15 m/s gate is retained as an - independent check, not removed. **WP12 L4 makes this guidance - ESTIMATE-DRIVEN**: the translation EKF is in the loop (truth is - error-recording only) under measurement dropout and an unestimated - range-bias random walk; the truth-evaluated contact speed is **0.0998 m/s** - (`wp12_est_guided_contact_speed_m_s` = 0.099759, gate 0.15 m/s, - `generated/reference_metrics.csv`) [L4: L0 dynamics + dropout + bias walk, - deterministic seed]. Honest addition: under the unestimated bias walk the - translation-EKF NEES runs to **~310** (`wp12_est_nees_trans` = 310.437825) - against an ideal **~6** — the filter is optimistic about (i.e. - underestimates) its own error; adding bias states is the identified fix, - not implemented here (evidence pack §3 fidelity ladder, §6 limitations). - As of WP4 the attitude-sync loop consumes estimates from noisy sensors; - attitude sync in the campaign remains the truth-driven WP2 primitive - (documented WP5 simplification), and the truth-driven variants remain in - the test suite as the control-law regression references. -- **Decay model is first-order and single-object** (WP3): quasi-circular drag - decay over an exponential atmosphere with a single altitude-independent - solar-cycle factor (a coarse proxy — the real swing is altitude-dependent); - no J2, no attitude-dependent ballistic area, no re-entry demisability model - (that safety/cost conflict is open trade T2). The EDT branch is a parametric - placeholder, not a tether performance model. -- **Passive-safety claims are model-scoped (F2).** The keep-out and - safety-ellipse guarantees are exact only in the linear Clohessy–Wiltshire - model: circular target orbit, no J2, no differential drag, small separations. - J2 and differential drag erode drift-free safety ellipses over time, and the - CW linearization error grows with separation — so every passive-safety number - in this README holds in the model, not in the real environment. A real - mission would re-verify all coasts against a higher-fidelity propagator. -- **Campaign scope is deliberately narrow (WP5).** The campaign reuses the - truth-driven `run_tumble_sync` primitive for tractable N≥500 Monte-Carlo, so - the dispersions that flow through it are the initial-condition and vehicle - ones (tumble rate/axis, servicer initial attitude, actuator torque-scale and - misalignment); **sensor noise/bias dispersions are drawn and their realized - factors recorded but not re-propagated per run** — the closed-loop sensor - effect is characterized by the WP4 estimate-driven acceptance instead. The Δv, - time and phasing cost coefficients and all dispersion magnitudes are - PLACEHOLDER. WP5 produces **no visualization/charts** and performs **no legal - or regulatory determination**: the future-facing compliance columns are - passive research-profile metadata (`owner_consent_assumed` is a research - scenario assumption per D9, never a legal fact), reserved for WP7/WP8. -- **Cost model is relative and parametric (WP6).** Every cost/FoM coefficient - (`CostConfig`) and both congestion-weight tables are **PLACEHOLDER**; outputs - are in relative CU. **WP6 predicts no absolute program cost** — a cited - CU→currency range is deliberately left unfilled (a WP7 deliverable), and no - point-value dollar figure is emitted anywhere. Per-catalog FoM is dominated by - removed mass (the ~9 t class outranks the ~1.4 t class under both weightings); - the metric-choice disagreement that WP6 surfaces is the **band-priority flip** - between the two weightings (open trade T5). WP6 emits no charts. -- **The compliance precheck is not legal advice (WP8).** It checks a declared - profile against research-grade rulepacks and reports evidence gaps — it does - not and cannot determine legal conformity, and its outputs never say - otherwise. Rulepacks are **versioned snapshots that can go stale**; every - rule carries `source_date_or_version` and a `limitations` field, low-confidence - rules are downgraded to `placeholder`, and jurisdiction coverage is honestly - partial (UN treaties + US + ESA reference; **no** Russian/Chinese/Japanese - national law yet, despite those being adopter targets — future work). -- **Small-debris (1–10 cm) removal is out of scope (T6)** — a documented - exclusion, not neglect: the `flux_sweep` target regenerates the physics (≈50 - MJ/kg / ≈12× TNT at 10 km/s; a 1 cm Al fragment ≈ 71 kJ ≈ 17 g TNT; km²-scale - collector area — ≈32 km² average, ≈3.8 km² peak — for 1 %/yr removal) into - `generated/t6_flux_sweep.md`. -- Numbers for mass, inertia, power, PCM capacity and the target orbit/keep-out - are plausible placeholders, not derived from a specific bus or target design. + +| cost/FoM (relative CU; WP14 MUSD ranges) | value | +|---|---| +| amortization minimum (N = 4) | **44.80 CU/removal = 0.285× the N = 1 baseline** | +| FoM p50, SL-16 (spatial / criticality) | 156.70 / 200.90 kg/CU | +| derived anchor (low / mid / high) | 0.0628 / 0.1546 / 0.4213 MUSD/CU | +| cost/removal p50 at the anchor | 2.81 / 6.92 / 18.87 MUSD | + +Full model + sources-or-PLACEHOLDER itemization: +[docs/cost_model.md](docs/cost_model.md). + -## TRL assessment (honest) +Kit-class trade (WP13): sail closes for the ~1.4 t class (7..115 m²) but +**not** for the ~9 t class (135..2155 m²); EDT is an honest **candidate with +open risks** for the heavy class (2.98..9.17 yr band; libration T7 unresolved) +— [docs/target_selection.md](docs/target_selection.md). -**TRL 4 — for the GNC software element only**: with WP4 the attitude-sync GNC -element (tracking controller + estimator + sensor models) runs closed loop **on -estimated states** in a laboratory/simulation environment, with filter -consistency verified (NEES/NIS) rather than assumed. That is the element-level -TRL 4 definition; the earlier "TRL 5–6" framing (v1.21) was not supportable and -WP1–WP3 stayed at 3–4 because control consumed true states. Scope caveats, -stated plainly: **TRL 4 applies to the GNC software element, not the system** — -system-level TRL is undefined and not claimed here. Reaching TRL 5 for the -element requires real-time processor-in-the-loop execution on representative -hardware (see the WP7.7 flight-software migration annex direction in the spec), -which this repo deliberately does not attempt. WP5 adds -robustness-under-dispersions evidence, but this strengthens the TRL-4 evidence -base rather than raising the TRL. WP6 adds a relative-unit cost/FoM model on top -of that evidence — an economic-viability argument, not a GNC maturity change — -so the TRL stays 4. The same holds for Phase 0 and beyond (spec v5 §9, binding): -WP10 forensics, WP11 safety hardening + closed-loop guidance, and the WP12 -fidelity ladder (higher-fidelity propagation is a wider validated envelope, -not a maturity change) widen the validated envelope at TRL 4 — they do not -raise it; raising it requires the real-time processor-in-the-loop track -(WP9, reserved, not started). +## Safety status -## Build +Approach safety is a **passively-safe approach design with clearance-verified +aborts (keep-out violations 0/500 per catalog [L0, ds-v1, Wilson ≤ 0.0076])**, +re-verified at L1 (+J2) and L2 (+drag) with zero violations over the 292/291 +re-verified abort events [ds-v1/ds-v2] — never "strict approach safety": the +guarantees are model-scoped (R14 tags everywhere), and a real mission would +re-verify against a higher-fidelity propagator. +[docs/safety.md](docs/safety.md). -Requires a C++17 compiler and Eigen 3.3+. +## Reproduce ``` -git clone https://github.com/HeliCorgi/ADSC.git -cd ADSC -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release # add -DADSC_WERROR=ON for R3 -cmake --build build -./build/adsc_sim -./build/adsc_campaign # WP5: regenerates generated/wp5_campaign_* (full N=500) -./build/adsc_cost # WP6: regenerates generated/wp6_cost_* (consumes the WP5 summary) -./build/decay_trade # WP3: re-emits the sail-decay trade as generated/wp3_decay_trade.csv -python3 tools/viz/make_viz.py . generated/viz # WP7a: regenerates generated/viz/*.svg + dashboard -python3 tools/compliance/check_compliance.py # WP8: regenerates generated/compliance_findings.json -python3 tools/compliance/generate_matrix.py # WP8: regenerates evidence/compliance_matrix.{md,csv} -python3 tools/evidence/make_evidence.py # WP7: regenerates evidence/adsc_evidence_pack.md -bash tools/regenerate_all.sh build # ...or regenerate EVERYTHING in order (CI runs this) -ctest --test-dir build # fuel_store/relmotion/sync/decay/mission/estimator/campaign/cost/flux/viz/compliance/evidence +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build +bash tools/regenerate_all.sh build # regenerates EVERY committed artifact ``` -When invoking bare g++ directly, prefer passing Eigen via `-isystem` to avoid -GCC `-Wmaybe-uninitialized` false positives; CMake handles this automatically. +CI enforces byte-identity of `generated/`, `evidence/`, this README's number +blocks, and `docs/` on every push. Requires C++17 + Eigen 3.3; Python 3 +stdlib only for tooling. Details: [docs/technical_architecture.md](docs/technical_architecture.md). -## Layout +## Structure ``` -include/adsc/ public headers (fuel_store, dynamics, controller, thermal, - relmotion, decay, estimator, mission, campaign, cost, flux) -src/ implementations + main.cpp sim + WP5/WP6/T6/WP3 emitters (main_*.cpp) -tests/ C++ unit tests (+ Python tests under tools/, ctest `viz`/`compliance`) -tools/viz/ WP7a Python 3 stdlib-only visualization generator (make_viz.py) -tools/compliance/ WP8 regulatory precheck: rulepacks, schema, checker, matrix generator -tools/evidence/ WP7 evidence-pack generator + claim-audit test -tools/regenerate_all.sh single regeneration entry point (CI runs exactly this) -generated/ committed artifacts: WP5 campaign, WP6 cost/FoM, T6 flux, WP3 decay, - reference_metrics CSVs, viz/ SVGs, compliance_findings.json, - WP10/WP11 forensics (wp10_violation_forensics.{md,csv} — legacy-law - archive since WP11; wp11_abort_audit.{md,csv}; wp11_guidance_modes.md), - WP12 fidelity-ladder re-verification (wp12_ladder.{csv,md}) -evidence/ committed generated evidence artifacts (adsc_evidence_pack.md, - compliance matrix; same reproducibility gate as generated/) -.github/ CI (Ubuntu + Eigen: cmake build + ctest, warnings-as-errors, reproducibility gate) -adsc-specification-v5.md active spec (work packages, hard rules, locked decisions) -adsc-specification-v4.md retired v4.2 spec (superseded by v5; kept because v5 - references its D/R/T items) +evidence/ the product: generated, claim-audited evidence pack +docs/ concept, architecture, gnc, safety, targets, cost, + legal, limitations, roadmap, adoption brief, one-pager, + 5-page summary, deck source, release engineering +generated/ committed machine-readable artifacts (CSV/md/SVG) +include/ src/ tests/ C++17 simulator + unit tests +tools/ stdlib-only generators, audits, compliance precheck +adsc-specification-v5.md the binding spec (rules R1-R16, decisions D1-D13) ``` -## Roadmap (work packages — see `adsc-specification-v5.md`) - -- **WP1 — Relative orbital motion + passive safety** ✅ implemented. -- **F1/F2 — Capped-abort honesty + model-scope note** ✅ implemented. -- **WP2 — Tumble synchronization** ✅ implemented (tracking SMC with torque-free - feedforward against a precessing target). -- **WP3 — Attach event + kit decay trades** ✅ implemented (contact-energy - budget; sail vs EDT decay trade with honest negative results). -- **WP4 — Estimator + sensor models** ✅ implemented (translation EKF + - multiplicative attitude EKF; control on estimates; NEES/NIS consistency - watchdog). -- **WP5 — Campaign Monte-Carlo** ✅ implemented (dispersions; success / abort / - keep-out rates with Wilson CIs; Δv / kit / removal percentiles; stable - machine-readable CSV for WP6/WP7/WP8). -- **WP6 — Parametric cost model** ✅ implemented (relative-unit C_campaign; - amortization curve; FoM under ≥2 weightings with T5; tornado sensitivity). -- **WP7a — Visualization Pack** ✅ implemented (Python 3 stdlib-only static SVG - figures + static report page from the committed CSVs; no external deps). -- **WP8 — Compliance Matrix Generator / Regulatory Precheck** ✅ implemented - (versioned rulepacks; precheck + evidence matrix; **not legal advice**, no - legal conformity determination). -- **WP7 — Evidence Pack** ✅ implemented (`evidence/adsc_evidence_pack.md`, - generated, claim-audited, zero hand-written numbers — the actual product). -- **WP9 — Processor-in-the-loop / flight-software track** — **reserved, not - started** (the only path above element TRL 4; see the spec). -- **WP10 — Phase 0: adoption, citations, forensics** ✅ implemented (spec v5 - adopted; citation-fill; keep-out-violation forensics — see - `generated/wp10_violation_forensics.md`, now the legacy-law archive - superseded by WP11). -- **WP11 — Safety hardening + closed-loop guidance** ✅ implemented - (clearing-abort law, reachability screen, guided approach); keep-out - violations 0 of 500 per catalog at L0/ds-v1, Wilson 95% upper bound 0.0076. -- **WP12 — Fidelity ladder** ✅ implemented (L0/L1/L2 + L4/L5 elements; zero - keep-out violations re-verified at L1/L2 [ds-v1/ds-v2]; the - CW-safe-but-higher-level-unsafe case does NOT materialize for this - dispersion set — measured, see `generated/wp12_ladder.md`). -- **WP13 — Kit-class trade + EDT physics** ✅ implemented (aligned-dipole - EDT-v1 band model with mandatory η(i) = |cos i|..cos²i — derivation: - `wp13-edt-derivation.md`, citations: `wp13-literature.md`; per-class - recommended-kit table `generated/wp13_kit_trade.csv`; class-C - controlled-reentry comparison `generated/wp13_classC.md`; libration T7 - explicitly **unresolved**; SL-16 EDT is a *candidate with open risks*, - not a closed recommendation). -- **WP14–WP15 (v5)** — not started; `adsc-specification-v5.md` defines the - cost ranges + FoM upgrade and the proposal package. WP9 remains the only - path to TRL 5, unchanged by v5. - -**Roadmap end state: WP1–WP8 complete; Phase 0 (WP10), WP11 safety -hardening, WP12 fidelity ladder, and WP13 kit-class trade + EDT physics -complete.** The package is regenerable end-to-end with one command -(`bash tools/regenerate_all.sh build`) and CI enforces byte-identity of every -committed artifact on every push. - -Reproducible WP1 numbers (regenerate with `./build/adsc_sim`): for the 825 km -reference orbit, mean motion n ≈ 1.03×10⁻³ rad/s and period ≈ 6084 s; the closest -thrust-off approach across the sampled corridor is ≈ 424 m — comfortably outside -the 200 m keep-out sphere. - -Reproducible WP2 numbers (regenerate with `./build/adsc_sim`, scenario 5): -against a 2.0 deg/s precessing tumble (target inertia ratios 1.0 / 0.6 / 0.3) -from a 40° attitude offset at zero rate, sync is achieved at **16.87 s** and the -criteria then hold for the 30 s dwell; after the dwell the errors stay at -max |w_rel| = 0.0213 deg/s (tol 0.1) and max attitude error = 0.0985° (tol 2°), -ending the 120 s run at 0.00043 deg/s and 0.098°. The feedforward-honesty bound -(sliding variable with the reaching term disabled, limit 5×10⁻³) is asserted and -printed by `./build/test_sync`. - -Reproducible WP3 numbers (regenerate with `./build/adsc_sim`, scenario 6): the -installer mission reaches all phases (approach corridor closest 424.3 m → sync -at 16.87 s → attach → depart, bounded coast 400 m) for a catalog-A-mass target, -with a **contact-energy budget of 0.333 J** (29.6 kg servicer at 0.15 m/s, -dry+kit mass basis — worst case with residual propellant ≈ 0.41 J) — the -no-fragmentation-at-contact argument. The sail-only decay trade to the 180 km -handoff (range = solar max .. solar min) makes the honest structure concrete: - -| target | 100 m² sail | 1000 m² sail | sail area for 25 yr | -|---|---|---|---| -| A: SL-16, ≈9 t, 840 km | 33.7 .. 538.8 yr | 3.4 .. 53.9 yr | **135 .. 2155 m²** | -| B: SL-8, ≈1.4 t, 750 km | 1.8 .. 28.8 yr | 0.2 .. 2.9 yr | **7 .. 115 m²** | - -Sail-only closes for the lighter/lower class B (tens of m²) but **not** for the -heavy, high stage A (hundreds-to-thousands of m² — impractical): the honest -negative that brackets open trade T1 (drag sail vs electrodynamic tether). The -legacy EDT *parametric* study spans, e.g., 36.1 yr at 50 m/day down to 1.2 yr at -1500 m/day of along-track decay; since WP13 the EDT branch also has a *physics* -band (aligned-dipole, capped-current, mandatory η(i) = |cos i|..cos²i): the SL-16 -class works out at **2.98 .. 9.17 yr** with a 3 km / 2 A (PLACEHOLDER) tether — -an **EDT candidate with open risks** (libration T7 unresolved, plasma Ne pair -PLACEHOLDER), *not* a closed recommendation — see `generated/wp13_kit_trade.csv` -and `wp13-edt-derivation.md`. (The 25-year column is the IADC guideline; -under a US FCC license the current rule is **5 years** — FCC 22-74, 2022 — which -makes the class-A sail-only negative strictly harder. See the WP8 rulepacks.) +## Roadmap -Reproducible WP4 numbers (regenerate with `./build/adsc_sim`, scenario 7; fixed -seed 20260703): driven purely by estimates under sensor noise, sync is achieved -on the **truth** state at **17.07 s** with post-dwell max |w_rel| = 0.0130 deg/s -(tol 0.1) and max attitude error = 0.0848° (tol 2°). Estimation RMS over the -stats window: own attitude 0.0020°, relative attitude 0.0341°, target rate -0.00106 deg/s, relative position 0.038 m, velocity 0.0011 m/s. Filter -consistency: NIS 3.944/4 (translation, N=800), 3.063/3 (star tracker, N=400), -3.190/3 (vision, N=160); NEES 7.796/6, 2.660/3, 4.066/6 — all inside their -documented χ² bands; covariance health min eigenvalue 1.3×10⁻¹², max asymmetry -exactly 0. +WP1–WP8 ✅ · WP10 forensics ✅ · WP11 safety hardening ✅ · WP12 fidelity +ladder ✅ · WP13 kit trade + EDT ✅ · WP14 cost ranges + FoM ✅ · **WP15 +proposal package ✅ (this release)** · WP9 processor-in-the-loop — **reserved, +not started, the only path above TRL 4**. Detail: +[docs/roadmap.md](docs/roadmap.md). -Reproducible WP5 numbers (regenerate with `./build/adsc_campaign`; fixed master -seed `0x5AD5C0DECAFE2026`, full N = 500 runs per catalog preset, 6 targets / -mission, 4 kits, 140 m/s Δv budget). CI runs the same full N = 500 and uploads -the four `generated/` artifacts (the committed copies), so routine CI and the -full campaign are the same run — no reduced CI subset was needed (runtime is -well under the 5-minute guideline; `adsc_campaign 100` produces a stratified -subset if a faster smoke run is ever wanted). Rates carry a Wilson 95% CI -(z = 1.959963984540054); distributions are p05/p50/p95. +## Limitations - -The full N = 500 campaign (both catalog presets) runs in a few tens of seconds -on a CI runner (see the Actions log of the current run for the actual figure). -`success` here means a **productive end** — the mission installed its -full 4-kit complement (`kit_exhausted`) or cleared all 6 targets (`completed`) — -not one cut short by Δv exhaustion or a keep-out violation. - -| metric | SL-16 / Zenit-2 class | SL-8 / Kosmos-3M class | -|---|---|---| -| success rate | **0.556** [0.512, 0.599] | **0.542** [0.498, 0.585] | -| nonproductive-termination rate (= 1 − success) | **0.444** [0.401, 0.488] | **0.458** [0.415, 0.502] | -| gate-abort-run rate (abort-path exposure) | **0.444** [0.401, 0.488] | **0.458** [0.415, 0.502] | -| keep-out-violation rate | **0.000** [0.000, 0.008] [L0, ds-v1] | **0.000** [0.000, 0.008] [L0, ds-v1] | -| Δv used p05/p50/p95 [m/s] | 124 / 124 / 136 | 124 / 124 / 136 | -| kits used p05/p50/p95 | 3 / 4 / 4 | 3 / 4 / 4 | -| removals/mission p05/p50/p95 | 3 / 4 / 4 | 3 / 4 / 4 | -| sync arrival p05/p50/p95 [s] | 14.47 / 17.68 / 20.09 | 14.13 / 17.72 / 20.16 | -| failure counts (runs) | dv_exhausted 222, kit_exhausted 278, keep_out 0, completed 0 | dv_exhausted 229, kit_exhausted 271, keep_out 0, completed 0 | -| per-target events | gate_abort 292, sync_timeout 0 | gate_abort 291, sync_timeout 0 | - -Two abort-related rates are reported and are deliberately distinct: -**`gate-abort-run rate`** is the abort-path exposure (fraction of runs with ≥ 1 -closing-speed gate abort — what the spec calls the "abort rate"), while -**`nonproductive-termination rate`** is 1 − success. Under the current *flat -PLACEHOLDER* Δv cost the two coincide numerically — every aborting mission needs -an extra target-slot to still install its kits and so exhausts the 140 m/s -budget — but they are separate concepts and will diverge once the cost model -gains structure. The honest campaign finding: the servicer is **Δv-limited about -44% of the time** (`dv_exhausted` = 222/500) and installs its full kit -complement the rest; the attitude sync **never** times out across the sampled -tumble/attitude/actuator dispersions, and keep-out violations are **0 of 500** -per catalog [L0: linear CW, dispersion set ds-v1, Wilson 95% upper bound -0.0076] — the WP11 clearing-abort law accepts an abort only when the analytic -post-burn ellipse clears keep-out plus a design margin (mechanism forensics: -`generated/wp10_violation_forensics.md`; audit: -`generated/wp11_abort_audit.md`). `completed` (all 6 targets) is 0 by -construction — 4 kits cannot service 6 targets. The `Δv used` / `kits used` -percentiles matching across the two presets is expected (a flat PLACEHOLDER cost -takes quantized, catalog-independent values — not a copy-paste bug). Full per-run -records and the column schema are in [generated/](generated/). - - -WP5 produces **no** charts and performs **no** legal/regulatory determination; -the CSV schema (`generated/wp5_campaign_schema.md`, version `1.0`) is stabilized -for future WP6 cost/FoM, WP7 visualization, and WP8 compliance tooling. - -Reproducible WP6 numbers (regenerate with `./build/adsc_cost`; relative cost -units **CU**, **no absolute-dollar figure claimed**). WP6 consumes the WP5 -campaign (schema 1.0), re-running the engine across a kits-carried sweep at the -fixed master seed; the baseline (N = 4) reproduces `wp5_campaign_runs.csv`. - - -The full sweep (500 runs × 2 catalogs × N = 1..8 kits) runs in a few tens of -seconds on a CI runner (see the Actions log of the current run for the actual -figure), and the baseline (N = 4) removals reproduce the committed WP5 CSV -exactly (`MATCH (schema 1.0 consumed)`). - -**Amortization curve — cost/removal vs kits carried N (SL-16 class):** - -| N | cost/removal p50 [CU] | ratio to N=1 | removals p50 | -|---:|---:|---:|---:| -| 1 | 157.07 | 1.000 | 1 | -| 2 | 82.22 | 0.523 | 2 | -| 3 | 57.27 | 0.365 | 3 | -| **4** | **44.80** | **0.285** | 4 | -| 5 | 46.14 | 0.294 | 4 | -| 8 | 50.15 | 0.319 | 4 | - -Batch amortization drives cost/removal down to **28.5 % of the single-target -(N = 1) baseline** — a **3.5× per-removal saving** — bottoming at N = 4 where the -**Δv budget (not the kit count)** caps removals at 4; carrying more kits then -adds cost without removals and the curve turns back up. That shape *is* the -quantitative installer/batch argument (SL-8 class is within ~0.3 %). - -**FoM = Σ mᵢ·w(hᵢ)/C_campaign at N = 4 (p50, kg/CU):** - -| catalog | spatial-density | criticality | -|---|---:|---:| -| SL-16 (~9 t, 840 km) | 156.70 (w = 0.78) | 200.90 (w = 1.00) | -| SL-8 (~1.4 t, 750 km) | 31.35 (w = 1.00) | 21.32 (w = 0.68) | - -The ~9 t class outranks the ~1.4 t class under **both** weightings (FoM is -mass-dominated), but the **band weight flips** — spatial density values the -750 km band more, the criticality index values the 840 km band more — a genuine -metric-choice disagreement tracked as **open trade T5**. **Tornado** (SL-16, -±30 %, ranked by cost/removal swing): `c_dev_cu` (15.0 CU) ≫ `c_bus_cu` (4.5) > -`c_launch_cu_per_kg` (3.7) > `c_kit_cu` (2.4) > `c_ops_cu_per_day` (1.2). All -values are relative CU; **no absolute-dollar figure is claimed**. - +TRL 4 applies to the GNC software element only; system-level TRL is not +claimed. Passive-safety numbers hold in the stated models, not the real +environment. Decision-critical inputs remain PLACEHOLDER until cited +(inventoried automatically in the evidence pack §10). +[docs/limitations.md](docs/limitations.md) is the honest one-stop list. -## Disclaimer / 免責 +## License / Disclaimer -Conceptual prototype for peaceful ADR research and education only. Real flight -hardware requires formal design review, qualification, and compliance with -international space law (IADC guidelines, UN COPUOS). No military or re-entry-body -application is intended. +MIT ([LICENSE](LICENSE)). Conceptual prototype for peaceful ADR research and +education only; no military or re-entry-weapon application is intended. +Regulatory outputs are research prechecks, **not legal advice**. -本プロジェクトは平和的デブリ除去技術の教育・研究目的の概念プロトタイプです。実際の -宇宙機運用・打ち上げには専門機関による設計審査・認定・国際法遵守が必要です。兵器・ -高速再突入体への使用は一切想定していません。 +本プロジェクトは平和的デブリ除去技術の教育・研究目的の概念プロトタイプです。 +実際の宇宙機運用には専門機関による設計審査・認定・国際法遵守が必要です。 diff --git a/docs/concept.md b/docs/concept.md new file mode 100644 index 0000000..8597a20 --- /dev/null +++ b/docs/concept.md @@ -0,0 +1,168 @@ +# Concept + +> This is a concept dossier for a research/concept-study audience — **not a +> mission proposal**. Nothing here is legal advice, a flight-readiness claim, +> or a commitment on behalf of any agency. TRL 4, GNC software element only +> (see [roadmap.md](roadmap.md)). + +## What ADSC is + +ADSC is an open-source, reproducible **evidence package** for installer-type +active debris removal, framed as **Kessler-precursor removal**: instead of +towing large debris or chasing fragments, a small servicer installs a passive +deorbit kit on the massive derelicts that would otherwise become the next +fragment clouds, and departs. The audience is mission designers at the +agencies and contractors of major launching states. The product is +credibility; the success criterion is that a skeptical engineer can clone the +repository and regenerate every claimed number +(`bash tools/regenerate_all.sh build`, byte-identical across three independent +platforms — see [technical_architecture.md](technical_architecture.md)). + +Canonical summary blurb, used verbatim (spec §1): + +> ADSC is not a debris collector. It is a low-cost concept for +> Kessler-precursor removal: a small servicer installs a passive deorbit kit +> on high-risk massive derelicts and departs, preventing future fragment +> clouds before they form — maximizing cleanup value per unit cost with a +> passively-safe approach design. + +The owner's one-line governing value for v5: maximum orbital-environment +cleanup value per unit cost, with approach safety treated as a design +requirement rather than a statistic. That approach safety is a +**passively-safe approach design with clearance-verified aborts (keep-out +violations 0/500 per catalog [L0, ds-v1, Wilson ≤ 0.0076])**, re-verified at L1 +(+J2) and L2 (+drag) — never "strict approach safety" or "safe satellite"; +the full campaign statistics and re-verification tables are +[safety.md](safety.md)'s to own, not repeated here. + +"Minimum cost" as an absolute claim is banned throughout this package. The +defensible claim is **cost-effectiveness — cost per removal and cost per unit +risk reduction** (see [cost_model.md](cost_model.md)). + +## Kessler-precursor removal — the cascade framing + +The collisional cascade's fuel is the population of massive intact derelicts +in congested orbital bands; fragments are the symptom. One intact-intact +collision produces thousands of trackable fragments — the 2009 Iridium 33 / +Cosmos 2251 collision alone produced 598 + 1603 = 2201 fragments cataloged by +the U.S. Space Surveillance Network as of January 2013 (Liou, J.-C., "An +Analysis of the FY-1C, Iridium 33, and Cosmos 2251 Fragments", NASA Orbital +Debris Program Office, NTRS 20150003820, 2014). Removing, or equipping for +removal, the objects that would become the next fragment clouds attacks the +source term rather than the symptom — that is what **Kessler-precursor +removal** names. + +Anchors in the open literature: + +- Kessler, D. J., Cour-Palais, B. G., "Collision Frequency of Artificial + Satellites: The Creation of a Debris Belt", *Journal of Geophysical + Research* 83(A6), 1978 — the cascade mechanism itself. +- Liou, J.-C., Johnson, N. L., "Risks in Space from Orbiting Debris", + *Science* 311(5759), 2006 — LEO population growth even without new launches + (the instability argument). +- Liou, J.-C., Johnson, N. L., Hill, N. M., "Controlling the growth of future + LEO debris populations with active debris removal", *Acta Astronautica* + 66(5-6), 2010 — the classical few-removals-per-year + environment-stabilization-class result (~5 objects/yr, jointly with ~90% + post-mission-disposal compliance — removal alone does not carry the result) + that sizes the campaign cadence ADSC targets. +- McKnight, D., et al., "Identifying the 50 statistically-most-concerning + derelict objects in LEO", *Acta Astronautica* 181, 282-291, 2021, + doi:10.1016/j.actaastro.2021.01.021 — a consensus ranking (11 expert teams) + dominated at the top by SL-16 / Zenit-2 second stages; average mass of the + top-50 ≈ 5,295 kg; removing the top-50 would roughly halve LEO + collision-risk (top-10 ≈ 30% reduction). This is the citation basis for + ADSC's reference target classes (D2). +- ESA Space Debris Office, "ESA's Annual Space Environment Report", + GEN-DB-LOG-00288-OPS-SD, Issue 10.0, 1 May 2026, §7.1 — the + environmental-index peak near ~800-900 km / 70-80° inclination matches + ADSC's catalog_A reference class (~840 km / ~71°), under a 90% + post-mission-disposal-success assumption. +- Population figures (≥1 cm ~1.2 M objects) are MASTER-8-class model values + (MASTER-8, released March 2019, reference epoch November 2016; Horstmann, + A., Hesselbach, S., Wiedemann, C., "Enhancement of S/C Fragmentation and + Environment Evolution Models", Final Report, ESA contract + 4000115973/15/D/SR, 2020), carried as PLACEHOLDER inputs in the T6 flux + table (`generated/t6_flux_sweep.md`). + +These are bibliographic/regulatory citations (years, DOIs, report editions), +not ADSC simulation results, and are cited here rather than machine-included +from a committed CSV. + +## Installer, not tug — the quantitative argument (D1) + +Deorbit Δv scales with the mass it must decelerate. A tug must decelerate the +full ~9,000 kg SL-16-class stage — roughly 304× the entire ADSC servicer's +own contact mass (29.6 kg dry+kit; see evidence pack §2) — and a realistic tug +bus would be heavier still. The installer instead transfers a 2.4 kg passive +kit and departs, so the propellant cost of the deorbit itself is carried by +drag (sail) or electrodynamic drag (tether), not by the servicer. One mission +services several targets in one plane (batch amortization — +[cost_model.md](cost_model.md) owns the amortization-curve numbers); the +capture interface is a geometry-keyed clamp on features present on every +upper stage by construction (nozzle throat / adapter ring, D4), not a generic +manipulator. This mass-ratio argument is the placeholder-independent core of +the package: it survives every open parameter, because it is a scaling +argument, not a point estimate. + +The GNC element itself is textbook-level by design (D11): Clohessy-Wiltshire +relative motion, quaternion sliding-mode tracking, multiplicative EKF — open +literature methods applied to class-level targets, with no live ephemerides +and no target-specific operational products +([gnc.md](gnc.md) owns the mechanism detail). + +## What an agency already has — the counterfactual + +An agency evaluating this package already has, or can buy, several adjacent +things. None of them is the installer trade this package makes: + +- **A funded capture/tug-class demonstration.** ESA's ClearSpace-1 mission — + contracted at **~€86M** (November 2020; ~€100M total project cost), now + targeting the defunct PROBA-1 satellite (retargeted from the Vespa adapter + in April 2024) — is presently expected to launch **~2028**. This is a + single-target capture-and-deorbit tug demonstration, not a multi-target + batch-amortized installer, and it does not by itself answer the + installer-vs-tug mass-ratio question this package argues quantitatively. +- **A funded national rocket-body-removal demonstration.** JAXA's Commercial + Removal of Debris Demonstration Phase II (CRD2) — Astroscale Japan's + ADRAS-J2, contracted at **~¥13.2B** (2024) — targets an unprepared Japanese + upper stage, with the demonstration presently planned toward **FY2027–28**. + Like ClearSpace-1, this is a single-target removal demonstration by a funded + agency program; it validates that agencies will pay for large-object + removal, not that an installer architecture is the cost-effective way to do + it at scale. +- **Published passive-kit physics, without an installer-architecture trade.** + The bare electrodynamic tether (EDT) literature is mature and citable: + Sanmartín, Martínez-Sánchez & Ahedo's 1993 OML bare-tether foundation paper + (*J. Propulsion and Power* 9(3), 353-360, doi:10.2514/3.23629), and the EU + FP7 BETs project (GA 262972, coordinated by Prof. Sanmartín, UPM, + 2010-2014), which demonstrates order-of-magnitude tether-mass-vs-deorbit-time + results (e.g. a 10 kg tether deorbiting a 1000 kg spacecraft from 1000 km in + about a month). Drag-sail deorbit is similarly well published. What this + literature does not provide is an open, honesty-audited trade of the + *installer architecture itself* — batch amortization across several + targets per mission, the geometry-keyed capture interface, and a + campaign-level Monte-Carlo safety case with clearance-verified aborts. + +**The niche this package fills**: an open, reproducible, honesty-audited +*installer-architecture trade* — not a new capture demonstration and not a +new tether-physics result — sitting between the funded single-target +tug/removal programs above and the published component-level kit physics, +making the batch-amortization and installer-vs-tug argument in a form a +skeptical engineer can regenerate byte-for-byte. + +## Scope guardrails + +- **Operator-legality framing (D9).** The package assumes the operator is, or + is contracted/consented by, the launching state of the target. Nothing in + this repository assumes or enables unconsented approach to another state's + object. +- **Dual-use guardrail (D11).** Methods stay at open-literature level (CW, + sliding-mode control, EKF are textbook material). Targets remain + class-level parameters: no mission-planning products against specific + catalog IDs, and no ingestion of live ephemerides/TLEs. +- **Language discipline (spec §1, binding across README/docs/evidence pack).** + The anchor phrase **"Kessler-precursor removal"** appears in every summary + text, including this one. Adoption-facing deliverables — README, docs/, + pitch docs — stay in English (R11); a Japanese equivalent of the summary + blurb may accompany it only in owner-facing material, not here. diff --git a/docs/cost_model.md b/docs/cost_model.md new file mode 100644 index 0000000..1e920de --- /dev/null +++ b/docs/cost_model.md @@ -0,0 +1,147 @@ +# Cost model + +This is the full detail behind the README's cost/FoM "Key results" digest. It +covers the parametric relative-cost model, the amortization curve, the +two-weighting figure of merit (FoM) with open trade T5, the WP14 +itemized-range/currency-anchor upgrade, and the honesty framing that governs +every absolute-dollar figure this project ever states. Campaign inputs are +[docs/safety.md](safety.md) (the full WP5 table); kit-class/target context is +[docs/target_selection.md](target_selection.md). + +**An absolute minimum-cost claim is banned** (spec section 1) — the +defensible claim throughout this file is **cost-effectiveness**: cost per +removal and cost per unit risk reduction. + +## The parametric model (WP6) + +`cost` implements the spec's + +``` +C_campaign = C_dev + C_bus(m_dry) + N·C_kit + C_launch(m_wet, band) + C_ops(T) +``` + +in **relative cost units (CU)** — no point-value dollar figure is ever +emitted from this core model (R6/D10); the CU→currency anchor is a cited +range, derived at runtime from the WP14 itemized table (see below), never a +hand-typed constant. It consumes the WP5 campaign (**schema 1.1** — +`generated/wp5_campaign_schema.md`; the amortization sweep re-runs the WP5 +engine across a kit-inventory sweep at the fixed master seed, and the +baseline (N = 4) reproduces `wp5_campaign_runs.csv` exactly). Cost, +cost/removal, and + +``` +FoM = Σᵢ mᵢ·w(hᵢ) / C_campaign +``` + +are propagated **per run** (p05/p50/p95, never mean-only). FoM is reported +under **two** normalized congestion weightings (spatial-density + +criticality-style, both PLACEHOLDER tables); they disagree on band priority +— **open trade T5**, discussed below. A machine-readable tornado ranks each +cost parameter's ± impact. Outputs `generated/wp6_cost_summary.{md,csv}` + +`wp6_cost_schema.md`, **schema 1.1** (the schema bumped from 1.0 to 1.1 for +WP14's absolute-cost rows; this file states the current version rather than +carrying the "1.0" label forward — a drift the WP15 content-mapping pass +found and fixed in this document, per R16's whole point). Unit-tested +(`tests/test_cost.cpp`). Regenerate with `./build/adsc_cost`. + +## Amortization curve, FoM, and tornado — the full table + + +The full sweep (500 runs × 2 catalogs × N = 1..8 kits) runs in a few tens of +seconds on a CI runner (see the Actions log of the current run for the actual +figure), and the baseline (N = 4) removals reproduce the committed WP5 CSV +exactly (`MATCH (schema 1.1 consumed)`). + +**Amortization curve — cost/removal vs kits carried N (SL-16 class):** + +| N | cost/removal p50 [CU] | ratio to N=1 | removals p50 | +|---:|---:|---:|---:| +| 1 | 157.07 | 1.000 | 1 | +| 2 | 82.22 | 0.523 | 2 | +| 3 | 57.27 | 0.365 | 3 | +| **4** | **44.80** | **0.285** | 4 | +| 5 | 46.14 | 0.294 | 4 | +| 8 | 50.15 | 0.319 | 4 | + +Batch amortization drives cost/removal down to **28.5 % of the single-target +(N = 1) baseline** — a **3.5× per-removal saving** — bottoming at N = 4 where the +**Δv budget (not the kit count)** caps removals at 4; carrying more kits then +adds cost without removals and the curve turns back up. That shape *is* the +quantitative installer/batch argument (SL-8 class is within ~0.3 %). + +**FoM = Σ mᵢ·w(hᵢ)/C_campaign at N = 4 (p50, kg/CU):** + +| catalog | spatial-density | criticality | +|---|---:|---:| +| SL-16 (~9 t, 840 km) | 156.70 (w = 0.78) | 200.90 (w = 1.00) | +| SL-8 (~1.4 t, 750 km) | 31.35 (w = 1.00) | 21.32 (w = 0.68) | + +The ~9 t class outranks the ~1.4 t class under **both** weightings (FoM is +mass-dominated), but the **band weight flips** — spatial density values the +750 km band more, the criticality index values the 840 km band more — a genuine +metric-choice disagreement tracked as **open trade T5**. **Tornado** (SL-16, +±30 %, ranked by cost/removal swing): `c_dev_cu` (15.0 CU) ≫ `c_bus_cu` (4.5) > +`c_launch_cu_per_kg` (3.7) > `c_kit_cu` (2.4) > `c_ops_cu_per_day` (1.2). All +values are relative CU; **no absolute-dollar figure is claimed**. + + +### Open trade T5 (metric choice changes band priority) + +The two normalized congestion weightings peak at different altitudes +(spatial ≈750 km, criticality ≈840 km), so they rank target *bands* +differently: the spatial-density weighting values the lower (catalog B, +≈750 km) band more, the criticality weighting values the higher (catalog A, +≈840 km) band more. Absolute per-catalog FoM stays mass-dominated under both +(catalog A outranks catalog B either way), but the band-priority flip is real +and is kept visible rather than resolved by fiat. Both weighting tables are +PLACEHOLDER and must be filled with citations; the criticality weighting's +peak is McKnight-anchored (catalog A, ≈840-850 km — see +[docs/target_selection.md](target_selection.md)), but the rest of the table +(the falloff away from that peak) remains an uncited interpolation. + +## Absolute cost ranges (WP14) + +Spec D10 rule, quoted verbatim: "Every row carries a source or stays +PLACEHOLDER — this is D10 applied, not relaxed. Relative CU results remain +primary." Absolute cost is itemized as **low/mid/high ranges only** across +development, manufacturing, launch/rideshare, ground segment, ops, +SSA/tracking, insurance/licensing, kit unit, and failure contingency, at both +per-target and per-campaign scale. Every row in the committed +`generated/wp6_cost_summary.csv` (schema 1.1, WP14 rows) carries a source +citation or is marked PLACEHOLDER — never silently filled. + +The **CU→currency anchor** is derived at runtime from a 5-component core of +that itemized table (development + manufacturing_bus_unit + +kit_unit_scaled_9t × baseline-N-kits + launch + operations_3yr), reported as +a low/mid/high range — never a point-value dollar figure. Two flagged rows +in the current itemized table are honestly non-authoritative: an SSA/tracking +subscription cost that vendors decline to quote publicly (marked +PLACEHOLDER) and a 9-t-class installer-kit unit cost for which no citable +product exists at this scale (marked as a training-data extrapolation). + +**Current computed anchor values, campaign totals, and cost-per-removal +ranges are not repeated here** — they already have one home (R16): see +[evidence/adsc_evidence_pack.md](../evidence/adsc_evidence_pack.md) section 5 +("Absolute cost ranges (WP14)") and the WP14 rows of +`generated/wp6_cost_summary.csv` directly, or the README's own Key-results +digest for the current headline figures. + +**Honesty framing**, quoted verbatim from `generated/wp6_cost_schema.md` +(never hand-retyped, so this document and the schema cannot drift apart): + +> External absolute-cost estimation by an unaffiliated author is inherently low-standing; agencies re-run with their own parametric models; the relative CU results remain primary. No point-value currency figure is emitted: absolute costs appear ONLY as low/mid/high cited ranges. + +Tug-architecture cost comparison is only performed on the **same target +set** (comparing different targets is meaningless and banned) — see the +Class C discussion in [docs/target_selection.md](target_selection.md). + +## What this cost model does not claim + +- No absolute-dollar point value is ever emitted by the CU core model; + currency figures appear only as cited low/mid/high ranges (D10). +- No absolute minimum-cost claim — cost-effectiveness (cost per removal, cost + per unit risk reduction) is the defensible framing throughout. +- The FoM band-priority disagreement (T5) is not resolved by this model; it + is a metric-choice fact, reported rather than adjudicated. +- Legal accessibility is a gate, never a scoring multiplier, in any figure + here (D12) — see [docs/legal_regulatory.md](legal_regulatory.md). diff --git a/docs/gnc.md b/docs/gnc.md new file mode 100644 index 0000000..673779b --- /dev/null +++ b/docs/gnc.md @@ -0,0 +1,174 @@ +# GNC — dynamics, control, estimation, guidance + +Not a mission proposal; TRL 4, GNC software element only +([roadmap.md](roadmap.md)). This page owns the **mechanism** of the core GNC +algorithms (WP1 relative motion, WP2 controller + tumble sync, WP4 estimator, +the WP11 guidance-law mechanics) and, per R16, states numbers only via +generated includes, explicit references to [safety.md](safety.md) / +`generated/reference_metrics.csv` / the evidence pack, or — where no CSV +backs a number yet — prose with the exact regeneration command cited. The +campaign-level safety claims these mechanisms produce (keep-out rates, +abort-event statistics, contact-energy budgets) are [safety.md](safety.md)'s +to own; this page never restates them. + +## Relative orbital motion (WP1) — `relmotion` + +Clohessy-Wiltshire (Hill) linear dynamics about a circular target orbit in +the LVLH frame, with **both** an analytic state-transition matrix and a +fixed-step RK4 integrator, cross-validated against each other. Includes a +drift-free "safety ellipse" construction and a passively-safe +approach-corridor generator; `compute_safe_abort` returns a +Clohessy-Wiltshire impulse toward a drift-free relative orbit through the +current position, capped at the thruster budget, with a `Clean`/`Capped` +status carrying the verified minimum range of the propagated post-burn coast +(when the cap binds, the code reports the actual coast range rather than +implying safety — the honesty argument and its numbers are +[safety.md](safety.md)'s). Unit tested (`tests/test_relmotion.cpp`), +including a ≥100-point thrust-off coast check. + +**Reproducible WP1 numbers** — mean motion and orbital period for the 825 km +reference orbit are printed by `./build/adsc_sim` console output only; there +is no committed CSV row backing them yet (a documented gap, not a hidden +one). Regenerate and read them directly: `./build/adsc_sim` (n ≈ 1.03×10⁻³ +rad/s, period ≈ 6084 s, at the time of writing — treat the console output as +authoritative, not this sentence). The keep-out-corridor claim itself (worst +coast range vs. the 200 m keep-out sphere) is a safety number owned by +[safety.md](safety.md), which cites `generated/reference_metrics.csv` +(`wp1_coast_samples`, `wp1_worst_coast_min_range_m`, `wp1_keep_out_radius_m`). + +## Rigid-body dynamics + sliding-mode controller (WP2) — `dynamics`, `controller` + +**Rigid-body attitude dynamics**: quaternion kinematics + Euler's equation +with a full inertia tensor, integrated with fixed-step RK4. + +**Sliding-mode attitude controller, tracking form**: a quaternion sliding +surface on the *relative* motion, `s = w_e + λ·sign(q_e0)·q_ev` with +`w_e = w − C(q_e)ᵀ·w_t`, boundary layer (saturation instead of `sign` to +suppress chatter), per-axis DACS deadband and torque clamp, and +target-motion feedforward: the target's torque-free acceleration +`ẇ_t = I_t⁻¹(−w_t × I_t w_t)` plus the transport term +`−w_e × C(q_e)ᵀw_t`. Regulation (the v2 detumble case) is the `w_t = 0` +special case, and its code path is kept verbatim so the quoted regression +numbers cannot drift — the equivalence is asserted in `tests/test_sync.cpp`. + +**Tumble synchronization** (`run_tumble_sync`): closed-loop sync against a +torque-free tumbling target with deliberately asymmetric inertia (so the +rate vector precesses — an isotropic target would tumble at constant rate +and make tracking trivially easy). Acceptance — |w_rel| < 0.1 deg/s and +attitude error < 2° held 30 s — is asserted in `tests/test_sync.cpp`, plus a +**feedforward-honesty test** that disables the reaching term (k = 0) so SMC +robustness cannot mask a wrong feedforward: on a perfectly synchronized start +the sliding variable must stay parked near zero with nothing to rescue it. + +**Point-mass inertia update** on capture (parallel-axis), with numerical +regularization before inversion. + +**Reproducible WP2 numbers** (regenerate: `./build/adsc_sim`, scenario 5; +against a 2.0 deg/s precessing tumble, target inertia ratios 1.0/0.6/0.3, +from a 40° attitude offset at zero rate): + + +| metric | value | tolerance | +|---|---:|---:| +| sync time | **16.87 s** | criteria first held, then dwelled 30 s | +| max \|w_rel\| after dwell | **0.021284 deg/s** | tol 0.1 deg/s | +| max attitude error after dwell | **0.098531°** | tol 2.0° | + + +The v2 regulation-mode detumble regression reference (a different scenario — +the `w_t = 0` special case, not the tracking sync above) is +`v2_detumble_settle_time_s` — see `generated/reference_metrics.csv`, or the +evidence pack §3, rather than restated here. + +Two figures from the same scenario have no committed CSV backing today (a +documented gap, prose-pinned only): the end-of-120s-run figures +(0.00043 deg/s max rate, 0.098° max attitude error — regenerate: +`./build/adsc_sim` scenario 5, read the console tail) and the +feedforward-honesty bound (sliding variable with the reaching term disabled, +limit 5×10⁻³ — this is a test threshold constant in `tests/test_sync.cpp`, +not a CSV row; run `./build/test_sync` to see it asserted and printed). + +## Estimator + sensor models (WP4) — `estimator` + +The sync control loop consumes **estimates, not truth** — enforced +structurally: the controller is handed an `EstimatedState` only, and truth is +read solely by the sensor models and the error-statistics recorder. Two +decoupled filters (assumptions documented in the header): a **translation +EKF** on the 6-state LVLH relative motion (prediction = the analytic WP1 CW +state-transition matrix; 10 Hz range + line-of-sight **unit-vector** +measurements — az/el is avoided because its Jacobian is singular at +boresight while `d(r/|r|)/dr = (I−r̂r̂ᵀ)/|r|` is regular everywhere), and a +**multiplicative attitude EKF** (3-component small-angle error states, +covariance 3×3/6×6; an additive quaternion EKF is deliberately not used) +with gyro-propagated own attitude + star-tracker updates and a 2 Hz vision +relative-pose channel estimating `q_rel` and the target rate `w_t`. +Covariance updates are Joseph-form + symmetrized; the tests check P symmetry +and positive definiteness over a full run, and a **NEES/NIS consistency +watchdog** (fixed seed, χ² bands) rejects the classic fake of inflating Q/R +until acceptance passes. All randomness is one fixed-seed `mt19937` + +explicit Box-Muller (bit-stable across platforms). + +Estimator scope is deliberately narrow: target inertia is assumed **known** +for the torque-free feedforward and the MEKF model; sensor models are +Gaussian abstractions (no outliers, no dropouts in WP4 itself, no +star-tracker occlusion, no vision-pose ambiguity); gyro/rangefinder biases +exist as config knobs but are **not estimated** in the WP4 baseline (the +consistency tests assume the zero defaults). Attitude sync in the campaign +remains the truth-driven WP2 primitive (a documented WP5 simplification — +see [limitations.md](limitations.md)). + +**Reproducible WP4 numbers** — sync time, RMS (relative attitude, relative +position), and NIS/NEES (translation channel) are committed in +`generated/reference_metrics.csv` (`wp4_sync_time_s`, `wp4_rms_att_rel_deg`, +`wp4_rms_pos_m`, `wp4_nis_trans`, `wp4_nees_trans`) — see +[safety.md](safety.md) / the evidence pack §3 for the stated values rather +than a second copy here. The **per-channel breakdown** — own-attitude RMS, +target-rate RMS, velocity RMS, star-tracker/vision NIS/NEES, covariance +min-eigenvalue/asymmetry — is the largest documented gap in this package: +none of it is in `reference_metrics.csv` today. Regenerate and read it +directly: `./build/adsc_sim` scenario 7 (fixed seed 20260703), console +output. Extending `reference_metrics.csv`/`sim_metrics` to carry these +figures, or dropping them from any future headline claim, is an open +decision flagged for the owner rather than silently resolved here. + +## Guidance mechanics (WP11 law + WP12 L4) + +**Clearing-abort law + reachability screen + closed-loop guidance** +(`compute_clearing_abort` / `clearing_abort_for`, `guidance`): a 3-stage +escalation ladder that accepts an abort only when the analytic post-burn +ellipse (`bounded_coast_min_range`, closed form) clears keep-out plus a +design margin — drift-null baseline, bounded radial reshape, two-impulse +retreat hop — with the legacy `Capped` honesty preserved past the Δv budget. +The campaign keep-out screen runs this law (D13); a truth-fed L0 +guided-approach demo (far approach → hold → sync → final approach → contact +→ retreat) screens abort feasibility before every committed impulse and +produces the contact speed by a glideslope-with-floor profile rather than +merely gating it. Unit tested (`tests/test_guidance.cpp`) plus the pinned +forensic-14 regression (`tests/test_forensic14.cpp`): the legacy law +reproduces all 14 WP10c violations analytically, the clearing law clears +every one. The campaign-level abort/keep-out statistics this law produces +are [safety.md](safety.md)'s. + +**L4 — estimate-driven guidance** (WP12 fidelity-ladder element, mechanism +only; the ladder's own architecture is [technical_architecture.md](technical_architecture.md)'s): +the same guidance mode machine, but with the WP4 translation EKF in the +loop — truth is error-recording only — under measurement dropout and an +unestimated range-bias random walk. This is the honest place the estimator +and the guidance law meet: an unestimated bias walk makes the filter +optimistic about its own error (elevated NEES), which is a first-class +reported finding, not a hidden one — the exact figures live in +[safety.md](safety.md) / the evidence pack §3 (fidelity ladder) rather than +here. + +## Subsystem detail + +- **First-order PCM thermal budget** integrated over the control loop — a + single lumped bucket, no eclipse/sunlight radiative balance, no per-node + conduction. +- **Deorbit gating**: autonomous by default, human-in-the-loop only on the + emergency path, blocked below the fuel reserve. + +Full honest-scope caveats for everything on this page (estimator narrowness, +the retired continuous-torque DACS approximation, model-scoped passive +safety, campaign dispersion scope) are [limitations.md](limitations.md)'s to +own in one place, not repeated here. diff --git a/docs/legal_regulatory.md b/docs/legal_regulatory.md new file mode 100644 index 0000000..92ed99c --- /dev/null +++ b/docs/legal_regulatory.md @@ -0,0 +1,115 @@ +# Legal and regulatory precheck + +**This is not legal advice.** Nothing in this file, in the WP8 Compliance +Matrix Generator, or anywhere else in this repository is a legal conformity +determination. It is the full detail behind the README's compliance-related +lines: the WP8 precheck's scope and mechanism, the D9/D12 framing decisions, +the rulepack staleness caveat, and the honest jurisdiction-coverage gaps. +Consent-gate application to a specific mission class (the Class-C +controlled-reentry comparison) is [docs/target_selection.md](target_selection.md); +the peaceful-use disclaimer is the README's License section. + +## Framing: D9 (operator-legality) and D12 (gate, not multiplier) + +- **D9 — Operator-legality framing.** The package assumes the operator is, + or is contracted/consented by, the launching state of the target. Nothing + in this repository assumes or enables unconsented approach to another + state's object; this framing is the targeting rationale, not merely a + constraint bolted on afterward. +- **D12 — Legal accessibility is a gate and metadata flags, never a + multiplier in the FoM.** The compliance engine (WP8) already blocks + unconsented ADR; target prioritization ([docs/target_selection.md](target_selection.md)) + consumes its PASS/BLOCK output and flags. Rationale: open trade T5 already + shows metric-choice sensitivity in the cost/FoM model + ([docs/cost_model.md](cost_model.md)); folding a subjective legal weight + into a product score would multiply arbitrariness and damage credibility. + +## WP8 precheck: scope and mechanism + +The Compliance Matrix Generator (`tools/compliance/`) is a Python 3 +standard-library-only checker (no `jsonschema` — validation is hand-written +and dual-maintained with the documented `mission_profile.schema.json`) that +evaluates a declared mission profile against **versioned rulepacks**: UN +treaties, US FCC/FAA/NOAA, ITU, ESA reference, and ADSC internal policy — +**primary sources only**. It emits `generated/compliance_findings.json` and +`evidence/compliance_matrix.{md,csv}` (schema 1.0, deterministic, no +timestamps). Every finding separates **binding law / non-binding guideline / +agency guidance / ADSC policy / placeholder research assumption** (see the +binding-type legend in `evidence/compliance_matrix.md`) — these categories +are never conflated. Unit-tested (`tools/compliance/test_compliance.py`, +ctest `compliance`). + +**Policy, both directions:** *unknown is safer than a false PASS* — missing +inputs are UNKNOWN or fail, never PASS; missing mandatory evidence yields +WARN/BLOCK. Unconsented active interference with a registered object is +**BLOCKed** as ADSC policy (`ADSC-POL-01`), test-enforced — and any +live-ephemeris or target-specific-product profile is also BLOCKed (D11 +guardrail, `ADSC-POL-02`). The gate genuinely works both ways: it is not only +a permission check, it is also a scope guardrail that blocks the package's +own dual-use risk. + +The precheck also cross-checks the WP5 campaign's compliance metadata +columns against the declared profile (`ADSC-META-01`) — a consistency check +between two research artifacts, making no statement about the real world. + +## Compliance summary (current committed profile) + +For the committed research profile +(`tools/compliance/examples/adsc_research_profile.json`): **PASS=6, INFO=2, +WARN=1, BLOCK=0, UNKNOWN=0, NOT_APPLICABLE=9** — see +[evidence/adsc_evidence_pack.md](../evidence/adsc_evidence_pack.md) section 7 +and [evidence/compliance_matrix.md](../evidence/compliance_matrix.md) for the +full per-rule findings table (never re-derived here, R16). The research-only, +class-level profile is not blocked merely for being an ADR concept; the one +WARN is the honest export-control-review-not-started flag (`ADSC-EXP-01`). +Consent in the research profile is a **declared scenario assumption** (D9), +never a legal fact — the finding notes must be read that way. + +## Rulepack staleness caveat + +Rulepacks are **versioned research artifacts that can go stale** as +regulations change. Every rule carries a `source_date_or_version` and a +`limitations` field; low-confidence rules are downgraded to +`binding_type: placeholder`. Regenerate and re-verify against current law +before any real-world use — this is stated in the compliance matrix itself +and repeated here because it is easy to forget once a precheck has PASSed +once. + +Rule dates currently carried (citations, not simulation results — not +subject to the numeric-literal-outside-include-block scan): the **IADC +25-year** post-mission-disposal guideline (IADC-02-01 Rev. 2, March 2020; +later revisions exist — confirm the current one before citing); the **US FCC +5-year** rule for LEO space stations (FCC 22-74, adopted 29 September 2022); +the **ESA** debris-mitigation reference (ESSB-ST-U-007 Issue 1, 30 October +2023, contractual on ESA-procured missions, cited as a reference here, not a +legal requirement on third parties). The 25-year IADC line is the one this +project's own decay trades report ([docs/target_selection.md](target_selection.md)); +under an FCC-jurisdiction scenario the operative standard is stricter (5 +years), which makes the sail-only negative for catalog A strictly harder, +not easier. + +## Jurisdiction coverage gaps (honest, partial) + +Coverage today is **UN treaties + US agency rules (FCC/FAA/NOAA) + ITU +references + ESA reference guidelines only**. **Russian, Chinese, and +Japanese national law — all adopter targets for this package — are not yet +covered.** This is future work, stated plainly rather than implied to be +complete. Every rule's own `limitations` field additionally flags where the +precheck only verifies that evidence was *declared*, not that it is valid or +sufficient (e.g. Outer Space Treaty Art. VI: "verifies that authorization +evidence is declared, not that a valid authorization exists or covers this +activity"). + +## What this precheck does not claim + +- **Never a legal conformity determination.** It reports evidence gaps + against versioned research-grade rulepacks; the compliance matrix says so + in its own header, and this document repeats it because it is the single + most important honesty line in the package. +- No jurisdiction beyond UN + US + ITU + ESA reference today (above). +- No export-control clearance — `ADSC-EXP-01` is an open WARN, not a PASS, + and open-literature methods (D11) are the current basis for research + distribution, not a substitute for a completed review. +- No claim that the WP5 campaign's compliance metadata says anything about a + real mission — it is a consistency check between two research artifacts + (`ADSC-META-01`). diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 0000000..d99bd91 --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,200 @@ +# Limitations (honest scope) + +This is the project's one-stop, honest "what is not real yet" list — the +full detail behind the README's **Not-implemented** and **Limitations** +lines. It carries the old README's NOT-implemented bullets forward in full, +the binding TRL statement, the model-scoping summary, a pointer to the +mechanically-collected PLACEHOLDER inventory, and the contact-energy +worst-case caveat with its arithmetic. Per R16, where a number below already +has a home in another docs/ file, this document cross-refers to it instead +of restating it — the qualitative limitation is repeated here (that is the +point of a single honest list), but the pinned figure is not duplicated. + +## What is explicitly not implemented + +- **Estimator scope is deliberately narrow (WP4).** The target inertia is + assumed **known** for the torque-free feedforward and the MEKF model — + exact in this simulation, but a real mission needs inertia identification. + Sensor models are Gaussian abstractions (no outliers, no dropouts, no + star-tracker occlusion, no vision pose ambiguity); gyro/rangefinder biases + exist as config knobs but are **not estimated**. The translation state is + estimated, consistency-tested, and — since WP12 L4 — **used for control**: + the guided approach flies on the translation EKF's estimate under + measurement dropout and an unestimated range-bias walk, including the + honest NEES inconsistency that the unestimated bias produces (pinned + figures and L-tags: [docs/safety.md](safety.md) WP12 section). Attitude + sync in the campaign remains the truth-driven WP2 primitive (a documented + WP5 simplification). +- **Continuous-torque DACS approximation — retired at L5 (WP12), with an + honest remainder.** The v2–v4 controller assumed continuous torque; WP12 + L5 re-demonstrates sync under a delta-sigma minimum-impulse-bit actuator + model instead (pinned sync-time comparison: [docs/safety.md](safety.md)). + The MIB magnitude is PLACEHOLDER and ideal duty-cycling (no thruster + hardware model, no minimum on-time) is still assumed; the faster-looking + crossing under degradation is a metric artifact of the + first-crossing-then-dwell criterion, not a physical speed-up — the claim + is only that sync still completes under MIB + delay + fault. +- **Thermal model is a single lumped PCM bucket** — no eclipse/sunlight + radiative balance, no per-node conduction. +- **Closed-loop rendezvous guidance is L0 truth-fed (WP11) plus L4 + estimate-driven (WP12), not flight guidance.** The L0 mode machine (far + approach → hold → sync → final approach → contact → retreat) still flies + the approach to contact truth-fed; WP12 L4 makes it estimate-driven under + measurement dropout and an unestimated range-bias random walk, with the + identified-but-not-implemented fix being explicit bias states in the + filter (pinned contact-speed and NEES figures: + [docs/safety.md](safety.md)). As of WP4 the attitude-sync loop consumes + estimates from noisy sensors; attitude sync in the campaign remains the + truth-driven WP2 primitive, and the truth-driven variants remain in the + test suite as control-law regression references. +- **Decay model is first-order and single-object (WP3).** Quasi-circular + drag decay over an exponential atmosphere with a single altitude- + independent solar-cycle factor (a coarse proxy — the real swing is + altitude-dependent); no J2, no attitude-dependent ballistic area, no + re-entry demisability model — that safety/cost conflict is **open trade + T2** (controlled vs uncontrolled reentry, driven by demisability; see + [docs/target_selection.md](target_selection.md) for the Class-C + discussion). The EDT branch is a physics-based band model (WP13), not a + tether-hardware performance guarantee, and libration/dynamic stability + remains **open trade T7** ([docs/target_selection.md](target_selection.md)). +- **Passive-safety claims are model-scoped (F2).** The keep-out and + safety-ellipse guarantees are exact only in the linear Clohessy-Wiltshire + model: circular target orbit, no J2, no differential drag, small + separations. J2 and differential drag erode drift-free safety ellipses + over time, and the CW linearization error grows with separation — so + every passive-safety number in this package holds in the model, not in + the real environment. A real mission would re-verify all coasts against a + higher-fidelity propagator; the WP12 fidelity ladder + ([docs/safety.md](safety.md)) is a first concrete step toward that, not a + substitute for it. +- **Campaign scope is deliberately narrow (WP5).** The campaign reuses the + truth-driven `run_tumble_sync` primitive for tractable N≥500 Monte Carlo, + so the dispersions that flow through it are the initial-condition and + vehicle ones (tumble rate/axis, servicer initial attitude, actuator + torque-scale and misalignment); **sensor noise/bias dispersions are drawn + and their realized factors recorded but not re-propagated per run** — the + closed-loop sensor effect is characterized by the WP4/WP12-L4 + estimate-driven acceptance instead. The Δv, time, and phasing cost + coefficients and all dispersion magnitudes are PLACEHOLDER. WP5 produces + **no** visualization/charts and performs **no** legal or regulatory + determination: the future-facing compliance columns are passive research- + profile metadata (`owner_consent_assumed` is a research scenario + assumption per D9, never a legal fact — see + [docs/legal_regulatory.md](legal_regulatory.md)). +- **Cost model is relative and parametric (WP6).** Every cost/FoM + coefficient and both congestion-weight tables are PLACEHOLDER; outputs are + in relative CU. WP6 predicts **no absolute program cost** on its own — a + cited CU→currency range is the WP14 upgrade + ([docs/cost_model.md](cost_model.md)), and no point-value dollar figure is + ever emitted anywhere. Per-catalog FoM is dominated by removed mass; the + metric-choice disagreement between the two weightings is **open trade + T5** ([docs/cost_model.md](cost_model.md)). WP6 emits no charts. +- **The compliance precheck is not legal advice (WP8).** It checks a + declared profile against research-grade rulepacks and reports evidence + gaps — it does not and cannot determine legal conformity. Rulepacks are + versioned snapshots that can go stale, and jurisdiction coverage is + honestly partial (UN treaties + US + ESA reference only — Russian, + Chinese, and Japanese national law, despite being adopter targets, are + **not** yet covered). Full detail: [docs/legal_regulatory.md](legal_regulatory.md). +- **Small-debris (1–10 cm) removal is out of scope — open trade T6, resolved + and visible, not neglect.** At ≈10 km/s the specific kinetic energy is + ≈50 MJ/kg (≈12× the TNT specific-energy ratio); a 1 cm aluminum fragment + carries ≈70.7 kJ (≈16.9 g TNT-equivalent); removing 1%/yr of the ≥1 cm + population would need km²-scale collection area that would itself be the + largest collision cross-section in the band. Full table: + `generated/t6_flux_sweep.md`. +- Numbers for mass, inertia, power, PCM capacity, and the target + orbit/keep-out radius are plausible placeholders, not derived from a + specific bus or target design. + +## TRL statement (binding, spec section 9) + +**TRL 4 — for the GNC software element only.** With WP4 the attitude-sync +GNC element (tracking controller + estimator + sensor models) runs closed +loop **on estimated states** in a laboratory/simulation environment, with +filter consistency verified (NEES/NIS) rather than assumed — this is the +element-level TRL 4 definition. Scope caveat, stated plainly: **TRL 4 +applies to the GNC software element, not the system** — system-level TRL is +undefined and not claimed here. Reaching TRL 5 for the element requires +real-time processor-in-the-loop execution on representative hardware (WP9, +**reserved, not started** — the flight-software migration path is documented +in the evidence pack's annex, not implemented). WP5 (campaign robustness), +WP6 (cost/FoM), WP10 (forensics), WP11 (safety hardening + guidance), WP12 +(fidelity ladder), and WP13 (kit-class trade) all strengthen the TRL-4 +evidence base or widen the validated envelope at TRL 4 — none of them raises +the TRL. Fidelity upgrades widen the validated envelope; they do not raise +maturity. This statement holds throughout WP10–WP15 and is repeated verbatim +across README, this file, and the evidence pack so it cannot silently drift. + +## Model-scoping summary + +Every safety or performance claim in this package carries an explicit +fidelity-level tag (L0…L6, R14). The short version: **L0** = linear +Clohessy-Wiltshire, the baseline every committed number traces back to; +**L1** = +J2 numerical propagation; **L2** = +free-molecular drag under an +independent dispersion stream; **L4** = navigation-error extensions (dropout, +bias walk) in the guidance loop; **L5** = actuator realism (minimum impulse +bit, command delay, single-axis fault). No claim in this package extends +past L2 for passive-safety geometry, or past L5 for actuator/guidance +realism — see [docs/safety.md](safety.md) for exactly which numbers carry +which tag, and [docs/gnc.md](gnc.md) for the underlying math each level +touches. + +## Contact-energy worst case: the 0.41 J caveat + +The committed contact-energy budget is **0.333 J** at the 0.15 m/s gate +speed with the 29.6 kg **dry+kit** servicer mass — that number is +machine-read from `generated/reference_metrics.csv` and owned by +[docs/safety.md](safety.md) (R16: one home per number). A second, worse-case +figure is quoted for the condition of **maximum residual propellant** +still aboard at contact, and it is documented here, in full, as an honest +gap rather than a mechanically-generated one: + +``` +dry_mass_kg (27.2) + kit_mass_kg (2.4) + initial_fuel_kg (7.2) = 36.8 kg +0.5 * 36.8 kg * (0.15 m/s)^2 = 0.414 J (quoted as "~0.41 J") +``` + +The three mass terms are committed `Config` constants +(`include/adsc/mission.hpp: dry_mass_kg, kit_mass_kg, initial_fuel_kg`); the +speed term is the same `max_v_rel` gate used for the 0.333 J figure. **This +arithmetic is not currently emitted by any generated CSV or by any binary's +console output** — it is a hand-computed illustrative combination of already +-committed constants, which is exactly the category R16 is designed to +close over time. It is kept as prose, marked as a gap, rather than silently +promoted to a machine-checked number it is not yet: **regenerate the +underlying masses** with `./build/adsc_sim` (scenario 6 prints the dry+kit +contact mass and gate speed) and cross-check `initial_fuel_kg` directly in +`include/adsc/mission.hpp`; turning this specific combination into a +committed row of `generated/reference_metrics.csv` (via `sim_metrics`) is +open follow-up work, not done in this PR. The gate speed is the conservative +term in this comparison (fixed by design, not by mass); the mass term is +not — a heavier residual-propellant case is a real, if less likely, +operating point. **Open trade T8** tracks whether either energy figure is +below any validated material-damage threshold for aged MLI/paint — neither +is claimed to be (see [docs/safety.md](safety.md)). + +## PLACEHOLDER inventory + +Every unvalidated parameter in the repository is marked with the uppercase +`PLACEHOLDER` tag (R10) and mechanically collected into one appendix — never +hand-maintained, never silently dropped. Current total: **140** marks +(**56 decision-critical**, **43 moderate**, **41 cosmetic**) — see +[evidence/adsc_evidence_pack.md](../evidence/adsc_evidence_pack.md) appendix +("Appendix — PLACEHOLDER inventory") for the full, per-line, per-importance +table, regenerated by `tools/evidence/make_evidence.py`. If a value is +listed there, treat it as unvalidated until a cited source replaces it. +Decision-critical items (target masses/altitudes/inclinations, +collision-risk weights, cost coefficients, phasing Δv, sensor/actuator +uncertainties, contact threshold, density/solar model, EDT performance) are +the priority burn-down list; unresolved ones keep an explicit impact +statement rather than a silent PASS. + +## Cross-references + +- Pinned safety/campaign figures with their L-tags: [docs/safety.md](safety.md) +- GNC/estimator math and per-level model detail: [docs/gnc.md](gnc.md) +- Kit-class trade, EDT model scope, T1/T7 open items: [docs/target_selection.md](target_selection.md) +- Cost/FoM model, T5 open item: [docs/cost_model.md](cost_model.md) +- Regulatory precheck scope and jurisdiction gaps: [docs/legal_regulatory.md](legal_regulatory.md) +- One-code-path fidelity-ladder architecture: [docs/technical_architecture.md](technical_architecture.md) diff --git a/docs/one_pager.md b/docs/one_pager.md new file mode 100644 index 0000000..15f6b96 --- /dev/null +++ b/docs/one_pager.md @@ -0,0 +1,66 @@ +# ADSC — One-Page Overview + +> ADSC is not a debris collector. It is a low-cost concept for +> Kessler-precursor removal: a small servicer installs a passive deorbit kit +> on high-risk massive derelicts and departs, preventing future fragment +> clouds before they form — maximizing cleanup value per unit cost with a +> passively-safe approach design. + +**What this is.** An open, reproducible **evidence package** for +installer-type active debris removal (a C++17 GNC simulator plus generated, +claim-audited artifacts) — not flight software and not a mission proposal +(TRL 4, GNC software element only). The product is the evidence pack itself: +every number regenerates byte-for-byte from a clean clone. + +**The placeholder-independent core.** An installer never carries per-target +deorbit Δv and never tows or detumbles a multi-tonne stage; batch +amortization across several targets per mission is a mass-ratio scaling +argument that survives every open parameter — see README **Key results** / +[concept.md](concept.md). + +## Five headline numbers (each sourced, none re-derived here) + +1. **Batch amortization** drops cost-per-removal to a fraction of the + single-target baseline at the Δv-limited optimum kit count — README + **Key results** / [cost_model.md](cost_model.md). +2. **Approach safety**: zero keep-out violations across the full campaign + catalog, re-verified at two higher fidelity levels (+J2, +drag) with zero + violations — README **Safety status** / [safety.md](safety.md). +3. **Campaign robustness**: a majority-productive mission-success rate under + full Monte-Carlo dispersion, reported with Wilson 95% confidence + intervals, never as a bare point estimate — README **Key results** / + [safety.md](safety.md). +4. **Kit trade**: a drag sail closes the 25-year deorbit guideline for the + lighter reference class but not the heavier one; an electrodynamic + tether is carried as an honest open candidate for the heavier class — + [target_selection.md](target_selection.md). +5. **Figure of merit** (debris-risk reduction per cost) ranks the heavier + reference class first under both weightings evaluated — cited, not + asserted — [cost_model.md](cost_model.md) / + `generated/wp14_prioritization.md`. + +## The honest negatives + +- **TRL 4, GNC software element only** — system-level TRL is not claimed; + WP9 (processor-in-the-loop) is reserved and the only path to TRL 5. +- **Not flight software, not a mission proposal, not legal advice.** +- **Linear-CW scope**: exact passive-safety statements hold in the stated + models (L0/L1/L2), not the real environment; a real mission re-verifies + against a higher-fidelity propagator. +- **No inertia identification, no plane-change optimization, no homebrew + breakup/plasma models.** +- **140 PLACEHOLDER marks remain (56 decision-critical)** — every one is + inventoried, none is hidden. Full list: [limitations.md](limitations.md), + evidence pack §10. +- **Absolute costs are cited low/mid/high ranges only** — never a + point-value figure, and "minimum cost" is a banned absolute claim + throughout (the defensible claim is cost-effectiveness). + +## Reproduce + +``` +git clone https://github.com/HeliCorgi/ADSC.git && cd ADSC && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build && bash tools/regenerate_all.sh build +``` + +Full package: [README](../README.md) · +[evidence pack](../evidence/adsc_evidence_pack.md). diff --git a/docs/pitch_agencies.md b/docs/pitch_agencies.md new file mode 100644 index 0000000..07f29f3 --- /dev/null +++ b/docs/pitch_agencies.md @@ -0,0 +1,146 @@ +# ADSC — Adoption Brief for Mission-Design Teams + +> This is an **adoption brief / concept dossier for mission-design teams — +> not a mission proposal** and not a funding request. It is agency-agnostic +> by design: ADSC seeds ideas for any launching state's space agency or its +> contractors (spec §1 identity), not a pitch addressed to one in particular. +> Nothing here is legal advice, a flight-readiness claim, or a commitment on +> behalf of any agency. TRL 4, GNC software element only — see +> [roadmap.md](roadmap.md). + +ADSC is a low-cost concept for **Kessler-precursor removal**: a small +servicer installs a passive deorbit kit on high-risk massive derelicts and +departs, preventing future fragment clouds before they form — maximizing +cleanup value per unit cost with a passively-safe approach design (spec §1 +blurb, verbatim). Full framing: [concept.md](concept.md). + +## Who this is realistically for + +Read plainly, not oversold: the nearer-term, higher-confidence value of this +package today is as a citable, reproducible reference for the active-debris- +removal research and concept-study community. Adoption by an agency +mission-design process is the aspirational case this brief is written for, +not a claimed outcome — an internal review of the honest odds put +P(this artifact alone moves an agency design decision) low, and +P(it earns citation/reference value in the ADR research community) high. +That asymmetry is why the brief leads with the argument and the evidence, +not with a request. + +## The core argument — survives every open parameter + +An installer never carries per-target deorbit Δv and never tows or +detumbles a multi-tonne stage; it transfers a small passive kit (drag sail +or electrodynamic tether) and departs, so the deorbit propellant cost is +carried by drag, not by the servicer. Batch amortization across several +targets in one mission drives cost-per-removal down sharply as kit count +grows, until the Δv budget — not the kit count — caps removals (measured, +not asserted; see README **Key results** / [cost_model.md](cost_model.md) +for the amortization-minimum figure and the curve that produces it). This +is a mass-ratio scaling argument, not a point estimate: it holds regardless +of which cost coefficient or target mass is still PLACEHOLDER. Lead with +this when the placeholder-dependent numbers invite scrutiny — the argument +does not depend on them. + +## What is done vs. what is research — one glance + +| Done (simulated, reproducible, TRL 4 GNC element) | Research / explicitly open | +|---|---| +| Closed-loop attitude (SMC) + translation (EKF) GNC, campaign Monte-Carlo robustness, WP12 fidelity ladder (L0 CW → L1 +J2 → L2 +drag, one code path), clearing-abort guidance | Flight-qualified code (WP9, reserved — the only path to TRL 5) | +| Passive-kit decay trade: drag sail + WP13 electrodynamic-tether (EDT) physics band | EDT libration dynamics (open trade T7), plasma-density citation gaps | +| Relative-cost-unit (CU) model, primary metric, plus WP14 cited absolute-cost ranges | Point-value absolute costs (excluded by design — see below) | +| Regulatory precheck against versioned rulepacks (UN/FCC/ITU/ESA references) | Legal determinations — the precheck is explicitly **not legal advice** | +| Reproducible, byte-identical evidence pack regenerated from a clean clone | Inertia identification, plane-change optimization, homebrew breakup/plasma models | + +Full honest-scope list, one click away: [limitations.md](limitations.md) and +the evidence pack's PLACEHOLDER inventory (140 marks, 56 decision-critical — +evidence pack §10). + +## Own the counterfactual: what an agency already has + +None of the following is being reproduced or claimed here; they are cited +for context so the niche this package fills is stated plainly rather than +implied. Full citations and dates: [concept.md](concept.md). + +- **A funded capture/tug-class demonstration.** ESA's ClearSpace-1 — + contracted at ~€86M, now targeting the defunct PROBA-1 satellite, + presently expected to launch ~2028 — is a single-target capture-and- + deorbit tug demonstration. It does not answer the installer-vs-tug + mass-ratio question this package argues quantitatively. +- **A funded national rocket-body-removal demonstration.** JAXA's CRD2 — + Astroscale's ADRAS-J2, contracted at ~¥13.2B — targets a single + unprepared upper stage, presently planned toward FY2027–28. It validates + that agencies will pay for large-object removal, not that an installer + architecture is the cost-effective way to do it at scale. +- **Published passive-kit physics, without an installer-architecture + trade.** The bare-electrodynamic-tether literature (Sanmartín et al., + the EU FP7 BETs project) and the drag-sail deorbit literature are mature + and citable. Neither supplies an open, honesty-audited trade of the + installer architecture itself. + +(These external figures are publicly reported program numbers, not +ADSC-computed results — no committed CSV backs them, and they are outside +this package's own numeric-consistency scan by construction; they are cited +for context, not audited as evidence-pack claims.) + +**The niche**: an open, reproducible, honesty-audited trade for the +*installer* architecture specifically — attach-a-kit-and-depart, batch +amortization, geometry-keyed capture, a campaign-level Monte-Carlo safety +case — sitting between the funded single-target programs above and the +published component-level kit physics. That combination does not otherwise +exist in the open literature. + +## Safety case, stated the required way + +Approach safety is a **passively-safe approach design with +clearance-verified aborts (keep-out violations 0/500 per catalog [L0, +ds-v1, Wilson ≤ 0.0076])** — never "strict approach safety" or "safe +satellite." Re-verified at L1 (+J2) and L2 (+drag) with zero violations +over the re-verified abort events, per catalog (README **Safety status** / +[safety.md](safety.md)). A pre-WP11 legacy baseline showed a nonzero +violation rate, archived under R15 at `v0.10-phase0-baseline`; WP11 closed +it, and the guarantees remain model-scoped (R14 tags throughout) — a real +mission re-verifies every coast against a higher-fidelity propagator before +flight. Full re-verification table and campaign statistics: +[safety.md](safety.md). + +## Kit trade and target prioritization + +The kit trade is an honest negative-first result: a drag sail closes the +IADC 25-year guideline for the ~1.4 t class but does **not** close it for +the ~9 t class (impractical sail area at that mass); an electrodynamic +tether is carried as an honest **candidate with open risks** for the heavy +class, not a closed recommendation (libration trade T7 unresolved). The +WP14 target-prioritization table ranks the ~9 t class first on +figure-of-merit under both weightings evaluated, with the ~1.4 t class +second (closes today with a practical sail kit) and a separate +controlled-reentry mission class evaluated on its own terms, not +kit-ranked against the two above. Full trade, per-class recommended kit, +and the prioritization defense paragraphs: +[target_selection.md](target_selection.md) and +`generated/wp14_prioritization.md`. + +## Cost: cost-effectiveness, never "minimum cost" + +"Minimum cost" as an absolute claim is banned throughout this package +(spec §1). The defensible, and only claimed, framing is +**cost-effectiveness — cost per removal and cost per unit risk +reduction** — reported as relative cost units (CU, primary) plus cited +low/mid/high absolute-cost ranges (never a point-value currency figure, +D10). Headline numbers, all sourced from committed artifacts, not +re-derived here: README **Key results** and [cost_model.md](cost_model.md). + +## Reproduce in one command + +``` +git clone https://github.com/HeliCorgi/ADSC.git && cd ADSC && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build && bash tools/regenerate_all.sh build +``` + +A clean clone regenerates every committed number byte-for-byte +(`git diff --exit-code -- generated/ evidence/ README.md docs/` stays +clean). Requirements: C++17 + Eigen 3.3+, CMake, Python 3 stdlib only. + +## Contact + +This project has no dedicated inbox or gatekeeper by design (agency-agnostic, +per spec §1) — open an issue or discussion on the repository: +. diff --git a/docs/pitch_deck.md b/docs/pitch_deck.md new file mode 100644 index 0000000..f1deeb3 --- /dev/null +++ b/docs/pitch_deck.md @@ -0,0 +1,192 @@ + + +# ADSC +### Kessler-precursor removal — an installer-architecture evidence package + +Not a debris collector. Not flight software. Not a mission proposal. +TRL 4, GNC software element only. + +*Reproduce every number: `bash tools/regenerate_all.sh build` — byte-identical +from a clean clone.* + +--- + +## The problem + +The collisional cascade's fuel is the population of **massive intact +derelicts** in congested orbital bands — fragments are the symptom, not the +cause. One intact-intact collision (Iridium 33 / Cosmos 2251, 2009) produced +2,201 cataloged fragments (Liou 2014, NASA ODPO). Removing, or equipping for +removal, the objects that would become the *next* fragment cloud attacks the +source term. That is what **Kessler-precursor removal** names — full +citations in [concept.md](concept.md). + +--- + +## The installer concept + +Not a tug. A small servicer: + +1. Rendezvous with a massive derelict upper stage (class-level target, open + literature, no live ephemerides — D11). +2. Attaches a **passive** deorbit kit (drag sail or electrodynamic tether) + at a geometry-keyed capture feature present by construction (nozzle + throat / adapter ring, D4) — no generic manipulator. +3. Departs. The kit, not the servicer, carries the deorbit propellant cost + (drag / electrodynamic drag). +4. One mission services **several** targets in one plane. + +Deorbit Δv scales with the mass decelerated — a tug must decelerate the full +multi-tonne stage; the installer decelerates nothing but its own small kit +transfer. Mechanism detail: [gnc.md](gnc.md), +[technical_architecture.md](technical_architecture.md). + +--- + +## The argument that survives every placeholder + +Batch amortization across N targets per mission drives cost-per-removal down +until the mission **Δv budget**, not the kit count, caps removals — a +mass-ratio scaling argument, not a point estimate. It holds regardless of +which cost coefficient or target mass is still an open PLACEHOLDER. Measured +figure: README **Key results** / [cost_model.md](cost_model.md). + +This is the lead argument precisely *because* it is placeholder-independent +— everything downstream (absolute costs, FoM weightings, EDT performance) is +honestly still open, and is presented as such later in this deck. + +--- + +## How the evidence is built + +- C++17 GNC simulator: rigid-body dynamics, sliding-mode attitude control, + translation EKF + attitude MEKF, one code path across fidelity levels + (L0 CW → L1 +J2 → L2 +drag; no fork). +- Monte-Carlo campaign engine: N = 500 dispersed missions per catalog, fixed + master seed, Wilson 95% CIs — never bare point estimates. +- Every number in this deck traces to a committed, regenerable artifact: + `tools/regenerate_all.sh` reproduces `generated/` and `evidence/` + byte-for-byte; CI enforces it on every push. +- Claim discipline: every safety/performance claim carries a fidelity tag + (R14, L0…L6); the claims-audit test fails on an unaudited headline claim. + +--- + +## GNC evidence snapshot + +- Attitude sync demonstrated truth-driven **[L0]** and re-demonstrated + estimate-driven under sensor noise **[L0, estimator-in-the-loop]**, with an + NIS/NEES watchdog that rejects covariance-inflation fakes. +- Guided approach with reachability-screened aborts, retained as an + independent gate (not guidance alone) **[L0]**. +- Estimation gap reported honestly, not hidden: under an unestimated + range-bias walk, the filter is measurably *optimistic* about its own + accuracy **[L4]** — the identified fix (bias states) is not yet + implemented. +- Continuous-torque idealization caveat **retired by measurement**: sync + still completes under a minimum-impulse-bit actuator with command delay + and a single-axis fault **[L5]**. + +Full numbers and per-channel breakdown: [gnc.md](gnc.md). + +--- + +## The safety case + +**Passively-safe approach design with clearance-verified aborts +(keep-out violations 0/500 per catalog [L0, ds-v1, Wilson ≤ 0.0076])** — +never "strict approach safety." + +- History (R15): a pre-WP11 baseline showed a nonzero violation rate, + archived at `v0.10-phase0-baseline`; WP11's clearing-abort law closed it. +- Re-verified at **L1** (+J2) and **L2** (+drag): zero violations over every + re-verified abort event at both higher levels — a negative-negative + result, reported with the same discipline as a positive one. +- All guarantees are model-scoped. A real mission re-verifies every coast + against a higher-fidelity propagator before flight. + +Full table: [safety.md](safety.md). + +--- + +## Kit trade — the honest negative first + +- **~9 t reference class:** sail-only does **not** close the IADC 25-year + guideline (impractical sail area). An electrodynamic tether is carried as + an honest **candidate with open risks**, not a closed recommendation + (libration trade T7 open; plasma-density inputs PLACEHOLDER). +- **~1.4 t reference class:** sail-only **closes** at a practical area — + no tether needed. +- **A third, separate class** (defunct massive sun-synchronous payload) is + evaluated on controlled-reentry terms, not kit-ranked against the two + above — kit-only deorbit is explicitly not the recommended path for it. +- WP14 prioritization ranks the heavy class first on figure-of-merit under + both weightings evaluated, with a one-paragraph defense per class. + +Full trade: [target_selection.md](target_selection.md), +`generated/wp14_prioritization.md`. + +--- + +## Cost and figure of merit + +- Primary metric: **relative cost units (CU)**. "Minimum cost" as an + absolute claim is **banned** — the defensible claim is + **cost-effectiveness: cost per removal, cost per unit risk reduction.** +- Amortization curve bottoms at a specific kit count (Δv-limited, not + monotonic) — the honest capacity story. +- Figure of merit reported under **two independent congestion weightings** + that can disagree on band priority (open trade T5) — kept visible, not + resolved by fiat. +- WP14 absolute-cost ranges: cited **low/mid/high**, never a point value; + every itemized row sourced or explicitly PLACEHOLDER (D10). The external + currency anchor is explicitly framed as low-standing next to an agency's + own parametric model. + +Full model: [cost_model.md](cost_model.md). + +--- + +## What is NOT claimed + +- **Not** TRL 5+; system-level TRL is not claimed at all. WP9 + (processor-in-the-loop) is reserved and the only path above TRL 4. +- **Not** flight software, **not** a mission proposal, **not** legal advice. +- **Not** a solved EDT performance case (libration T7 open); **not** a + solved absolute-cost case (ranges only, D10). +- **Not** validated against a real environment: linear-CW safety statements + hold in the stated models (L0/L1/L2), not the real one. +- **140 PLACEHOLDER marks remain (56 decision-critical)** — inventoried + automatically, none hidden. [limitations.md](limitations.md). + +--- + +## Roadmap + +WP1–WP8 baseline → WP10 forensics → WP11 safety hardening (0/500 keep-out) → +WP12 fidelity ladder (L0–L2, L4/L5 elements) → WP13 kit trade + EDT physics → +WP14 cost ranges + FoM → **WP15, this package**. WP9 +(processor-in-the-loop, the only path to TRL 5) remains reserved, not +started. Detail: [roadmap.md](roadmap.md). + +--- + +## The ask + +This is offered as a **citable, reproducible reference** for the ADR +research and concept-study community first; adoption by an agency +mission-design process is the aspirational case, not a claimed outcome. + +- **Reproduce it yourself:** + `git clone https://github.com/HeliCorgi/ADSC.git && cd ADSC && cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build && bash tools/regenerate_all.sh build` +- **Read the honest scope first:** [limitations.md](limitations.md), evidence + pack §10 (PLACEHOLDER inventory). +- **Engage:** open an issue or discussion on + — no other channel by design. diff --git a/docs/release_engineering.md b/docs/release_engineering.md new file mode 100644 index 0000000..c5a50f8 --- /dev/null +++ b/docs/release_engineering.md @@ -0,0 +1,134 @@ +# Release engineering + +Scope: how ADSC is version-tagged, when a tag is cut, and the exact, +step-by-step procedure for an archived, DOI-carrying release (Zenodo) — +plus the explicit deferral note (spec:288-291) covering why that step has +not been executed yet. + +## Versioning scheme + +ADSC is **not** on strict SemVer with a stable public API — it is an +evidence package, pre-1.0, and a version number does not promise API +compatibility to anything. The scheme actually in use is: + +``` +v0.- +``` + +- `` tracks the work-package/phase the repository had just + completed at tag time — not a release cadence, and not applied to every + merged PR. A tag marks a **phase boundary**: either the point a phase's + headline numbers need archiving as an R15 "before" reference, or a point + a major deliverable completed. +- `` is a short human-readable label for what that phase + delivered (`phase0-baseline`, `fidelity`), not a restatement of the WP + number. +- Tags are lightweight/annotated git tags on the merge commit; `CMakeLists.txt`'s + own `project(ADSC VERSION 4.0 ...)` field is a separate, legacy + CMake-internal version number and is not currently kept in lockstep with + the git tag scheme (noted here as a known inconsistency, not fixed by + this PR — flag to the owner if it should be reconciled). + +## Precedents (both already in the repository) + +| tag | commit | what it marks | +|---|---|---| +| `v0.10-phase0-baseline` | `1f8c0aa` | End of Phase 0 (WP10: spec adoption, citation-fill, keep-out-violation forensics — no behavior-changing code). Cut **before** WP11 landed specifically so the pre-WP11 keep-out-violation rate (0.014 [0.007, 0.029]) had an archived, citable reference commit for the R15 BEFORE/AFTER table once WP11 closed it to 0/500 (spec §1). | +| `v0.12-fidelity` | `a0deb9d` (merge of PR #26, `fixpack-pre-wp13`) | End of the WP12 fidelity-ladder phase plus the pre-WP13 fix-pack, cut immediately before WP13 (kit-class trade + EDT physics) began — a clean boundary between "safety/fidelity" work and "kit/cost" work. | + +**Known gap, stated plainly:** WP13 (`4b02ac0`) and WP14 (`f41cb85`) merged +without their own phase tags — the tag-at-phase-boundary practice was not +applied mechanically to every WP in this chain. This is not fixed +retroactively by this PR (no git tags are created here); it is flagged so +the owner can decide whether to backfill `v0.13-kit-trade` / +`v0.14-cost-fom` tags on their existing merge commits, or accept the gap +and simply resume the practice from WP15 forward. + +## Practice going forward + +1. Tag at the merge commit of a PR that closes a WP or a WP group, when + either (a) a headline number in that PR needs to become an R15 "before" + reference for a later change, or (b) the PR completes a deliverable + worth pointing an external reader at directly (a released package + snapshot, not mid-refactor state). +2. Tag name: `v0.-`, matching the two + precedents above. For this PR, the natural next tag at WP15's merge + commit is **`v0.15-proposal-package`**. +3. Creating the tag and any GitHub Release is an owner/Fable5-authorized + action (this PR does not create it): `git tag -a v0.15-proposal-package + -m "" ` then `git push origin + v0.15-proposal-package`, authored with the repository's established + commit identity (`HeliCorgi <269209176+HeliCorgi@users.noreply.github.com>`). +4. A **git tag alone does not trigger Zenodo** (see below) — an actual + GitHub *Release* object must be published against that tag, e.g. + `gh release create v0.15-proposal-package --title "ADSC v0.15 — proposal + package" --notes-file ` (the `gh` CLI is available at + `C:\Program Files\GitHub CLI\gh.exe` on this machine as of 2026-07-05). + +## The Zenodo DOI procedure — exact steps + +Review improvement #11 (spec, WP15 task 5): earlier is better; the DOI +should not be gated on pitch-perfection. The steps below are the complete, +literal procedure; only step 1 has an external dependency this PR cannot +satisfy. + +1. **Owner logs into Zenodo** at , e.g. via "Log in + with GitHub" OAuth, so the Zenodo account is linked to the `HeliCorgi` + GitHub identity. **This is the blocking step** — see deferral note + below. +2. **Enable GitHub sync for this repository.** In Zenodo, go to + account **Settings → GitHub** (`https://zenodo.org/account/settings/github/`). + If `HeliCorgi/ADSC` is not listed, click **"Sync now"** to refresh the + repository list from GitHub (this requires the Zenodo GitHub + integration to have been granted access to the account's repositories). + Toggle the switch **ON** next to `HeliCorgi/ADSC`. This registers a + webhook on the repository that fires on new GitHub Releases; it does + **not** archive anything by itself yet. +3. **(Recommended, not yet done in this repo) Add citation metadata.** + Add a `CITATION.cff` file (and/or a `.zenodo.json`) at the repository + root with title, author/creator (`HeliCorgi`), license (MIT), and a + short description. Without this, Zenodo auto-generates a deposition + from bare GitHub metadata (repo name/description only), which is + functional but generic. This file does not exist in the repository + today — flagged here as a recommended follow-up, not created by this + PR (out of this PR's exact file list). +4. **Cut and publish a GitHub Release** against the tag to be archived + (e.g. `v0.15-proposal-package`) via the repository's **Releases** page, + or `gh release create v0.15-proposal-package --notes-file `. + Zenodo's DOI minting triggers on a published **Release**, not on a bare + tag push — a tag with no corresponding Release will never reach Zenodo. +5. **Zenodo mints the DOI automatically.** Within moments of the Release + being published, the registered webhook fires; Zenodo downloads a zip + snapshot of the repository at that tag, creates a new deposition, and + mints a version-specific DOI. A **concept DOI** (stable across all + versions of the archived repo) is also minted on the *first* archived + release and continues to resolve to the latest version thereafter. +6. **Owner reviews/completes the deposition metadata** on the Zenodo + record page (title, author list, keywords, related identifiers such as + the GitHub repository URL and the specification document) — the files + themselves become immutable once published, but metadata fields remain + editable. +7. **Add the DOI badge back to the repository.** Copy the Markdown badge + snippet Zenodo generates for the record (a `https://zenodo.org/badge/DOI/...svg` + image linking to the DOI) and add it near the top of `README.md` and + this file, pointing at the **concept DOI** (so the badge does not go + stale on the next tagged release). +8. **Repeat only steps 4–7 for every future archived release** — the + GitHub sync (steps 1–2) and citation metadata (step 3) are one-time + setup; every subsequent published Release automatically becomes a new + version under the same Zenodo concept DOI, preserving a citable version + history consistent with the tag-at-phase-boundary practice above. + +## Explicit deferral note + +**The DOI-carrying archived release is deferred, not skipped**, per +spec:288-291 ("archived, DOI-carrying release (e.g., Zenodo) considered +and documented") and WP15 task 5 (`04-wp15-instructions.md`): *"execute if +the owner provides/authorizes a Zenodo account, else commit +`docs/release_engineering.md` documenting the exact procedure and defer."* +No Zenodo account has been provided or authorized as of this PR. This +document is the "considered and documented" deliverable; steps 1–3 above +remain outstanding and gate every step after them. No git tag, GitHub +Release, or Zenodo deposition is created by this PR — creating them is an +owner + Fable5 action per the task's role split, not a mechanical-worker +action. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..c53bfd2 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,129 @@ +# Roadmap + +Not a mission proposal; TRL 4, GNC software element only — this page is the +full work-package history and the full "why this does not raise TRL" +rationale that the README only summarizes. Binding spec: +`adsc-specification-v5.md` (work packages, hard rules R1-R16, locked +decisions D1-D13). WP9 is a fixed point throughout this history: **the +processor-in-the-loop (PIL) / flight-software track remains RESERVED and not +started, and is the only path above element TRL 4.** Nothing in WP10-WP15 +changes that. + +## History: what changed v1.21 → v2.0 + +The v1.21 README named an SR-UKF and a sliding-mode DACS, but the source only +declared unused state and printed status lines — no estimator, no control +law, no dynamics loop. v2.0 replaced the prints with an actual closed-loop +simulation and stated plainly what is and isn't implemented; every release +since has kept that discipline (D8: implemented/not-implemented lists and +the TRL statement updated every WP; negative results are first-class +deliverables). + +## Work-package history + +- **WP1 — Relative orbital motion + passive safety** — done. Clohessy-Wiltshire + relative motion with an analytic STM + RK4 cross-check, drift-free safety + ellipse, passively-safe approach corridor. +- **F1/F2 — Capped-abort honesty + model-scope note** — done. Capped-impulse + aborts report their actual verified coast instead of implying safety when + the cap binds; passive-safety guarantees stated as model-scoped (linear CW: + circular orbit, no J2, no differential drag, small separations). +- **WP2 — Tumble synchronization** — done. Tracking sliding-mode control with + torque-free feedforward against a precessing target, plus a + feedforward-honesty test. +- **WP3 — Attach event + kit decay trades** — done. Contact-energy budget at + the gated closing speed; sail vs. EDT decay trade with an honest negative + result for the heavy class; mission reflowed into + approach→sync→attach→depart phases. +- **WP4 — Estimator + sensor models** — done. Translation EKF + multiplicative + attitude EKF; the control loop runs on estimates, not truth; NEES/NIS + consistency watchdog. +- **WP5 — Campaign Monte-Carlo** — done. Dispersions under a fixed master + seed; success/abort/keep-out rates with Wilson CIs; Δv/kit/removal + percentiles; stable machine-readable CSV (schema **1.1** — corrected here; + the CSV was schema 1.0 at WP5's original landing and was bumped to 1.1 by + WP11's additive columns, per `generated/wp5_campaign_schema.md`). +- **WP6 — Parametric cost model** — done. Relative-unit `C_campaign`; + amortization curve; FoM under ≥2 congestion weightings (open trade T5); + tornado sensitivity (schema **1.1** — corrected here for the same reason as + WP5, per `generated/wp6_cost_schema.md`). +- **WP7a — Visualization Pack** — done. Python 3 stdlib-only static SVG + figures + static report page from the committed CSVs; no external + dependencies. +- **WP8 — Compliance Matrix Generator / Regulatory Precheck** — done. + Versioned rulepacks; precheck + evidence matrix; explicitly not legal + advice, no legal-conformity determination. +- **WP7 — Evidence Pack** — done. `evidence/adsc_evidence_pack.md`, generated, + claim-audited, zero hand-written numbers — the project's actual product. +- **WP9 — Processor-in-the-loop / flight-software track** — **reserved, not + started.** The only path above element TRL 4; see spec §9 and the + discussion below. +- **WP10 — Phase 0: adoption, citations, forensics** — done. Spec v5 adopted; + citation-fill; keep-out-violation forensics + (`generated/wp10_violation_forensics.md`, the legacy-law archive superseded + by WP11). No behavior change. +- **WP11 — Safety hardening + closed-loop guidance** — done. Clearing-abort + law, reachability screen, guided approach; closed the pre-WP11 nonzero + keep-out-violation baseline to **0 of 500 per catalog** [L0, ds-v1, Wilson + 95% upper bound 0.0076] — the legacy pre-WP11 rate (0.014 [0.007, 0.029]) + is archived at the `v0.10-phase0-baseline` release per R15, not deleted. +- **WP12 — Fidelity ladder** — done. L0/L1/L2 (+drag, +J2) and L4/L5 + (estimate-driven guidance, MIB actuator) elements; zero keep-out violations + re-verified at L1/L2 [ds-v1/ds-v2] — see `generated/wp12_ladder.md` and + [safety.md](safety.md) for the numbers. +- **WP13 — Kit-class trade + EDT physics** — done. Aligned-dipole EDT-v1 band + model with mandatory η(i) = |cos i|..cos²i (derivation: + `wp13-edt-derivation.md`, citations: `wp13-literature.md`); per-class + recommended-kit table; class-C controlled-reentry comparison; libration T7 + explicitly **unresolved** — an EDT candidate with open risks, not a closed + recommendation. +- **WP14 — Cost ranges + FoM upgrade** — **done** (corrected here: the + pre-WP15 README stated "WP14–WP15 — not started", which was already stale + by the time of this rewrite). Absolute cost as low/mid/high ranges only, + itemized and every row sourced-or-PLACEHOLDER; tug-architecture comparison + restricted to the same target set; target-prioritization table generated + (`generated/wp14_prioritization.md`); decision-critical PLACEHOLDER count + tracked in the evidence-pack changelog. Relative CU results remain primary. +- **WP15 — Proposal package** — this release. README restructured to the + What/Why/Concept/Implemented/Not-implemented/Key results/Safety + status/Reproduce/Structure/Roadmap/Limitations/License shape; detail moved + to `docs/` (this tree); R16 enforced across `docs/` via an extended + claims-audit; one-page overview, 5-page technical summary, pitch-deck + source, target-prioritization pointer, and this limitations/roadmap pair; + release engineering (version tag, DOI-carrying archived release or its + documented deferral) tracked separately — see + [pitch_agencies.md](pitch_agencies.md) and [limitations.md](limitations.md). + +## Why none of this raises the TRL + +**TRL 4 — for the GNC software element only.** With WP4 the attitude-sync +GNC element (tracking controller + estimator + sensor models) runs closed +loop **on estimated states** in a laboratory/simulation environment, with +filter consistency verified (NEES/NIS) rather than assumed — that is the +element-level TRL 4 definition; the earlier "TRL 5–6" framing (v1.21) was not +supportable, and WP1–WP3 stayed at TRL 3–4 because control still consumed +true states at that point. Scope caveat, stated plainly: **TRL 4 applies to +the GNC software element, not the system** — system-level TRL is undefined +and not claimed anywhere in this package. + +Work package by work package, why the TRL does not move: + +- **WP5** adds robustness-under-dispersions evidence — this strengthens the + TRL-4 evidence base, it does not raise the TRL. +- **WP6** adds a relative-unit cost/FoM model on top of that evidence — an + economic-viability argument, not a GNC maturity change. +- **WP10** (forensics), **WP11** (safety hardening + closed-loop guidance), + and **WP12** (the fidelity ladder — higher-fidelity propagation is a wider + validated envelope, not a maturity change) all widen the validated + envelope **at** TRL 4; none of them raises it. +- **WP13** (kit-class trade + EDT physics) and **WP14** (cost ranges + FoM + upgrade) are, respectively, a target/kit-selection argument and an + economic argument — neither is a GNC maturity change. +- **WP15** (this package) is a packaging/communication exercise — it changes + how the evidence is presented, not what TRL the evidence supports. + +Reaching TRL 5 for the element requires real-time processor-in-the-loop +execution on representative hardware — the WP9 track, reserved, not started, +unchanged by v5 and by every WP since. This is stated once, here, as the +canonical version; the README's Safety status section references it rather +than restating the full rationale. diff --git a/docs/safety.md b/docs/safety.md new file mode 100644 index 0000000..515de52 --- /dev/null +++ b/docs/safety.md @@ -0,0 +1,286 @@ +# Safety case + +This is the full detail behind the README's **Safety status** line. It covers +passive-safety-by-design (D5), the keep-out discipline (D13), the F1/F2 +honesty follow-ups, the WP11 clearing-abort law and guided approach, the WP12 +fidelity-ladder re-verification, and the full WP5 campaign statistics. The +underlying control-law and estimator math is [docs/gnc.md](gnc.md); the +one-code-path fidelity-ladder architecture is +[docs/technical_architecture.md](technical_architecture.md); everything this +case does **not** cover is [docs/limitations.md](limitations.md). + +Every number below regenerates from `generated/reference_metrics.csv`, the +WP5 campaign CSVs, or `generated/wp12_ladder.{csv,md}` — see +[evidence/adsc_evidence_pack.md](../evidence/adsc_evidence_pack.md) section 3 +for the same figures with the same citations (R16: this file and the evidence +pack are not allowed to drift apart). + +## Design principle: passively-safe trajectories (D5) and the keep-out discipline (D13) + +- **D5 — Passively-safe relative trajectories.** From any nominal-approach + point, a thrust-off coast must not enter the keep-out sphere. Autonomous + abort is available in every phase. +- **D13 — Keep-out violation is a design-unacceptable failure condition**, + not merely a reported statistic. The known primary mechanism (capped-abort + residual drift re-entering the keep-out sphere under dispersed initial + conditions) was a hypothesis confirmed by WP10 forensics and eliminated by + WP11 design, not assumed away. + +The approach-safety wording is fixed and must not be paraphrased upward or +downward: approach safety is a **passively-safe approach design with +clearance-verified aborts (keep-out violations 0/500 per catalog [L0, ds-v1, +Wilson ≤ 0.0076])** — never "strict approach safety" or "safe satellite". The +history clause: the pre-WP11 baseline showed a nonzero keep-out-violation +rate (0.014 [0.007, 0.029], archived at the `v0.10-phase0-baseline` release); +WP11 closed it to 0/500 (below); WP12 re-verified that result at L1 (+J2) and +L2 (+drag) (below). Any further upgrade of this wording is itself an +R15-documented change. + +**Passive corridor result (D5/WP1) [L0: linear CW]:** across **120 +thrust-off coasts** sampled along the approach corridor and propagated 2 +orbital periods, the worst closest approach is **424.264069 m** (≈424.3 m), +comfortably outside the **200 m** keep-out sphere +(`reference_metrics.csv: wp1_worst_coast_min_range_m, wp1_keep_out_radius_m`). +This holds in the linear CW model only — see the F2 caveat below and +[docs/limitations.md](limitations.md) for the full model-scope discussion. + +## F1: capped-abort honesty + +`compute_safe_abort` returns a Clohessy-Wiltshire impulse toward a drift-free +relative orbit (a bounded safety ellipse) through the current position, +capped at the thruster budget. The result carries a `Clean`/`Capped` status +plus the **verified** minimum range of the propagated post-burn coast — when +the cap binds, the bounded-orbit guarantee is lost and the code reports the +actual coast range instead of implying safety: + +- **Clean case** (contact-range abort): impulse **0.245016 m/s**, full + drift-null delivered, verified post-burn coast minimum **0.119712 m**. +- **Capped case** (large along-track drift): impulse saturates at the + **2.0 m/s** thruster budget, the bounded-orbit property is lost, and the + verified post-burn coast minimum is **3565.717926 m** — reported plainly, + not implied to be safe. + +(`reference_metrics.csv: f1_clean_abort_dv_m_s, f1_clean_coast_min_m, +f1_capped_abort_dv_m_s, f1_capped_coast_min_m`.) + +## F2: model-scope caveat (linear CW) + +Every passive-safety number in this file is **exact only in the linearized +Clohessy-Wiltshire model**: circular target orbit, no J2, no differential +drag, small separations. J2 and differential drag erode drift-free safety +ellipses over time, and the CW linearization error grows with separation — +so a real mission would re-verify every coast against a higher-fidelity +propagator before flight. The WP12 fidelity ladder (below) is the first +concrete step in that direction, not a substitute for it. Full discussion: +[docs/limitations.md](limitations.md). + +## WP11: clearing-abort law + guided approach + +`compute_clearing_abort` / `clearing_abort_for` implement a 3-stage +escalation ladder that accepts an abort only when the analytic post-burn +ellipse (`bounded_coast_min_range`, closed form) clears keep-out plus a +design margin: **drift-null baseline** → **bounded radial reshape** → +**two-impulse retreat hop**, with the legacy `Capped` honesty (F1, above) +preserved past the Δv budget. The campaign keep-out screen runs this law +(D13). + +**Campaign-level result:** keep-out violations **0 of 500** per catalog +**[L0: linear CW, dispersion set ds-v1, Wilson 95% upper bound 0.0076]** — +the full campaign table is below. The independent replay audit is +`generated/wp11_abort_audit.md`; the archived pre-WP11 forensics (the +legacy-law violation mechanism) are `generated/wp10_violation_forensics.md`. + +**Guided-approach demo (L0, truth-fed, deterministic)** — far approach → hold +→ sync → final approach → contact → retreat, with a reachability screen +evaluated before every committed impulse: + +| metric | value | +|---|---:| +| reachability screen held at every step | 1/1 (yes) | +| LOS cone held throughout final approach | 1/1 (yes) | +| contact speed (produced by glideslope-with-floor, not merely gated) | 0.100 m/s | +| total demo Δv | 2.533611 m/s | +| retreat Δv (burn1+burn2, +station-keep hop if needed) | 0.403983 m/s | +| min clearance outside the keep-out sphere (pre-authorization flight) | 200.0 m | + +(`reference_metrics.csv: wp11_guided_*, wp11_abort_feasible_every_step, +wp11_los_cone_ok, wp11_retreat_dv_total_m_s`; mode machine and per-mode abort +conditions: `generated/wp11_guidance_modes.md`.) This closes the +long-standing "gate, not guidance" limit at L0 — the 0.15 m/s `max_v_rel` +gate is retained as an independent check, not removed. + +**Forensic-14 story.** The 14 WP10c/WP11 pinned violation states (R15) are a +regression fixture, not an anecdote: the legacy (pre-WP11) law reproduces all +14 of them analytically as keep-out violations; the WP11 clearing law clears +every one of the same 14 states (`tests/test_forensic14.cpp`). The WP12 +fidelity ladder (below) re-checks the same 14 states at L1/L2. + +## WP12: fidelity ladder re-verification + +Runtime-selectable **L0/L1/L2** propagation on **one code path** (no fork, +R1): **L0** is the original WP1 Clohessy-Wiltshire linearization (every +committed WP5 number stays byte-identical); **L1** differences a full +inertial two-body+J2 RK4 propagation of both craft into the target's +instantaneous LVLH frame; **L2** adds a per-craft free-molecular drag term +under an **independent** ballistic-coefficient dispersion stream (`ds-v2`; +the committed campaign's own `ds-v1` stream is untouched). Cross-validation +(tested, `tests/test_ladder.cpp`): with J2 forced off, L1 reproduces the CW +closed form within a stated **2.0 m** linearization bound (the CW model's own +error, measured at worst 0.52 m across the forensic-14 states); with drag +forced off, L2 reproduces L1 bit-for-bit. L1/L2 numbers are **new claims** +(R14-tagged) that **re-verify** the existing WP11 clearing-abort law under a +higher-fidelity coast — they are not a new abort law. + + +Abort-event re-verification (292 catalog-A + 291 catalog-B closing-speed-gate +events from the committed campaign; master seed `0x5AD5C0DECAFE2026`, same +draws as `wp5_campaign_runs.csv` for `ds-v1`): + +| level | dispersion set | catalog | n_events | n_violations | violation_rate | wilson 95% CI | clearance floor (m) | p05 (m) | p50 (m) | +|---|---|---|---:|---:|---:|---|---:|---:|---:| +| L0 | ds-v1 | SL-16 / Zenit-2 second stage | 292 | 0 | 0.000000 | [0.000000, 0.012985] | 20.0053 | 36.3253 | 139.7579 | +| L1 | ds-v1 | SL-16 / Zenit-2 second stage | 292 | 0 | 0.000000 | [0.000000, 0.012985] | 19.0070 | 35.8083 | 139.2909 | +| L2 | ds-v2 | SL-16 / Zenit-2 second stage | 292 | 0 | 0.000000 | [0.000000, 0.012985] | 19.0086 | 35.7539 | 139.2424 | +| L0 | ds-v1 | SL-8 / Kosmos-3M second stage | 291 | 0 | 0.000000 | [0.000000, 0.013029] | 20.0002 | 26.5835 | 143.9558 | +| L1 | ds-v1 | SL-8 / Kosmos-3M second stage | 291 | 0 | 0.000000 | [0.000000, 0.013029] | 18.8096 | 26.2484 | 143.0785 | +| L2 | ds-v2 | SL-8 / Kosmos-3M second stage | 291 | 0 | 0.000000 | [0.000000, 0.013029] | 18.8129 | 26.0505 | 142.9937 | + +Forensic-14 per-level clearance: all 14 of 14 pinned cases clear keep-out at +**L0, L1 and L2** — the anticipated CW-safe-but-higher-level-unsafe case does +**not** materialize for this dispersion set: the WP11 clearance margin +absorbs the measured ~1.0-1.2 m of J2+drag coast erosion (clearance floor +20.0 m at L0 → 18.8 m at L2, SL-8 class, the tighter of the two). A +negative-negative result, reported with the same discipline as a positive +one. Full 14-row per-case table: `generated/wp12_ladder.md`. + +Safety-ellipse margin decay under J2 (the F2 caveat, promised qualitatively +since WP1, now measured): worst 5-orbit min-range erosion **1.40 m** on the +400 m standoff ellipse [L1: two-body+J2]. Full per-orbit table for both +geometries and both catalogs: `generated/wp12_ladder.md`. + + +**L4 (estimate-driven guidance) and L5 (actuator realization), honestly +reported:** + +- **L4** [L4: L0 dynamics + dropout + bias walk, deterministic seed]: the + guided approach flies on the translation EKF's estimate (truth is + error-recording only) under measurement dropout and an **unestimated** + range-bias random walk. Truth-evaluated contact speed **0.099759 m/s** + (gate 0.15 m/s), final position error **0.023540 m**. Consistency, + reported honestly: NIS **3.994325** (ideal ~4, consistent) but NEES + **310.437825** against an ideal ~6 — the filter is *optimistic* about its + own accuracy under the unestimated bias walk; adding bias states is the + identified fix, not implemented here (see + [docs/limitations.md](limitations.md)). +- **L5** [L5: MIB delta-sigma, 1-step delay, single-axis fault]: tumble sync + re-demonstrated under a delta-sigma minimum-impulse-bit actuator (MIB + 2×10⁻⁴ N·m·s PLACEHOLDER, 1-step command delay, one axis at 50% + authority): sync at **16.28 s** vs **16.87 s** under the continuous-torque + idealization — the earlier crossing is a metric artifact of the + first-crossing-then-dwell criterion under quantization, not a physical + speed-up; the claim is only that sync still completes under MIB + delay + + fault. Guidance contact velocity, quantized at a 1×10⁻³ m/s translation MIB + PLACEHOLDER, still meets the 0.10 m/s gate. + +(`reference_metrics.csv: wp12_est_*, wp12_l5_*`.) Coverage statement (from +`generated/wp12_ladder.md`): this ladder verifies keep-out-abort geometry +re-verification, forensic-14 regression clearance, and min-range erosion — +each tagged by the fidelity level that produced it. It does **not** verify +attitude dynamics beyond WP2's translation-only extension, or the full +closed-loop combination of L4 guidance with L5 actuation. No result here is a +legal, regulatory, or safety conclusion beyond its stated model scope. + +## WP5 campaign: the full statistical evidence + +The campaign is the safety/statistics story: 500 dispersed missions per +catalog at fixed master seed `0x5AD5C0DECAFE2026`, 6 targets/mission, 4 kits, +140 m/s Δv budget. Outputs are `generated/wp5_campaign_*.csv`, **schema +1.1** (`generated/wp5_campaign_schema.md`) — the schema bumped from 1.0 to +1.1 when WP11 added the `dispersion_set_id`/`worst_abort_clearance_m` +columns; this file states the current version rather than carrying the +stale "1.0" reference forward (a drift the WP15 content-mapping pass found +and fixed, per R16's whole point). Unit-tested +(`tests/test_campaign.cpp`: seed determinism, Wilson edge cases, +percentiles, termination, schema stability, the guardrail that no artifact +emits legal-approval language). Regenerate with `./build/adsc_campaign`. + + +The full N = 500 campaign (both catalog presets) runs in a few tens of seconds +on a CI runner (see the Actions log of the current run for the actual figure). +`success` here means a **productive end** — the mission installed its +full 4-kit complement (`kit_exhausted`) or cleared all 6 targets (`completed`) — +not one cut short by Δv exhaustion or a keep-out violation. + +| metric | SL-16 / Zenit-2 class | SL-8 / Kosmos-3M class | +|---|---|---| +| success rate | **0.556** [0.512, 0.599] | **0.542** [0.498, 0.585] | +| nonproductive-termination rate (= 1 − success) | **0.444** [0.401, 0.488] | **0.458** [0.415, 0.502] | +| gate-abort-run rate (abort-path exposure) | **0.444** [0.401, 0.488] | **0.458** [0.415, 0.502] | +| keep-out-violation rate | **0.000** [0.000, 0.008] [L0, ds-v1] | **0.000** [0.000, 0.008] [L0, ds-v1] | +| Δv used p05/p50/p95 [m/s] | 124 / 124 / 136 | 124 / 124 / 136 | +| kits used p05/p50/p95 | 3 / 4 / 4 | 3 / 4 / 4 | +| removals/mission p05/p50/p95 | 3 / 4 / 4 | 3 / 4 / 4 | +| sync arrival p05/p50/p95 [s] | 14.47 / 17.68 / 20.09 | 14.13 / 17.72 / 20.16 | +| failure counts (runs) | dv_exhausted 222, kit_exhausted 278, keep_out 0, completed 0 | dv_exhausted 229, kit_exhausted 271, keep_out 0, completed 0 | +| per-target events | gate_abort 292, sync_timeout 0 | gate_abort 291, sync_timeout 0 | + +Two abort-related rates are reported and are deliberately distinct: +**`gate-abort-run rate`** is the abort-path exposure (fraction of runs with ≥ 1 +closing-speed gate abort — what the spec calls the "abort rate"), while +**`nonproductive-termination rate`** is 1 − success. Under the current *flat +PLACEHOLDER* Δv cost the two coincide numerically — every aborting mission needs +an extra target-slot to still install its kits and so exhausts the 140 m/s +budget — but they are separate concepts and will diverge once the cost model +gains structure. The honest campaign finding: the servicer is **Δv-limited about +44% of the time** (`dv_exhausted` = 222/500) and installs its full kit +complement the rest; the attitude sync **never** times out across the sampled +tumble/attitude/actuator dispersions, and keep-out violations are **0 of 500** +per catalog [L0: linear CW, dispersion set ds-v1, Wilson 95% upper bound +0.0076] — the WP11 clearing-abort law accepts an abort only when the analytic +post-burn ellipse clears keep-out plus a design margin (mechanism forensics: +`generated/wp10_violation_forensics.md`; audit: +`generated/wp11_abort_audit.md`). `completed` (all 6 targets) is 0 by +construction — 4 kits cannot service 6 targets. The `Δv used` / `kits used` +percentiles matching across the two presets is expected (a flat PLACEHOLDER cost +takes quantized, catalog-independent values — not a copy-paste bug). Full per-run +records and the column schema are in [generated/](../generated/). + + +WP5 produces **no** charts and performs **no** legal/regulatory +determination; visualization is [docs/technical_architecture.md](technical_architecture.md)'s +WP7a pack, and the regulatory precheck is +[docs/legal_regulatory.md](legal_regulatory.md). + +## Contact-energy budget (D4/WP3, open trade T8) + +Clamping at the gated closing speed (**0.15 m/s**, `max_v_rel`) with the +**29.6 kg** dry+kit servicer mass carries **0.333 J** of kinetic energy +(`reference_metrics.csv: wp3_contact_energy_j, wp3_contact_speed_m_s, +wp3_contact_mass_kg`). The budget exists to show the geometry-keyed clamp +(D4) does not depend on aggressive grappling of a decades-degraded surface — +shedding MLI or paint would manufacture the very debris the mission removes. +**Whether this energy is below the actual MLI/paint damage threshold is not +claimed here** — open trade **T8**: the 0.333 J figure is reported without +claiming it is below any validated threshold until a citable source exists +(`[CITATION NEEDED — PLACEHOLDER: low-speed-contact damage-threshold source +for aged MLI/paint]`, evidence pack section 3). + +A worst-case figure exists for the residual-propellant condition (heavier +servicer at the same 0.15 m/s gate speed); it is **not** backed by a +committed CSV or generator today, so it is documented as an honest +scope-gap rather than restated here — see +[docs/limitations.md](limitations.md) for the number and its arithmetic +(R16: one home per number). + +## What this safety case does not claim + +- No claim beyond **L0/L1/L2** model scope for passive-safety numbers, and no + claim beyond **L4/L5** scope for guidance/actuation honesty numbers — see + the L-tags above and [docs/limitations.md](limitations.md) for the full + model-scoping discussion. +- No system-level or flight-readiness claim; TRL stays 4, element-scoped + (spec section 9; [docs/limitations.md](limitations.md)). +- No claim that the contact-energy budget is below any validated + material-damage threshold (T8, above). +- No legal or regulatory determination — see + [docs/legal_regulatory.md](legal_regulatory.md). diff --git a/docs/target_selection.md b/docs/target_selection.md new file mode 100644 index 0000000..6975bab --- /dev/null +++ b/docs/target_selection.md @@ -0,0 +1,289 @@ +# Target selection: catalogs, kit-class trade, and prioritization + +This is the full detail behind the README's kit-class-trade line. It covers +the three class-level target catalogs (A/B/C) and their citations, the WP13 +sail/EDT kit-class trade with the EDT-v1 model scope and the T7 open-risk +honesty, the Class-C controlled-reentry comparison, and a pointer to the WP14 +target-prioritization table. Cost figures referenced here are owned by +[docs/cost_model.md](cost_model.md); safety/campaign figures are owned by +[docs/safety.md](safety.md); regulatory-consent mechanics are +[docs/legal_regulatory.md](legal_regulatory.md). + +Targets remain **class-level parameters** throughout (D2, D11): no +mission-planning product is built against a specific catalog ID, and no live +ephemeris/TLE is ingested anywhere in this repository. + +## Catalogs A/B/C + +| catalog | class | mass | altitude | inclination | role | +|---|---|---:|---:|---:|---| +| A — SL-16 / Zenit-2 second stage | massive rocket body | ≈9,000 kg (8,300–9,000 kg, source spread) | ≈840–857 km | 71.0° | sail/EDT kit-trade anchor; also McKnight top-of-ranking | +| B — SL-8 / Kosmos-3M second stage | medium rocket body | ≈1,400 kg (1,435 kg empty) | ≈700–1,000 km (three-box cluster) | 74°/83° | sail closes; high per-class population | +| C — Envisat-class massive SSO payload | massive defunct payload | 8,211 kg | ≈765–800 km, sun-synchronous | 98.4–98.55° | separate controlled-reentry mission class, not kit-ranked | + +Citations (`_tasks_local/wp13-literature.md`, web-verified 2026-07-11): + +- **Catalog A (Zenit-2 / SL-16):** empty mass ≈8,300 kg (one source lists + ≈9,000 kg — carry both, the 8,300 kg figure is the more common one), length + 11.50 m, diameter 3.90 m, 1× RD-120 engine. Typical Tselina-2 payload orbit + ≈845×857 km, inclination 71.01° (also a 65° family; the wider SL-16 + rocket-body cluster spans ≈750–850 km, inclinations including 71° and 81°). + Sources: braeunig.us/space/specs/zenit.htm, astronautix.com/z/zenit-2.html, + Wikipedia "Zenit-2". Cross-check: McKnight et al. 2021 (below) — the top 20 + most-concerning LEO objects are all SL-16 second stages, ≈9 t class, + ≈850 km. +- **Catalog B (Kosmos-3M / SL-8):** empty mass ≈1,435 kg, gross mass + ≈20,135 kg, 1× 11D49 engine, length ≈6 m, diameter 2.4 m. Cluster survey + (as of 2014): Box 3 ≈700–800 km (50 rocket bodies), Box 2 ≈900–1,000 km + (143 rocket bodies), Box 1 ≈1,500–1,600 km (44 rocket bodies), inclinations + ≈74° and ≈83° — an order of magnitude larger combined population than the + SL-16 top-20 concern list. Sources: astronautix.com/k/kosmos11k65m.html, + space.skyrocket.de/doc_lau_det/kosmos-3m.htm. Appears in SL-8-specific ADR + mission studies (e.g. ADReS-A, Springer 10.1007/978-3-319-15982-9_3). +- **Catalog C (Envisat-class):** mass 8,211 kg (Service Module 2,673 + PEB + 1,021 + Payload Carrier 2,078 + Fuel 319 + Instruments 2,118 kg), + sun-synchronous orbit ≈765–800 km, inclination ≈98.4–98.55°, defunct since + April 2012 — the archetypal "too massive / high-casualty-risk-on- + uncontrolled-reentry" object cited across the debris-mitigation literature. + Sources: eoPortal (eoportal.org/satellite-missions/envisat), ESA Earth + Online, Wikipedia "Envisat". + +**McKnight basis (criticality anchor):** McKnight, D., Witner, R., Letizia, +F., Lemmens, S., Anselmo, L., Pardini, C., Rossi, A., Kunstadter, C., et al. +(2021), "Identifying the 50 statistically-most-concerning derelict objects in +LEO," *Acta Astronautica* 181, 282-291, DOI: 10.1016/j.actaastro.2021.01.021. +Eleven expert teams' consensus top-50 most-concerning LEO objects is +dominated by rocket bodies; the top ≈20 are **all** SL-16 (Zenit-2) second +stages (average mass of the top-50 objects ≈5,295 kg); removing the top-50 +would roughly halve LEO collision risk (top-10 ≈30% reduction). This is the +cited anchor for catalog A's criticality weight in the WP6/WP14 FoM — see +[docs/cost_model.md](cost_model.md). The rest of the criticality weighting +table (the non-peak altitude/weight pairs) remains an uncited interpolation +(**open trade T5**, [docs/cost_model.md](cost_model.md)). + +## Sail-decay trade (WP3) + +A quasi-circular drag-decay integrator over a Vallado exponential atmosphere +(Vallado, *Fundamentals of Astrodynamics and Applications*, 4th ed., Table +8-4; cross-checked against its constant-density closed form) produces the +sail-area × altitude × solar-activity → decay-years trade, always as a +solar-min..max **range** (open trade T4 — solar-activity dispersion, never a +point value). + + +**Catalog A — SL-16 / Zenit-2, ≈9,000 kg, 840 km:** + +| sail area [m²] | decay time, solar max [yr] | decay time, solar min [yr] | +|---:|---:|---:| +| 5 | 673.5 | 10775.2 | +| 10 | 336.7 | 5387.6 | +| 25 | 134.7 | 2155.0 | +| 50 | 67.3 | 1077.5 | +| 100 | 33.7 | 538.8 | +| 200 | 16.8 | 269.4 | +| 400 | 8.4 | 134.7 | +| 800 | 4.2 | 67.3 | +| 1000 | 3.4 | 53.9 | +| 1600 | 2.1 | 33.7 | +| 3200 | 1.1 | 16.8 | + +Sail area for the 25-year guideline (solar max..min): **135..2155 m²** — +impractical at the upper end. + +**Catalog B — SL-8 / Kosmos-3M, ≈1,400 kg, 750 km:** + +| sail area [m²] | decay time, solar max [yr] | decay time, solar min [yr] | +|---:|---:|---:| +| 5 | 36.0 | 576.4 | +| 10 | 18.0 | 288.2 | +| 25 | 7.2 | 115.3 | +| 50 | 3.6 | 57.6 | +| 100 | 1.8 | 28.8 | +| 200 | 0.9 | 14.4 | +| 400 | 0.5 | 7.2 | +| 800 | 0.2 | 3.6 | +| 1000 | 0.2 | 2.9 | +| 1600 | 0.1 | 1.8 | +| 3200 | 0.1 | 0.9 | + +Sail area for the 25-year guideline (solar max..min): **7..115 m²** — tens of +square meters, a practical kit. + +25-year figure is the **IADC guideline** (IADC-02-01, Space Debris Mitigation +Guidelines, guideline 5.3.2). Under a US FCC license the current rule (FCC +22-74, adopted 2022) requires **5-year** post-mission disposal for LEO space +stations instead — the class-A sail-only negative gets strictly harder under +that standard. Both rules are carried separately in the WP8 compliance +rulepacks (see [docs/legal_regulatory.md](legal_regulatory.md)); this trade +keeps the IADC 25-year reference line. + + +Sail-only closes for the lighter/lower catalog B (tens of m²) but **not** for +the heavier, higher catalog A (hundreds-to-thousands of m² — impractical): +the honest negative that brackets **open trade T1** (sail vs EDT crossover +altitude) and motivates the WP13 electrodynamic-tether branch below. + +## Kit-class trade + EDT physics (WP13) + +Data source: `generated/wp13_kit_trade.csv` (schema 1.0, column definitions +in `generated/wp13_kit_trade_schema.md`), combining the existing WP3 +sail-decay model with the WP13 EDT-v1 aligned-dipole physics core +(`src/decay.cpp: edt_deorbit_years`) — no hand-written numbers. The sail-area +25-year figures below are the **same numbers** as the WP3 sweep above (both +CSVs derive from the same `area_for_target_years` model); this file cites +`wp13_kit_trade.csv` as the canonical per-class source to avoid an ambiguous +"which CSV" question (per the WP15 content map). + + +| catalog | recommended kit | sail area, 25-yr [m²] (max..min) | EDT deorbit time [yr] | EDT η(i) band | +|---|---|---:|---:|---:| +| A — SL-16 / Zenit-2 second stage | edt-candidate-open-risks | 135..2155 | 3.0..9.2 | 0.106..0.326 | +| B — SL-8 / Kosmos-3M second stage | sail | 7..115 | 0.6..3.0 | 0.043..0.208 | +| C — Envisat-class massive SSO payload | controlled-reentry-mission-class | 61..983 | 5.5..37.5 | 0.021..0.146 | + +Kit mass, deploy risk, and diagnostic EMF/power (same source CSV): + +| catalog | sail kit mass [kg] | EDT kit mass [kg] (PLACEHOLDER) | EDT deploy-failure risk (PLACEHOLDER) | EDT open-circuit EMF [V] | +|---|---:|---:|---:|---:| +| A | 2.4 | 20.0 | 0.05 | 150.3 | +| B | 2.4 | 20.0 | 0.05 | 100.3 | +| C | 2.4 | 20.0 | 0.05 | 69.4 | + +Deploy risk is quoted for the record and is **not** folded into the +EDT-years band above. + + +**EDT-v1 model scope (R14 fidelity tag), quoted verbatim from the committed +CSV — never hand-transcribed, so this document and the data cannot drift +apart:** + +> [model: EDT-v1 aligned-dipole, capped-current; eta band |cos i|..cos^2 i; libration T7 OPEN - PLACEHOLDER duty factor] + +This states the model scope in-line with the number it qualifies (R14): +aligned (untilted) dipole geomagnetic field (SPENVIS centred dipole, IGRF +epoch 2000); force capped by either a fixed/power-limited current +(optimistic edge) or an EMF/collection-limited current (conservative edge); +the along-track efficiency factor η(i) is mandatory and never a point value +(spec WP13) — at catalog A's 71° inclination the efficiency loss is +first-order, and omitting it would overstate EDT performance. + +**T7-open honesty (never claim closed).** Libration / dynamic tether +stability is explicitly **unresolved** (Peláez, J., et al., 2000, cited in +`_tasks_local/wp13-literature.md`); the EDT-years band above folds it in only +as a flat PLACEHOLDER duty-cycle knob (`EdtConfig::eta_libration`), never +claimed solved. Plasma electron density is a cited PLACEHOLDER solar-min/max +pair, not an IRI implementation (spec non-goal). Bottom line, quoted verbatim +from `generated/wp13_kit_trade.csv`'s own recommended-kit rationale for +catalog A: "sail infeasible (135..2155 m2 for the 25-yr guideline, solar +max..min); EDT (band +3.0..9.2 yr) subject to T7; libration (T7, Pelaez et al. 2000) and plasma Ne +(PLACEHOLDER solar-min/max pair) remain open — an honest EDT candidate, not a +closed recommendation." + +## Class C: controlled reentry (a separate mission class) + +Class C (spec WP13) is **not** a sail/EDT kit choice: massive/high-risk +derelicts where the deliverable is a controlled reentry, evaluated on +casualty risk, ground footprint, Δv, consent, and cost. Comparison against +tug architectures is allowed **here only**, and only on the **same target +set** (D1, spec WP13/WP14 rule — comparing different targets is meaningless +and banned). Data source: `generated/wp13_classC.csv` (schema 1.0), +regenerated by `kit_trade` — no hand-written numbers. This is **open trade +T2** (controlled vs uncontrolled reentry, driven by demisability). + +**Uncontrolled-reentry casualty framing (cited, not computed):** +NASA-STD-8719.14A (with Change 1), Requirement 4.7-1: the expected +worldwide human-casualty risk from reentering debris shall not exceed +**Ec < 1×10⁻⁴** (1 in 10,000); casualty is assumed for any surviving fragment +with impact kinetic energy **> 15 J**; a controlled reentry must additionally +ensure no surviving >15 J fragment lands within **370 km** of foreign +landmasses (soma.larc.nasa.gov/SIMPLEx/pdf_files/871914.pdf; ESA applies the +same 1×10⁻⁴ limit via DRAMA/SARA, technology.esa.int/page/re-entry-safety). +This repository does **not** compute a per-object Ec for either candidate — +that needs a survivability/ground-footprint tool (NASA DAS/ORSAT or ESA SARA +class), out of scope here (`[CITATION NEEDED — PLACEHOLDER: per-object Ec +analysis]`). Both candidates are multi-tonne, so an uncontrolled reentry is +the presumptive risk driver the 370 km / Ec<1e-4 rule exists for — that +presumption is not itself a computed Ec value. + + +Controlled-deorbit Δv (COMPUTED): a single impulsive perigee-lowering burn +from the catalog's circular orbit (radius r_a) to a target perigee r_p = +6371+40 km (mean-Earth-radius convention), dv = v_c·(1 − √(2·r_p/(r_a+r_p))), +v_c = √(μ/r_a). This is the minimum single-burn Δv to commit the stage to +reentry; it excludes targeting/footprint-control burns and any margin. + +| candidate | v_c [m/s] | Δv [m/s] | +|---|---:|---:| +| Zenit-2 / SL-16 (catalog A) | 7431.2 | **223.4** | +| Envisat-class (catalog C) | 7462.2 | **208.3** | + + +**Consent gate (D12).** Legal accessibility enters as a **gate and metadata +flags, never a multiplier**: the WP8 compliance engine BLOCKs unconsented +active debris removal, and target prioritization consumes its PASS/BLOCK +output and flags rather than folding a subjective legal weight into any +score. Any controlled-reentry mission on either candidate above is subject +to that same gate, regardless of the casualty-risk or Δv numbers above. See +[docs/legal_regulatory.md](legal_regulatory.md). + +**Kit-installer vs tug, same target set.** A tug architecture also amortizes +fixed costs across a batch of targets in one plane, exactly like the +kit-installer's own batch amortization ([docs/cost_model.md](cost_model.md)). +What does *not* amortize for a tug, and is the installer's structural edge +(D1: installer-not-tug): (1) a tug must carry propellant to change **each** +target's orbit itself, so its per-target Δv scales with target count, while +an installer's kit-carrying stages deorbit themselves after release; (2) a +tug must detumble, dock/grapple, and physically tow each multi-tonne stage +through its own controlled-reentry burn, repeating the highest-risk contact +event per target, while the installer's own contact event is bounded by the +WP3 low-energy capture approach (0.333 J budget at 0.15 m/s, from +`generated/reference_metrics.csv`, owned by [docs/safety.md](safety.md)) and +does not itself carry a target through +reentry. Both architectures still need some per-target consent/compliance +gate and some reentry Δv paid by someone — the installer's edge is *where* +that Δv and that repeated high-risk contact event are paid, not that they +vanish. + +**Cost:** catalog A has itemized low/mid/high ranges via the WP14 cost model +([docs/cost_model.md](cost_model.md)); catalog C's controlled-reentry mission +is not separately itemized (the WP14 ranges are servicer-campaign-scale) — +an open item, stated rather than silently assumed. + +## Target prioritization (WP14) + +The generated, joined ranking over cost + FoM + kit trade — no new +simulation, no new statistics, every number read directly from +`generated/wp6_cost_summary.csv`, `generated/wp13_kit_trade.csv`, and +`generated/wp5_campaign_summary.csv` — is +[generated/wp14_prioritization.md](../generated/wp14_prioritization.md). +Summary of its ranking: + +1. **Catalog A (SL-16)** — mass-dominant FoM under both weightings; the + McKnight top-of-ranking class; recommended kit is the honest + EDT-candidate above, not a closed recommendation. +2. **Catalog B (SL-8)** — closes today with a cheap, practical sail kit; a + much larger per-class population than catalog A's top-20 concern list; + not a differentiator on cost per removal (within ≈0.3% of catalog A in + relative CU). +3. **Catalog C (Envisat-class)** — a separate controlled-reentry mission + class, **not kit-ranked** against A or B (no WP5/WP6 campaign rows exist + for it). + +Legal/consent gate (D12): no catalog above is ranked up or down on +legal/consent grounds anywhere in this document — consent is a gate, not a +scoring input. Full per-class defenses, one paragraph each, with the +McKnight/ADReS-A/Envisat citations quoted in full: +[generated/wp14_prioritization.md](../generated/wp14_prioritization.md). + +## What this trade does not claim + +- No claim that EDT libration risk (T7) is resolved, or that the plasma + density used is a validated model (it is a cited PLACEHOLDER pair). +- No per-object casualty-risk (Ec) computation for Class C — that is a + documented gap, not a silent omission. +- No absolute cost figure — cost figures are relative CU with cited + low/mid/high MUSD ranges only, detailed in + [docs/cost_model.md](cost_model.md). +- No legal or regulatory determination — see + [docs/legal_regulatory.md](legal_regulatory.md). diff --git a/docs/technical_architecture.md b/docs/technical_architecture.md new file mode 100644 index 0000000..c196ec9 --- /dev/null +++ b/docs/technical_architecture.md @@ -0,0 +1,197 @@ +# Technical architecture + +Not a mission proposal; TRL 4, GNC software element only +([roadmap.md](roadmap.md)). This page owns: the module map (which module +lives where and who else documents its numbers), the build/reproduction +instructions, the repository layout, and the CI reproducibility gate. GNC +algorithm detail (dynamics, controller, estimator, guidance mechanics) is +[gnc.md](gnc.md)'s; safety numbers are [safety.md](safety.md)'s; cost numbers +are [cost_model.md](cost_model.md)'s; kit/target trade numbers are +[target_selection.md](target_selection.md)'s; regulatory-precheck detail is +[legal_regulatory.md](legal_regulatory.md)'s. R16: this file states numbers +only via explicit references to committed artifacts, never a restated, +un-audited literal. + +## Module map + +Modules this file owns architecturally (mechanism, not GNC math or safety +numbers): + +- **TMR fuel store** (`include/adsc/fuel_store.hpp`, `fuel_store.cpp`): three + redundant copies guarded by a real CRC32 (IEEE, over the full 8-byte value + via `memcpy`), majority voting, and scrubbing. Recovers single- and + double-copy SEU flips and flags an unrecoverable triple failure instead of + returning a silent bad value. Unit tested (`tests/test_fuel_store.cpp`). + This is the avionics-philosophy component (D7: COTS + TMR/FDIR, not + full rad-hard); TMR state is non-atomic, and threading assumptions are + documented at call sites. +- **Fidelity ladder (WP12), one-code-path architecture** + (`include/adsc/propagation.hpp`, `src/main_ladder.cpp`, target + `adsc_ladder`): runtime-selectable **L0/L1/L2** propagation on **one code + path** — no fork. L0 is the original WP1 Clohessy-Wiltshire linearization + (every committed WP5 campaign number stays byte-identical under it); L1 + differences a full inertial two-body+J2 RK4 propagation of both craft into + the target's instantaneous LVLH frame; L2 adds a per-craft free-molecular + drag term under an *independent* ballistic-coefficient dispersion stream + (`ds-v2`; the committed campaign's own `ds-v1` stream is untouched). L4 + (estimate-driven guidance) and L5 (delta-sigma minimum-impulse-bit actuator) + extend the same ladder — see [gnc.md](gnc.md) for what each level changes + about the control/estimation loop, and [safety.md](safety.md) for the + re-verification numbers this architecture exists to produce. +- **Installer mission flow (WP3), phase-machine structure** + (`include/adsc/mission.hpp`, `run_installer_mission`): the mission runs as + a fixed phase sequence — approach → sync → attach → depart. Approach + verifies the passively-safe corridor; sync gates on the WP2 tumble-sync + criteria; attach clamps at the gated closing speed and hands the deorbit + kit over (the servicer loses the kit mass, the target gains kit mass + sail + area, changing its A/m); depart transfers to a bounded, keep-out-clearing + relative orbit. The contact-energy budget this phase machine produces is a + safety number ([safety.md](safety.md) owns it); the kit decay-trade + numbers this phase feeds are [target_selection.md](target_selection.md)'s. + Unit tested (`tests/test_mission.cpp`). +- **Visualization Pack (WP7a)** (`tools/viz/make_viz.py`, `decay_trade` + target): a **Python 3 standard-library-only** generator (spec R9 tooling + exception — no plotting library, no JS/CDN/external fonts; SVG is written + directly) that turns the committed WP5/WP6/WP3 CSVs into nine static SVG + figures plus a static report page. It reads every number from a committed + CSV (nothing hand-written); each caption carries the source CSV, master + seed and schema_version; no timestamps/run-times are embedded — a CI gate + asserts regeneration matches the committed bytes. It does not draw from any + live/streaming source, embed real imagery, or make interactive dashboards. + Tested via `tools/viz/test_viz.py` (ctest `viz`). +- **Evidence Pack generator (WP7 — the actual product)** + (`tools/evidence/make_evidence.py`): writes `evidence/adsc_evidence_pack.md` + — executive summary, the installer argument, safety case, honest + negatives, campaign/cost/FoM statistics, limitations, regulatory-precheck + summary, reproduction instructions, and an auto-collected PLACEHOLDER + inventory. Zero hand-written numbers: everything is machine-read from + committed CSVs. A claim-audit test (ctest `evidence`) rejects forbidden + overclaims (flight-ready/-proven, TRL 5/6 claims, "guaranteed", + legal-conclusion words), verifies required honesty sections, cross-checks + every quoted headline number against its source CSV, and enforces + determinism + committed==regenerated. `tools/regenerate_all.sh` is the + single regeneration entry point (CI runs exactly it — see below). + +Modules whose mechanism lives here only in one line, with detail owned +elsewhere: + +| Module | One-line role | Detail owned by | +|---|---|---| +| Rigid-body dynamics, SMC controller, tumble sync, point-mass inertia, relative motion (CW) | core GNC math | [gnc.md](gnc.md) | +| F1 capped-abort honesty, WP11 clearing-abort law + reachability screen | safety argument + campaign numbers | [safety.md](safety.md); guidance-law mechanics in [gnc.md](gnc.md) | +| Estimator + sensor models (WP4) | translation EKF + attitude MEKF | [gnc.md](gnc.md) | +| Campaign Monte-Carlo (WP5) | N=500/catalog dispersion campaign | [safety.md](safety.md) | +| Cost model + FoM (WP6) | relative-CU cost, WP14 absolute ranges | [cost_model.md](cost_model.md) | +| Kit decay trades, WP13 EDT physics (WP3/WP13) | sail/EDT trade, per-class kit table | [target_selection.md](target_selection.md) | +| Compliance Matrix Generator (WP8) | regulatory precheck, not legal advice | [legal_regulatory.md](legal_regulatory.md) | +| First-order PCM thermal budget, deorbit gating | minor subsystem detail | [gnc.md](gnc.md) | + +## Build + +Requires a C++17 compiler and Eigen 3.3+; Python 3 stdlib only for tooling +(spec R9 — no third-party Python packages). + +``` +git clone https://github.com/HeliCorgi/ADSC.git +cd ADSC +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release # add -DADSC_WERROR=ON for R3 +cmake --build build +``` + +## Reproduce every committed artifact + +`bash tools/regenerate_all.sh build` runs the following fixed order (its own +header comment is the single source of truth; CI runs exactly this script): + +| Step | Regenerates | Command | +|---:|---|---| +| 1/12 | WP5 campaign Monte-Carlo (N=500 × 2 catalogs) | `./build/adsc_campaign 500 generated` | +| 2/12 | WP12 fidelity-ladder re-verification (L0/L1/L2) | `./build/adsc_ladder 500 generated` | +| 3/12 | WP6 cost model + FoM (kit sweep, consumes WP5) | `./build/adsc_cost 500 generated` | +| 4/12 | WP3 decay-trade CSV | `./build/decay_trade generated` | +| 5/12 | WP13 kit-class trade + class-C comparison | `./build/kit_trade generated` | +| 6/12 | WP14 prioritization table (joins WP6 + WP13) | `python3 tools/prioritization/make_prioritization.py . generated` | +| 7/12 | T6 small-debris flux sweep | `./build/flux_sweep generated` | +| 8/12 | Reference metrics (pinned WP1/F1/WP2/WP3/WP4 numbers) | `./build/sim_metrics generated` | +| 9/12 | WP10c keep-out-violation forensics (read-only replay) | `python3 tools/forensics/make_forensics.py` | +| 10/12 | WP7a visualization pack | `python3 tools/viz/make_viz.py . generated/viz` | +| 11/12 | WP8 compliance precheck + matrix | `python3 tools/compliance/check_compliance.py`; `python3 tools/compliance/generate_matrix.py` | +| 12/12 | WP7 evidence pack | `python3 tools/evidence/make_evidence.py` | + +``` +./build/adsc_sim # closed-loop detumble driver (also scenarios 5/6/7, see gnc.md) +bash tools/regenerate_all.sh build # ...or regenerate EVERYTHING above in order (CI runs this) +ctest --test-dir build # fuel_store/relmotion/sync/decay/mission/estimator/campaign/ + # cost/flux/ladder/guidance/forensic14/viz/compliance/evidence/ + # forensics/prioritization/readme_numbers +``` + +When invoking bare `g++` directly, prefer passing Eigen via `-isystem` to +avoid GCC `-Wmaybe-uninitialized` false positives; CMake handles this +automatically. + +## Layout + +``` +include/adsc/ public headers (fuel_store, dynamics, controller, thermal, + relmotion, decay, estimator, mission, campaign, cost, flux, + propagation) +src/ implementations + main.cpp sim + WP3/WP5/WP6/WP13/T6 emitters (main_*.cpp) +tests/ C++ unit tests (+ Python tests under tools/, ctest viz/compliance/ + forensics/prioritization/readme_numbers) +tools/viz/ WP7a Python 3 stdlib-only visualization generator (make_viz.py) +tools/compliance/ WP8 regulatory precheck: rulepacks, schema, checker, matrix generator +tools/evidence/ WP7 evidence-pack generator + claim-audit test +tools/prioritization/ WP14 prioritization-table generator (joins WP6 + WP13) +tools/forensics/ WP10c keep-out-violation forensics (read-only replay of WP5) +tools/readme/ README.md marker-block filler + test (WP14 review C4 fix) +tools/regenerate_all.sh single regeneration entry point (CI runs exactly this) +generated/ committed artifacts: WP5 campaign, WP6 cost/FoM, T6 flux, WP3 decay, + WP13 kit trade + class-C, WP14 prioritization, reference_metrics CSVs, + viz/ SVGs, compliance_findings.json, WP10/WP11 forensics, + WP12 fidelity-ladder re-verification +evidence/ committed generated evidence artifacts (adsc_evidence_pack.md, + compliance matrix; same reproducibility gate as generated/) +docs/ concept, technical architecture (this file), gnc, safety, + target_selection, cost_model, legal_regulatory, limitations, + roadmap, pitch_agencies (spec:274), plus the one-pager, + 5-page summary, deck source and release-engineering notes + (WP15 tasks 4-5) +.github/ CI (Ubuntu + Eigen: cmake build + ctest, warnings-as-errors, + reproducibility gate) +adsc-specification-v5.md active spec (work packages, hard rules, locked decisions) +adsc-specification-v4.md retired v4.2 spec (superseded by v5; kept because v5 + references its D/R/T items) +``` + +## CI gates + +`.github/workflows/ci.yml` runs, on every push and pull request: install +Eigen3 (apt), configure with `-DADSC_WERROR=ON` (R3, warnings-as-errors), +build, run `ctest`, run `./build/adsc_sim`, then run +`bash tools/regenerate_all.sh build` (the exact script above — "single source +of truth for reproduction" per its own header) and +`python3 tools/readme/fill_readme_numbers.py .` (fills README's two number +marker-blocks from the committed WP5/WP6 CSVs — a separate step from +`regenerate_all.sh` by design, documented in that script's own header, since +README.md lives at the repo root and is not under `generated/` or +`evidence/`). It then runs the **reproducibility gate**: + +``` +git diff --exit-code -- generated/ evidence/ README.md +``` + +— any drift between committed bytes and freshly regenerated bytes fails the +build (R6). Generated artifacts are uploaded even on gate failure, so an R15 +re-baselining PR can take CI's regenerated bytes as its new committed +reference instead of requiring a local toolchain. + +**docs/ and the reproducibility gate.** R16 requires every number under +`docs/` to come from a generated include or an explicit reference, with the +same drift protection `generated/`/`evidence/`/`README.md` already have. +Extending the gate's diff path above to include `docs/`, and building the +`docs/`-scoped claims-audit (banned-word, numeric-consistency, L-tag +patterns) that makes drift or an unaudited numeric literal fail CI, is a +required companion change tracked alongside this docs/ tree (WP15 task 3) — +call out its status explicitly in the PR body rather than assuming it is +already wired in. diff --git a/docs/technical_summary_5p.md b/docs/technical_summary_5p.md new file mode 100644 index 0000000..76c4755 --- /dev/null +++ b/docs/technical_summary_5p.md @@ -0,0 +1,158 @@ +# ADSC — Technical Summary + +*A 5-page-equivalent technical summary of ADSC — an open, reproducible +evidence package for **Kessler-precursor removal**: a small servicer installs +a passive deorbit kit on the massive derelicts that would otherwise become +the next fragment clouds, and departs. For reviewers who want the mechanism, +not just the headline. Every safety/performance claim below carries its +fidelity tag (L0…L6, WP12); numbers are stated by reference to a committed +artifact, never re-derived. Scope: TRL 4, GNC software element only; not +flight software; not a mission proposal; not legal advice — see +[roadmap.md](roadmap.md) and [limitations.md](limitations.md).* + +## 1. Architecture + +ADSC simulates an **installer**, not a tug (D1): a small servicer attaches a +passive deorbit kit (drag sail or electrodynamic tether) to a massive +derelict upper stage and departs, rather than decelerating the full stage +mass itself. The C++17 simulator is one code path across fidelity levels +(no fork): a TMR/CRC32-protected fuel store models actuator-command +integrity; RK4 rigid-body dynamics drive a quaternion sliding-mode attitude +controller (regulation + tracking, torque-free feedforward, transport term); +relative motion runs on Clohessy-Wiltshire at L0 and on full inertial +two-body+J2 (L1) or +drag (L2) propagation differenced back into the target +LVLH frame at higher levels — every committed L0 campaign number is +byte-identical regardless of which level is compiled in. A phase-machine +mission driver (SAFE/APPROACH/SYNC/attach/depart) wraps the GNC core; a +Monte-Carlo campaign engine runs N=500 dispersed missions per catalog at a +fixed master seed; a relative-cost-unit (CU) model with WP14 absolute-range +extensions computes cost/FoM; a regulatory precheck evaluates a declared +mission profile against versioned rulepacks. Full module map: +[technical_architecture.md](technical_architecture.md); +GNC internals: [gnc.md](gnc.md). + +## 2. GNC evidence + +Attitude sync (tumble arrest to pointing lock) is demonstrated +truth-driven at **L0** and re-demonstrated estimate-driven under sensor +noise: the translation EKF + attitude MEKF run on estimates only (truth is +structurally isolated to sensor models and error recording), and the +NIS/NEES consistency watchdog rejects the classic covariance-inflation +fake. The guided approach flies closed-loop with reachability-screened +aborts, retained as an independent gate check rather than mere guidance +(closing the long-standing "gate, not guidance" limit). Under **L4** +(navigation-error extensions: measurement dropout + an unestimated +range-bias random walk), the estimator is honestly reported as *optimistic* +about its own accuracy — a documented estimation gap, not a hidden one; +adding bias states is the identified fix, not yet implemented. Under **L5** +(minimum-impulse-bit actuator realization with command delay and a +single-axis fault), the long-standing continuous-torque idealization +caveat is retired **by measurement**: sync still completes, and guidance +contact velocity quantized at the actuator's translation MIB still meets +its gate. L3 (SRP) and L6 (deterministic worst-case Monte Carlo) are not +yet implemented — stated here rather than implied. Pinned regression +values and full per-channel breakdown: [gnc.md](gnc.md). + +## 3. Safety case + +Approach safety is a **passively-safe approach design with +clearance-verified aborts (keep-out violations 0/500 per catalog [L0, +ds-v1, Wilson ≤ 0.0076])** — never "strict approach safety" or "safe +satellite" (spec §1 binding wording). History (R15): a pre-WP11 baseline +showed a nonzero violation rate, archived at `v0.10-phase0-baseline`; WP11's +clearing-abort law closed it to zero, and the **L1** (+J2) / **L2** (+drag) +fidelity ladder re-verifies zero violations over every committed abort +event at both higher levels — a negative-negative result (the anticipated +CW-safe-but-higher-level-unsafe failure mode did not materialize for this +dispersion set), reported with the same discipline as a positive one. +Safety-ellipse margin decay under J2 is measured, not merely flagged +qualitatively as in earlier revisions. A contact-energy budget bounds the +kinetic energy transferred at the gated closing speed, framed explicitly as +a geometry-keyed-clamp argument, not a claim that the resulting energy is +below any specific material damage threshold (that threshold remains +PLACEHOLDER-cited). All guarantees are model-scoped: a real mission +re-verifies every coast against a higher-fidelity propagator before flight. +Full re-verification table, forensic-regression detail, and campaign +statistics: [safety.md](safety.md). + +## 4. Kit trade + +The kit-decay trade reports the honest negative first (WP13): meeting the +IADC 25-year post-mission-disposal guideline needs an impractically large +sail for the ~9 t reference class, while the same guideline closes at a +practical sail area for the ~1.4 t reference class. For the heavy class, an +electrodynamic tether is carried as an honest **candidate with open +risks** — not a closed recommendation — because the along-track libration +dynamics of a rigid current-carrying tether remain an open trade (T7, +cited literature pointer, not resolved by this package) and plasma-density +inputs remain PLACEHOLDER at both solar-min and solar-max. A third, +separate mission class (a defunct massive sun-synchronous payload) is +evaluated on controlled-reentry terms, not kit-ranked against the two +classes above — a kit-only deorbit is explicitly not the recommended path +for that class. The WP14 target-prioritization table joins this trade with +the cost/FoM model (no new simulation, a pure derived view over already- +committed CSVs) and ranks the heavy class first under both figure-of-merit +weightings evaluated, with a one-paragraph defense per class. Full trade, +per-class kit recommendation, and prioritization defense: +[target_selection.md](target_selection.md), +`generated/wp14_prioritization.md`. + +## 5. Cost and figure of merit + +Cost is reported in **relative cost units (CU)** as the primary metric — +"minimum cost" as an absolute claim is banned throughout (spec §1); the +defensible claim is **cost-effectiveness: cost per removal and cost per +unit risk reduction**. The amortization curve bottoms at a specific kit +count because the mission Δv budget, not the kit count, caps removals per +mission — the honest capacity story a mission designer needs, not a +monotonically-improving curve. Parameter sensitivity is ranked by a +one-at-a-time tornado (development cost dominates). The figure of merit +(debris-risk reduction per cost) is reported under two independent +congestion weightings that can disagree about band priority (open trade +T5, kept visible rather than resolved by fiat); absolute per-catalog FoM is +mass-dominated under both. WP14 additionally derives a cited absolute-cost +**range** (never a point value) from an itemized cost table in which every +row carries a source or stays explicitly PLACEHOLDER (D10); the external +currency anchor is explicitly framed as low-standing relative to the +primary CU results, because an unaffiliated author's absolute-cost +estimate is inherently uncertain next to an agency's own parametric model. +Full model, itemized sources, and the honesty framing quoted verbatim from +the schema doc: [cost_model.md](cost_model.md). + +## 6. Limitations + +Stated plainly, none hidden: safety statements are exact only in the +linearized model families exercised (L0 Clohessy-Wiltshire; L1/L2 widen the +validated envelope but do not certify the real environment); inter-target +phasing Δv/time is a flat parameterized cost, not an optimized trajectory; +campaign-level sensor dispersions are drawn but not re-propagated through +the full closed loop at N=500 scale (the closed-loop sensor argument is +carried separately at smaller N); target inertia is assumed known (a real +mission needs inertia identification); estimator sensor biases are not yet +estimated, and this is measured, not merely disclosed, at L4; small-debris +(1–10 cm) removal is out of scope by physics, not neglect (collection-area +argument, full table cited); the regulatory precheck's jurisdiction +coverage is partial (UN/US/ITU/ESA references only; several adopter-state +national-law bodies are not yet covered); and 140 PLACEHOLDER marks remain +in the codebase (56 decision-critical), mechanically inventoried, not +hand-curated. None of this is claimed solved anywhere in this package. +Full list: [limitations.md](limitations.md), evidence pack §10. + +## Key numbers (pinned, current committed values) + + +| metric | value | fidelity / notes | source | +|---|---|---|---| +| Campaign success (productive end), SL-16 / SL-8 | 0.556 [0.512, 0.599] / 0.542 [0.498, 0.585] (Wilson 95%) | L0, ds-v1 | `generated/wp5_campaign_summary.csv` | +| Keep-out violations, both classes | 0 / 500, Wilson UB ≤ 0.0076 | L0, ds-v1; zero re-verified over every re-verified abort event at L1/L2 | `generated/wp5_campaign_summary.csv`, `generated/wp12_ladder.csv` | +| Amortization minimum (N = 4) | 44.80 CU/removal = 0.285× the N = 1 baseline | Δv-limited, measured | `generated/wp6_cost_summary.csv` | +| FoM p50, SL-16 class (spatial / criticality) | 156.70 / 200.90 kg/CU | two independent weightings, disagreement tracked (T5) | `generated/wp6_cost_summary.csv` | +| Cost/removal p50, SL-16 class (low / mid / high) | 2.81 / 6.92 / 18.87 MUSD | external anchor, cited range only (D10) | `generated/wp6_cost_summary.csv` (WP14 rows) | +| Attitude sync (truth-driven pin) | 16.87 s | L0 | `reference_metrics.csv` | +| Attitude sync, estimate-driven under sensor noise | 17.07 s | L0, estimator-in-the-loop | `reference_metrics.csv` | +| Guided-approach demo: contact speed / total Δv | 0.10 m/s (gate 0.15 m/s) / 2.53 m/s | L0, truth-fed guidance | evidence pack §3, `reference_metrics.csv` | + +*This block is generated by `tools/docs/fill_docs_numbers.py` from the same +committed CSVs as README's WP5-NUMBERS/WP6-NUMBERS blocks and +`reference_metrics.csv`; run with `--check` to verify, in-place to refresh.* + diff --git a/evidence/adsc_evidence_pack.md b/evidence/adsc_evidence_pack.md index 235e3b5..b094c55 100644 --- a/evidence/adsc_evidence_pack.md +++ b/evidence/adsc_evidence_pack.md @@ -536,38 +536,38 @@ table: `tools/evidence/make_evidence.py::classify_importance` (a small explicit path+keyword table, not a formal per-line audit -- a handful of comment lines may land one bucket over from a stricter reading). -Total marks: **140** (decision-critical: **56**, moderate: **43**, cosmetic: **41**) +Total marks: **143** (decision-critical: **56**, moderate: **46**, cosmetic: **41**) | location | importance | line | |---|---|---| | `include/adsc/campaign.hpp:17` | moderate | // as a parameterized Delta-v / time cost (PLACEHOLDER, no plane-change | | `include/adsc/campaign.hpp:22` | moderate | // Dispersions (all PLACEHOLDER where not physically validated, centralized in | -| `include/adsc/campaign.hpp:115` | moderate | // All PLACEHOLDER values are marked; none is a physically validated figure. | -| `include/adsc/campaign.hpp:128` | decision-critical | double dv_budget_m_s = 140.0; // PLACEHOLDER servicer Delta-v budget [m/s] | -| `include/adsc/campaign.hpp:130` | moderate | // Per-leg Delta-v cost model (PLACEHOLDER; parameterized, no plane-change | -| `include/adsc/campaign.hpp:132` | decision-critical | double dv_approach_m_s = 8.0; // PLACEHOLDER rendezvous/approach [m/s] | -| `include/adsc/campaign.hpp:133` | decision-critical | double dv_sync_m_s = 3.0; // PLACEHOLDER proximity + attitude sync [m/s] | -| `include/adsc/campaign.hpp:134` | decision-critical | double dv_depart_m_s = 5.0; // PLACEHOLDER safe departure [m/s] | -| `include/adsc/campaign.hpp:135` | decision-critical | double dv_abort_m_s = 4.0; // PLACEHOLDER safe-abort maneuver [m/s] | -| `include/adsc/campaign.hpp:136` | decision-critical | double dv_phasing_m_s = 15.0; // PLACEHOLDER inter-target phasing hop [m/s] | -| `include/adsc/campaign.hpp:138` | moderate | // Per-leg time model (PLACEHOLDER), for the mission elapsed-time metric. | -| `include/adsc/campaign.hpp:139` | decision-critical | double t_attach_s = 300.0; // PLACEHOLDER clamp + install [s] | -| `include/adsc/campaign.hpp:140` | decision-critical | double t_depart_s = 600.0; // PLACEHOLDER departure settle [s] | -| `include/adsc/campaign.hpp:141` | decision-critical | double t_phasing_s = 86400.0; // PLACEHOLDER inter-target phasing [s] | -| `include/adsc/campaign.hpp:143` | moderate | // --- dispersions (PLACEHOLDER 1-sigma unless noted) --- | -| `include/adsc/campaign.hpp:147` | decision-critical | double nominal_closing_m_s = 0.10; // PLACEHOLDER nominal capture closing speed [m/s] | -| `include/adsc/campaign.hpp:148` | decision-critical | double disp_closing_sigma_m_s = 0.045; // PLACEHOLDER (P(>0.15) ~ 0.13) [m/s] | -| `include/adsc/campaign.hpp:151` | decision-critical | double disp_rel_pos_m = 40.0; // PLACEHOLDER per-axis initial rel-position [m] | -| `include/adsc/campaign.hpp:152` | decision-critical | double disp_rel_vel_m_s = 0.02; // PLACEHOLDER per-axis initial rel-velocity [m/s] | -| `include/adsc/campaign.hpp:155` | decision-critical | double disp_tumble_rate_frac = 0.40; // PLACEHOLDER fractional 1-sigma on /w_t/ [-] | -| `include/adsc/campaign.hpp:156` | decision-critical | double disp_tumble_axis_rad = 0.30; // PLACEHOLDER tumble-axis tilt 1-sigma [rad] | -| `include/adsc/campaign.hpp:157` | decision-critical | double disp_init_att_rad = 0.35; // PLACEHOLDER servicer att-offset 1-sigma [rad] | -| `include/adsc/campaign.hpp:160` | decision-critical | double disp_actuator_scale = 0.10; // PLACEHOLDER ~+/-10% torque-scale 1-sigma [-] | -| `include/adsc/campaign.hpp:161` | decision-critical | double disp_actuator_misalign_rad = 0.01; // PLACEHOLDER axis-misalignment 1-sigma [rad] | -| `include/adsc/campaign.hpp:165` | decision-critical | double disp_sensor_noise_frac = 0.30; // PLACEHOLDER fractional 1-sigma on sensor sigmas [-] | -| `include/adsc/campaign.hpp:166` | decision-critical | double disp_sensor_bias_rad = 5.0e-4; // PLACEHOLDER sensor bias 1-sigma [rad] | -| `include/adsc/campaign.hpp:170` | decision-critical | double nominal_solar_factor = 1.0; // PLACEHOLDER mean atmospheric-density factor [-] | -| `include/adsc/campaign.hpp:171` | decision-critical | double disp_solar_factor_frac = 0.50; // PLACEHOLDER fractional 1-sigma [-] | +| `include/adsc/campaign.hpp:116` | moderate | // All PLACEHOLDER values are marked; none is a physically validated figure. | +| `include/adsc/campaign.hpp:129` | decision-critical | double dv_budget_m_s = 140.0; // PLACEHOLDER servicer Delta-v budget [m/s] | +| `include/adsc/campaign.hpp:131` | moderate | // Per-leg Delta-v cost model (PLACEHOLDER; parameterized, no plane-change | +| `include/adsc/campaign.hpp:133` | decision-critical | double dv_approach_m_s = 8.0; // PLACEHOLDER rendezvous/approach [m/s] | +| `include/adsc/campaign.hpp:134` | decision-critical | double dv_sync_m_s = 3.0; // PLACEHOLDER proximity + attitude sync [m/s] | +| `include/adsc/campaign.hpp:135` | decision-critical | double dv_depart_m_s = 5.0; // PLACEHOLDER safe departure [m/s] | +| `include/adsc/campaign.hpp:136` | decision-critical | double dv_abort_m_s = 4.0; // PLACEHOLDER safe-abort maneuver [m/s] | +| `include/adsc/campaign.hpp:137` | decision-critical | double dv_phasing_m_s = 15.0; // PLACEHOLDER inter-target phasing hop [m/s] | +| `include/adsc/campaign.hpp:139` | moderate | // Per-leg time model (PLACEHOLDER), for the mission elapsed-time metric. | +| `include/adsc/campaign.hpp:140` | decision-critical | double t_attach_s = 300.0; // PLACEHOLDER clamp + install [s] | +| `include/adsc/campaign.hpp:141` | decision-critical | double t_depart_s = 600.0; // PLACEHOLDER departure settle [s] | +| `include/adsc/campaign.hpp:142` | decision-critical | double t_phasing_s = 86400.0; // PLACEHOLDER inter-target phasing [s] | +| `include/adsc/campaign.hpp:144` | moderate | // --- dispersions (PLACEHOLDER 1-sigma unless noted) --- | +| `include/adsc/campaign.hpp:148` | decision-critical | double nominal_closing_m_s = 0.10; // PLACEHOLDER nominal capture closing speed [m/s] | +| `include/adsc/campaign.hpp:149` | decision-critical | double disp_closing_sigma_m_s = 0.045; // PLACEHOLDER (P(>0.15) ~ 0.13) [m/s] | +| `include/adsc/campaign.hpp:152` | decision-critical | double disp_rel_pos_m = 40.0; // PLACEHOLDER per-axis initial rel-position [m] | +| `include/adsc/campaign.hpp:153` | decision-critical | double disp_rel_vel_m_s = 0.02; // PLACEHOLDER per-axis initial rel-velocity [m/s] | +| `include/adsc/campaign.hpp:156` | decision-critical | double disp_tumble_rate_frac = 0.40; // PLACEHOLDER fractional 1-sigma on /w_t/ [-] | +| `include/adsc/campaign.hpp:157` | decision-critical | double disp_tumble_axis_rad = 0.30; // PLACEHOLDER tumble-axis tilt 1-sigma [rad] | +| `include/adsc/campaign.hpp:158` | decision-critical | double disp_init_att_rad = 0.35; // PLACEHOLDER servicer att-offset 1-sigma [rad] | +| `include/adsc/campaign.hpp:161` | decision-critical | double disp_actuator_scale = 0.10; // PLACEHOLDER ~+/-10% torque-scale 1-sigma [-] | +| `include/adsc/campaign.hpp:162` | decision-critical | double disp_actuator_misalign_rad = 0.01; // PLACEHOLDER axis-misalignment 1-sigma [rad] | +| `include/adsc/campaign.hpp:167` | decision-critical | double disp_sensor_noise_frac = 0.30; // PLACEHOLDER fractional 1-sigma on sensor sigmas [-] | +| `include/adsc/campaign.hpp:168` | decision-critical | double disp_sensor_bias_rad = 5.0e-4; // PLACEHOLDER sensor bias 1-sigma [rad] | +| `include/adsc/campaign.hpp:172` | decision-critical | double nominal_solar_factor = 1.0; // PLACEHOLDER mean atmospheric-density factor [-] | +| `include/adsc/campaign.hpp:173` | decision-critical | double disp_solar_factor_frac = 0.50; // PLACEHOLDER fractional 1-sigma [-] | | `include/adsc/cost.hpp:41` | decision-critical | // PLACEHOLDER (R10) and untouched by WP14. WP6/WP14 implement the cost model | | `include/adsc/cost.hpp:49` | decision-critical | // `altitude_km` under each weighting. PLACEHOLDER values -- fill with citations | | `include/adsc/cost.hpp:57` | decision-critical | double spatial; // PLACEHOLDER normalized spatial-density weight | @@ -615,7 +615,7 @@ Total marks: **140** (decision-critical: **56**, moderate: **43**, cosmetic: **4 | `include/adsc/mission.hpp:89` | moderate | // WP4: sensor + estimator abstractions. All PLACEHOLDER values (R10); | | `include/adsc/mission.hpp:134` | moderate | double sensor_dropout_prob = 0.05; // PLACEHOLDER per-sample Bernoulli missed-det... | | `include/adsc/mission.hpp:135` | moderate | double range_bias_walk_m_per_sqrt_s = 1.0e-3; // PLACEHOLDER unestimated range-bias random-w... | -| `include/adsc/mission.hpp:265` | moderate | double min_impulse_bit_nms = 0.0; // PLACEHOLDER MIB angular-impulse quantum [N m s], per ax... | +| `include/adsc/mission.hpp:266` | moderate | double min_impulse_bit_nms = 0.0; // PLACEHOLDER MIB angular-impulse quantum [N m s], per ax... | | `include/adsc/propagation.hpp:84` | moderate | // ---- Physical constants (cited, not PLACEHOLDER) ---- | | `src/campaign.cpp:286` | moderate | // Inter-target phasing to the next target (PLACEHOLDER flat cost). | | `src/campaign.cpp:400` | moderate | // Under the current flat PLACEHOLDER Delta-v cost these coincide numerically | @@ -667,19 +667,22 @@ Total marks: **140** (decision-critical: **56**, moderate: **43**, cosmetic: **4 | `src/main_ladder.cpp:79` | moderate | constexpr double kSL8AreaM2 = 7.5; // PLACEHOLDER SL-8 / Kosmos-3M bare-stage cross-sectio... | | `src/main_ladder.cpp:82` | moderate | // PLACEHOLDER statistical/numerical-safety constants are named here. | | `src/main_ladder.cpp:83` | moderate | constexpr double kBcDispersionFrac = 0.30; // PLACEHOLDER +/-30% 1-sigma per-craft ballistic... | -| `src/main_metrics.cpp:329` | moderate | l5_act.min_impulse_bit_nms = 2.0e-4; // PLACEHOLDER MIB angular impulse [N m s] | -| `src/main_metrics.cpp:330` | moderate | l5_act.delay_steps = 1; // PLACEHOLDER one-control-step actuator lag | -| `src/main_metrics.cpp:331` | moderate | l5_act.fault_axis = 0; // PLACEHOLDER: body x-axis | -| `src/main_metrics.cpp:332` | moderate | l5_act.fault_axis_scale = 0.5; // PLACEHOLDER: 50% torque authority on that axis | -| `src/main_metrics.cpp:346` | moderate | // at a 1e-3 m/s PLACEHOLDER translation MIB and re-checking the | -| `src/main_metrics.cpp:351` | moderate | const double mib_m_s = 1.0e-3; // PLACEHOLDER translation MIB [m/s] | -| `src/main_metrics.cpp:355` | cosmetic | "contact velocity quantized at a 1e-3 m/s PLACEHOLDER translation " | +| `src/main_metrics.cpp:331` | moderate | l5_act.min_impulse_bit_nms = 2.0e-4; // PLACEHOLDER MIB angular impulse [N m s] | +| `src/main_metrics.cpp:332` | moderate | l5_act.delay_steps = 1; // PLACEHOLDER one-control-step actuator lag | +| `src/main_metrics.cpp:333` | moderate | l5_act.fault_axis = 0; // PLACEHOLDER: body x-axis | +| `src/main_metrics.cpp:334` | moderate | l5_act.fault_axis_scale = 0.5; // PLACEHOLDER: 50% torque authority on that axis | +| `src/main_metrics.cpp:348` | moderate | // at a 1e-3 m/s PLACEHOLDER translation MIB and re-checking the | +| `src/main_metrics.cpp:353` | moderate | const double mib_m_s = 1.0e-3; // PLACEHOLDER translation MIB [m/s] | +| `src/main_metrics.cpp:357` | cosmetic | "contact velocity quantized at a 1e-3 m/s PLACEHOLDER translation " | +| `tools/docs/fill_docs_numbers.py:386` | moderate | L.append("PLACEHOLDER* %sv cost the two coincide numerically %s every aborting mission needs" | +| `tools/docs/fill_docs_numbers.py:407` | moderate | L.append("percentiles matching across the two presets is expected (a flat PLACEHOLDER cost") | +| `tools/docs/fill_docs_numbers.py:672` | moderate | L.append("/ catalog / sail kit mass [kg] / EDT kit mass [kg] (PLACEHOLDER) / " | +| `tools/docs/fill_docs_numbers.py:673` | cosmetic | "EDT deploy-failure risk (PLACEHOLDER) / EDT open-circuit EMF [V] /") | | `tools/prioritization/make_prioritization.py:150` | cosmetic | "from 840-850 km) remains PLACEHOLDER: the McKnight ranking gives a " | | `tools/prioritization/make_prioritization.py:503` | cosmetic | "years band above folds libration in only as a flat PLACEHOLDER " | | `tools/prioritization/test_prioritization.py:146` | cosmetic | check("PLACEHOLDER" in md_txt, | | `tools/prioritization/test_prioritization.py:148` | cosmetic | "still PLACEHOLDER") | -| `tools/readme/fill_readme_numbers.py:244` | moderate | L.append("PLACEHOLDER* %sv cost the two coincide numerically %s every " | -| `tools/readme/fill_readme_numbers.py:270` | cosmetic | "flat PLACEHOLDER cost") | +| `tools/readme/fill_readme_numbers.py:243` | moderate | L.append("Full model + sources-or-PLACEHOLDER itemization:") | ## 11. Changelog - R15 pin supersessions diff --git a/include/adsc/campaign.hpp b/include/adsc/campaign.hpp index 51a51d5..b9c5089 100644 --- a/include/adsc/campaign.hpp +++ b/include/adsc/campaign.hpp @@ -27,7 +27,8 @@ namespace adsc { // misalignment). Sensor noise/bias dispersions are drawn and their realized // scale is recorded, but their closed-loop effect is characterized by the WP4 // estimate-driven acceptance rather than re-simulated in every campaign run -- -// a documented WP5 simplification (see README known limits). +// a documented WP5 simplification (see docs/limitations.md; WP15: this +// known limit moved out of README). // // Keep-out abort screen (WP10c / WP11): the per-target gate-abort event calls // the abort law from the outer departure standoff, perturbed by the @@ -161,7 +162,8 @@ struct CampaignConfig { double disp_actuator_misalign_rad = 0.01; // PLACEHOLDER axis-misalignment 1-sigma [rad] // Sensor (drawn + recorded; NOT propagated through the truth-driven sync -- - // see the header note and README known limits). + // see the header note and docs/limitations.md; WP15: this known limit + // moved out of README). double disp_sensor_noise_frac = 0.30; // PLACEHOLDER fractional 1-sigma on sensor sigmas [-] double disp_sensor_bias_rad = 5.0e-4; // PLACEHOLDER sensor bias 1-sigma [rad] diff --git a/include/adsc/mission.hpp b/include/adsc/mission.hpp index 6423974..8ed076e 100644 --- a/include/adsc/mission.hpp +++ b/include/adsc/mission.hpp @@ -255,7 +255,8 @@ struct ActuatorError { Eigen::Vector3d misalign_rad = Eigen::Vector3d::Zero(); // small-angle body misalignment [rad] // WP12 L5 additive fields (retires the "continuous-torque DACS - // approximation" README known limit): a real DACS delivers torque in + // approximation" known limit -- docs/limitations.md, WP15: this limit + // moved out of README): a real DACS delivers torque in // discrete minimum-impulse-bit (MIB) quanta, may lag the command by a // fixed number of control steps, and may carry a partial single-axis // fault. Neutral defaults (0 MIB = no quantization, 0 delay, fault_axis diff --git a/src/guidance.cpp b/src/guidance.cpp index dd691ed..88ab948 100644 --- a/src/guidance.cpp +++ b/src/guidance.cpp @@ -448,8 +448,9 @@ GuidedApproachReport GuidedApproach::fly() { // ============================================================================ // WP12 L4: estimate-driven translation guidance // ---------------------------------------------------------------------------- -// Closes the WP11/README "estimate-driven translation guidance remains open" -// known limit. Truth propagates the SAME deterministic CW dynamics as fly() +// Closes the WP11 "estimate-driven translation guidance remains open" known +// limit (docs/limitations.md, WP15: this limit moved out of README). Truth +// propagates the SAME deterministic CW dynamics as fly() // above and receives MATCHED velocity process noise at each ranging interval // (mirrors run_estimated_sync, estimator.hpp/WP4); range + LOS-unit-vector // measurements are drawn from truth with the existing WP4 sigmas, gated by a diff --git a/src/main_campaign.cpp b/src/main_campaign.cpp index ea09839..62e414e 100644 --- a/src/main_campaign.cpp +++ b/src/main_campaign.cpp @@ -1,11 +1,13 @@ // WP5 campaign driver: regenerates the committed generated/ artifacts and -// prints a human-readable summary (evidence for the PR / README, R6/R12). +// prints a human-readable summary (evidence for the PR / docs/safety.md, +// R6/R12). // // adsc_campaign [n_runs] [out_dir] // // Defaults: n_runs = CampaignConfig::n_runs (500, the full campaign target), // out_dir = "generated". Pass a smaller n_runs for a fast CI subset without -// touching the committed full-campaign files (README documents the split). +// touching the committed full-campaign files (docs/safety.md documents the +// split; WP15: this moved out of README). #include #include #include diff --git a/src/main_cost.cpp b/src/main_cost.cpp index 1619593..e1ab1a1 100644 --- a/src/main_cost.cpp +++ b/src/main_cost.cpp @@ -1,6 +1,6 @@ // WP6 cost/FoM driver: regenerates generated/wp6_cost_* and prints the // amortization curves, FoM table and tornado ranking (evidence for the PR / -// README, R6/R12). +// docs/cost_model.md, R6/R12; WP15: this moved out of README). // // adsc_cost [n_runs] [out_dir] // diff --git a/src/main_decay_trade.cpp b/src/main_decay_trade.cpp index fdbfd63..9d7320b 100644 --- a/src/main_decay_trade.cpp +++ b/src/main_decay_trade.cpp @@ -26,9 +26,10 @@ const char* kSchema = "1.0"; // Sail areas swept for the trade curve [m^2]. Spans B's ~7 m^2 and A's // ~135-2155 m^2 25-year crossings so both are visible. 1000.0 added (WP13 -// review minor) so it lines up with main.cpp scenario-6's areas[] and the -// README's 1000 m^2 table column is byte-checkable against this CSV -- an -// additive row, not a column, so no schema bump (R15 clause below). +// review minor) so it lines up with main.cpp scenario-6's areas[] and +// docs/target_selection.md's DOCS-WP3-DECAY table (WP15: this table moved +// out of README) is byte-checkable against this CSV -- an additive row, not +// a column, so no schema bump (R15 clause below). const double kAreas[] = {5.0, 10.0, 25.0, 50.0, 100.0, 200.0, 400.0, 800.0, 1000.0, 1600.0, 3200.0}; diff --git a/src/main_metrics.cpp b/src/main_metrics.cpp index 90abc87..83e35c1 100644 --- a/src/main_metrics.cpp +++ b/src/main_metrics.cpp @@ -294,8 +294,9 @@ int main(int argc, char** argv) { } // --- WP12 L4: estimate-driven closed-loop translation guidance (closes - // the WP11/README "estimate-driven translation guidance remains - // open" known limit). Same Config otherwise; only guid_estimate_driven + // the WP11 "estimate-driven translation guidance remains open" known + // limit -- docs/limitations.md, WP15: this limit moved out of + // README). Same Config otherwise; only guid_estimate_driven // flips true, so this is a SEPARATE demo run, not a mutation of the // wp11_rep above. --- { @@ -319,7 +320,8 @@ int main(int argc, char** argv) { } // --- WP12 L5: minimum-impulse-bit (MIB) actuator realization -- retires - // the README "continuous-torque DACS approximation" known limit with + // the "continuous-torque DACS approximation" known limit + // (docs/limitations.md, WP15: this limit moved out of README) with // a MEASURED statement (R14). Neutral-default byte-identity of // run_tumble_sync/ActuatorError is guarded by tests/test_ladder.cpp // and tests/test_campaign.cpp; this demo exercises the NON-neutral diff --git a/src/mission.cpp b/src/mission.cpp index c73d135..71950f9 100644 --- a/src/mission.cpp +++ b/src/mission.cpp @@ -239,7 +239,8 @@ EstimatedSyncReport run_estimated_sync(const Config& cfg, P0_rel.block<3, 3>(3, 3) = s_wt * s_wt * Eigen::Matrix3d::Identity(); // The filter's "known" target inertia is the same (regularized) tensor the // truth target actually propagates with — the known-inertia assumption is - // exact in this simulation and documented as a limitation in the README. + // exact in this simulation and documented as a limitation in + // docs/limitations.md (WP15: this limitation moved out of README). // Initial-error draws are hoisted into named locals: function-argument // evaluation order is unspecified, and the RNG draw order must be fixed // for the run to be reproducible across compilers (R6). diff --git a/tests/test_campaign.cpp b/tests/test_campaign.cpp index 76317f1..113bb1a 100644 --- a/tests/test_campaign.cpp +++ b/tests/test_campaign.cpp @@ -102,8 +102,9 @@ int main() { CHECK(a.sync_time_s == b.sync_time_s); CHECK(a.max_rate_err_deg_s == b.max_rate_err_deg_s); CHECK(a.max_att_err_deg == b.max_att_err_deg); - // The quoted WP2 number (README 16.87 s) must not drift through the - // ActuatorError refactor of run_tumble_sync. + // The quoted WP2 number (docs/gnc.md 16.87 s -- WP15: this moved out + // of README) must not drift through the ActuatorError refactor of + // run_tumble_sync. CHECK(a.synced && std::abs(a.sync_time_s - 16.87) < 0.02); } diff --git a/tests/test_estimator.cpp b/tests/test_estimator.cpp index 4a30e71..7010525 100644 --- a/tests/test_estimator.cpp +++ b/tests/test_estimator.cpp @@ -139,7 +139,8 @@ int main() { } // 4. Estimation-error sanity (generous physical bounds; the exact values - // are demo/README material, regenerated by adsc_sim scenario 7). + // are demo/docs/gnc.md material -- WP15: this moved out of README -- + // regenerated by adsc_sim scenario 7). std::printf("estimator: RMS own att %.4f deg, rel att %.4f deg, " "w_t %.5f deg/s, pos %.3f m, vel %.5f m/s\n", rep.rms_att_own_deg, rep.rms_att_rel_deg, rep.rms_wt_deg_s, diff --git a/tools/docs/fill_docs_numbers.py b/tools/docs/fill_docs_numbers.py new file mode 100644 index 0000000..8c473ee --- /dev/null +++ b/tools/docs/fill_docs_numbers.py @@ -0,0 +1,894 @@ +#!/usr/bin/env python3 +"""docs/ marker-block filler (WP15, R16 mechanism). + + fill_docs_numbers.py [--check] + +R16 (`adsc-specification-v5.md` section 5): files under docs/ may state +numbers only via generated includes or explicit references to evidence +artifacts. This tool is the generated-include mechanism: it regenerates ONLY +the text strictly between each committed `` / +`` marker-comment pair in every `docs/*.md` file, from +the committed CSVs those markers cite. The marker comments themselves (and +everything outside them) are literal, hand-maintained prose and are never +touched by this tool -- exactly the same discipline +`tools/readme/fill_readme_numbers.py` already established for README.md's +WP5-NUMBERS/WP6-NUMBERS blocks (this tool's WP5/WP6 *full-table* builders are +the direct descendants of that script's original build_wp5_block()/ +build_wp6_block(), moved here because the full tables now live in docs/ while +README keeps only a compact digest). + +Blocks filled (source CSV in parentheses): + docs/safety.md DOCS-WP12-LADDER (generated/wp12_ladder.csv) + docs/safety.md DOCS-WP5-FULL (generated/wp5_campaign_summary.csv) + docs/cost_model.md DOCS-WP6-FULL (generated/wp6_cost_summary.csv) + docs/gnc.md DOCS-GNC-WP2 (generated/reference_metrics.csv) + docs/target_selection.md DOCS-WP3-DECAY (generated/wp3_decay_trade.csv) + docs/target_selection.md DOCS-WP13-KIT (generated/wp13_kit_trade.csv) + docs/target_selection.md DOCS-WP13-CLASSC (generated/wp13_classC.csv) + docs/technical_summary_5p.md DOCS-5P-KEY (wp5/wp6/reference_metrics) + +Default mode rewrites each docs/*.md file in place. `--check` instead exits +non-zero if any regenerated block differs from the committed file (for CI: +a stale doc fails the build instead of silently drifting from the CSVs it +claims to summarize -- see ci.yml). + +Python 3 standard library only (spec R9). This tool's own SOURCE stays pure +ASCII (spec R11): the docs' existing Unicode punctuation/math glyphs are +produced via explicit \\uXXXX escapes in ordinary string literals, never as +literal source bytes. No wall-clock timestamps; output is UTF-8 with "\\n" +line endings so the CI byte-reproducibility gate can compare it (matching +every other generator in this repository). +""" +import csv +import os +import re +import sys + +# ---- Unicode glyphs used inside the docs/ blocks, named so the source file +# ---- itself stays ASCII-only (spec R11) while the generated text is not -- +# ---- each is an explicit \uXXXX escape, never a literal byte. +EM = "\u2014" # em dash +MINUS = "\u2212" # minus sign (distinct from ASCII hyphen) +DELTA = "\u0394" # Greek capital delta (used for "Dv") +GE = "\u2265" # >= +LE = "\u2264" # <= +TIMES = "\u00d7" # multiplication sign +SUBI = "\u1d62" # Latin subscript small letter i +SIGMA = "\u03a3" # Greek capital sigma +DOT = "\u00b7" # middle dot +PM = "\u00b1" # plus-minus sign +GG = "\u226b" # much-greater-than +SUP2 = "\u00b2" # superscript 2 (m^2) +APPROX = "\u2248" # almost-equal +DEG = "\u00b0" # degree sign +ETA = "\u03b7" # Greek small letter eta +SQRT = "\u221a" # square root +MU = "\u03bc" # Greek small letter mu +ARROW = "\u2192" # rightwards arrow +SEC = "\u00a7" # section sign + +CATALOG_A = "SL-16 / Zenit-2 second stage" +CATALOG_B = "SL-8 / Kosmos-3M second stage" +CATALOG_C = "Envisat-class massive SSO payload" + +CATALOG_SHORT = {CATALOG_A: "SL-16", CATALOG_B: "SL-8"} +CATALOG_FOM_LABEL = {CATALOG_A: "SL-16 (~9 t, 840 km)", CATALOG_B: "SL-8 (~1.4 t, 750 km)"} +CATALOG_DECAY_LABEL = {CATALOG_A: "SL-16 / Zenit-2", CATALOG_B: "SL-8 / Kosmos-3M"} +CATALOG_LETTER = {CATALOG_A: "A", CATALOG_B: "B", CATALOG_C: "C"} + +CANDIDATE_LABEL = { + "Zenit-2 stage (catalog_A)": "Zenit-2 / SL-16 (catalog A)", + "Envisat-class (catalog_C)": "Envisat-class (catalog C)", +} +CANDIDATE_ORDER = ["Zenit-2 stage (catalog_A)", "Envisat-class (catalog_C)"] + + +def f0(v): + return "%.0f" % float(v) + +def f1(v): + return "%.1f" % float(v) + +def f2(v): + return "%.2f" % float(v) + +def f3(v): + return "%.3f" % float(v) + +def f4(v): + return "%.4f" % float(v) + +def f6(v): + return "%.6f" % float(v) + +def fcomma0(v): + return "{:,.0f}".format(float(v)) + + +def load_csv(path): + with open(path, newline="", encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +# --------------------------------------------------------------------------- +# Data loaders (one per committed CSV this tool reads). +# --------------------------------------------------------------------------- + +class Wp5Data: + """generated/wp5_campaign_summary.csv (schema 1.1).""" + + def __init__(self, root): + self.rows = load_csv(os.path.join(root, "generated", "wp5_campaign_summary.csv")) + self.catalogs = [] + for r in self.rows: + if r["catalog"] not in self.catalogs: + self.catalogs.append(r["catalog"]) + self.n_runs = int(float(self.rows[0]["n_runs"])) + self.master_seed_hex = "0x%X" % int(self.rows[0]["master_seed"]) + + def m(self, catalog, metric): + for r in self.rows: + if r["catalog"] == catalog and r["metric"] == metric: + return r + raise KeyError((catalog, metric)) + + +class Wp6Data: + """generated/wp6_cost_summary.csv (schema 1.1, WP14).""" + + def __init__(self, root): + self.rows = load_csv(os.path.join(root, "generated", "wp6_cost_summary.csv")) + self.schema_version = self.rows[0]["schema_version"] + + def q(self, catalog, record_type, metric, n_kits=None, weighting=None): + for r in self.rows: + if (r["catalog"] == catalog and r["record_type"] == record_type + and r["metric"] == metric + and (n_kits is None or r["n_kits"] == str(n_kits)) + and (weighting is None or r["weighting"] == weighting)): + return r + raise KeyError((catalog, record_type, metric, n_kits, weighting)) + + def first_row(self, catalog, record_type): + for r in self.rows: + if r["catalog"] == catalog and r["record_type"] == record_type: + return r + raise KeyError((catalog, record_type)) + + def anchor(self, scenario): + for r in self.rows: + if (r["record_type"] == "currency_anchor_derived" + and r["metric"] == "anchor_musd_per_cu" + and r["cost_scenario"] == scenario): + return r + raise KeyError(scenario) + + +class RefMetrics: + """generated/reference_metrics.csv (schema 1.0).""" + + def __init__(self, root): + rows = load_csv(os.path.join(root, "generated", "reference_metrics.csv")) + self.d = {r["metric"]: r["value"] for r in rows} + + def v(self, metric): + return float(self.d[metric]) + + +class Wp3Data: + """generated/wp3_decay_trade.csv (schema 1.0).""" + + def __init__(self, root): + self.rows = load_csv(os.path.join(root, "generated", "wp3_decay_trade.csv")) + + def decay_rows(self, catalog): + return [r for r in self.rows + if r["catalog"] == catalog and r["record_type"] == "decay_years"] + + def area25(self, catalog): + for r in self.rows: + if r["catalog"] == catalog and r["record_type"] == "area_for_25yr": + return r + raise KeyError(catalog) + + def meta(self, catalog): + for r in self.rows: + if r["catalog"] == catalog: + return r + raise KeyError(catalog) + + +class Wp13KitData: + """generated/wp13_kit_trade.csv (schema 1.0).""" + + def __init__(self, root): + self.rows = load_csv(os.path.join(root, "generated", "wp13_kit_trade.csv")) + + def row(self, catalog, record_type, kit_option=None): + matches = [r for r in self.rows + if r["catalog"] == catalog and r["record_type"] == record_type + and (kit_option is None or r["kit_option"] == kit_option)] + if len(matches) != 1: + raise RuntimeError( + "expected exactly 1 wp13_kit_trade.csv row for %r, got %d" + % ((catalog, record_type, kit_option), len(matches))) + return matches[0] + + +class Wp13ClassCData: + """generated/wp13_classC.csv (schema 1.0).""" + + def __init__(self, root): + self.rows = load_csv(os.path.join(root, "generated", "wp13_classC.csv")) + + def row(self, candidate, metric): + for r in self.rows: + if r["candidate"] == candidate and r["metric"] == metric: + return r + raise KeyError((candidate, metric)) + + +class Wp12LadderData: + """generated/wp12_ladder.csv (schema 1.0).""" + + def __init__(self, root): + self.rows = load_csv(os.path.join(root, "generated", "wp12_ladder.csv")) + + def ar(self, level, catalog, metric): + for r in self.rows: + if (r["section"] == "abort_reverify" and r["level"] == level + and r["catalog"] == catalog and r["metric"] == metric): + return r + raise KeyError((level, catalog, metric)) + + def dispersion_set(self, level, catalog): + return self.ar(level, catalog, "n_events")["dispersion_set"] + + def forensic_items(self): + return sorted(set((r["catalog"], r["item"]) for r in self.rows + if r["section"] == "forensic14")) + + def forensic_all_clear(self): + return all(float(r["value"]) == 1.0 for r in self.rows + if r["section"] == "forensic14" and r["metric"] == "clears") + + def margin_decay_erosion(self, item): + vals = {} + for r in self.rows: + if (r["section"] == "margin_decay" and r["metric"] == "erosion_m" + and r["item"] == item): + vals[r["catalog"]] = float(r["value"]) + return vals + + +class Data: + def __init__(self, root): + self.d5 = Wp5Data(root) + self.d6 = Wp6Data(root) + self.ref = RefMetrics(root) + self.wp3 = Wp3Data(root) + self.wp13 = Wp13KitData(root) + self.classc = Wp13ClassCData(root) + self.ladder = Wp12LadderData(root) + + +# --------------------------------------------------------------------------- +# Shared cell formatters (mirrors tools/readme/fill_readme_numbers.py). +# --------------------------------------------------------------------------- + +def rate_cell(row): + return "**%s** [%s, %s]" % (f3(row["estimate"]), f3(row["wilson_low"]), + f3(row["wilson_high"])) + + +def rate_plain(row): + return "%s [%s, %s]" % (f3(row["estimate"]), f3(row["wilson_low"]), + f3(row["wilson_high"])) + + +def ko_cell(row): + m = re.search(r"level tag (\S+), dispersion set (\S+)", row["notes"]) + return "%s [%s, %s]" % (rate_cell(row), m.group(1), m.group(2)) + + +def pctl(row, fmt): + return "%s / %s / %s" % (fmt(row["p05"]), fmt(row["p50"]), fmt(row["p95"])) + + +def count(row): + return int(round(float(row["estimate"]))) + + +def amortization_min(d6, catalog): + """Returns (nmin, cost_per_removal_p50_at_nmin, ratio_to_n1_p50_at_nmin).""" + ns = sorted({int(r["n_kits"]) for r in d6.rows + if r["record_type"] == "amortization" and r["metric"] == "cost_per_removal"}) + cpr = {n: float(d6.q(catalog, "amortization", "cost_per_removal", n)["p50"]) for n in ns} + nmin = min(ns, key=lambda n: cpr[n]) + ratio = float(d6.q(catalog, "amortization", "cost_per_removal_ratio_to_n1", nmin)["p50"]) + return nmin, cpr[nmin], ratio + + +# --------------------------------------------------------------------------- +# Block builders. +# --------------------------------------------------------------------------- + +def build_wp5_full(data): + """docs/safety.md DOCS-WP5-FULL: the full WP5 campaign table (the + original tools/readme/fill_readme_numbers.py build_wp5_block(), moved + here verbatim -- README now carries only a compact digest of the same + CSV, built by tools/readme/fill_readme_numbers.py's own, smaller + template).""" + d5 = data.d5 + A, B = d5.catalogs[0], d5.catalogs[1] + n_runs = d5.n_runs + + succA, succB = d5.m(A, "success_rate"), d5.m(B, "success_rate") + nptA, nptB = (d5.m(A, "nonproductive_termination_rate"), + d5.m(B, "nonproductive_termination_rate")) + gaA, gaB = d5.m(A, "gate_abort_run_rate"), d5.m(B, "gate_abort_run_rate") + koA, koB = (d5.m(A, "keep_out_violation_rate"), + d5.m(B, "keep_out_violation_rate")) + dvA, dvB = d5.m(A, "dv_used_m_per_s"), d5.m(B, "dv_used_m_per_s") + kuA, kuB = d5.m(A, "kits_used"), d5.m(B, "kits_used") + rmA, rmB = (d5.m(A, "removals_per_mission"), d5.m(B, "removals_per_mission")) + syA, syB = d5.m(A, "sync_arrival_time_s"), d5.m(B, "sync_arrival_time_s") + + dvexA, dvexB = count(d5.m(A, "dv_exhausted")), count(d5.m(B, "dv_exhausted")) + kitexA, kitexB = (count(d5.m(A, "kit_exhausted")), count(d5.m(B, "kit_exhausted"))) + koCountA, koCountB = (count(d5.m(A, "keep_out_violation")), + count(d5.m(B, "keep_out_violation"))) + compA, compB = count(d5.m(A, "completed")), count(d5.m(B, "completed")) + gaCountA, gaCountB = (count(d5.m(A, "gate_abort")), count(d5.m(B, "gate_abort"))) + stCountA, stCountB = (count(d5.m(A, "sync_timeout")), count(d5.m(B, "sync_timeout"))) + + m = re.search(r"level tag (\S+), dispersion set (\S+)", koA["notes"]) + l0tag, dstag = m.group(1), m.group(2) + dv_limited_pct = int(round(100.0 * dvexA / n_runs)) + + L = [] + L.append("The full N = %d campaign (both catalog presets) runs in a few tens of seconds" + % n_runs) + L.append("on a CI runner (see the Actions log of the current run for the actual figure).") + L.append("`success` here means a **productive end** %s the mission installed its" + % EM) + L.append("full 4-kit complement (`kit_exhausted`) or cleared all 6 targets (`completed`) %s" + % EM) + L.append("not one cut short by %sv exhaustion or a keep-out violation." % DELTA) + L.append("") + L.append("| metric | SL-16 / Zenit-2 class | SL-8 / Kosmos-3M class |") + L.append("|---|---|---|") + L.append("| success rate | %s | %s |" % (rate_cell(succA), rate_cell(succB))) + L.append("| nonproductive-termination rate (= 1 %s success) | %s | %s |" + % (MINUS, rate_cell(nptA), rate_cell(nptB))) + L.append("| gate-abort-run rate (abort-path exposure) | %s | %s |" + % (rate_cell(gaA), rate_cell(gaB))) + L.append("| keep-out-violation rate | %s | %s |" % (ko_cell(koA), ko_cell(koB))) + L.append("| %sv used p05/p50/p95 [m/s] | %s | %s |" + % (DELTA, pctl(dvA, f0), pctl(dvB, f0))) + L.append("| kits used p05/p50/p95 | %s | %s |" % (pctl(kuA, f0), pctl(kuB, f0))) + L.append("| removals/mission p05/p50/p95 | %s | %s |" % (pctl(rmA, f0), pctl(rmB, f0))) + L.append("| sync arrival p05/p50/p95 [s] | %s | %s |" % (pctl(syA, f2), pctl(syB, f2))) + L.append("| failure counts (runs) | dv_exhausted %d, kit_exhausted %d, " + "keep_out %d, completed %d | dv_exhausted %d, kit_exhausted " + "%d, keep_out %d, completed %d |" + % (dvexA, kitexA, koCountA, compA, dvexB, kitexB, koCountB, compB)) + L.append("| per-target events | gate_abort %d, sync_timeout %d | " + "gate_abort %d, sync_timeout %d |" + % (gaCountA, stCountA, gaCountB, stCountB)) + L.append("") + L.append("Two abort-related rates are reported and are deliberately distinct:") + L.append("**`gate-abort-run rate`** is the abort-path exposure (fraction of runs with %s 1" + % GE) + L.append("closing-speed gate abort %s what the spec calls the \"abort rate\"), while" % EM) + L.append("**`nonproductive-termination rate`** is 1 %s success. Under the current *flat" + % MINUS) + L.append("PLACEHOLDER* %sv cost the two coincide numerically %s every aborting mission needs" + % (DELTA, EM)) + L.append("an extra target-slot to still install its kits and so exhausts the 140 m/s") + L.append("budget %s but they are separate concepts and will diverge once the cost model" + % EM) + L.append("gains structure. The honest campaign finding: the servicer is **%sv-limited about" + % DELTA) + L.append("%d%% of the time** (`dv_exhausted` = %d/%d) and installs its full kit" + % (dv_limited_pct, dvexA, n_runs)) + L.append("complement the rest; the attitude sync **never** times out across the sampled") + L.append("tumble/attitude/actuator dispersions, and keep-out violations are **%d of %d**" + % (koCountA, n_runs)) + L.append("per catalog [%s: linear CW, dispersion set %s, Wilson 95%% upper bound" + % (l0tag, dstag)) + L.append("%s] %s the WP11 clearing-abort law accepts an abort only when the analytic" + % (f4(koA["wilson_high"]), EM)) + L.append("post-burn ellipse clears keep-out plus a design margin (mechanism forensics:") + L.append("`generated/wp10_violation_forensics.md`; audit:") + L.append("`generated/wp11_abort_audit.md`). `completed` (all 6 targets) is 0 by") + L.append("construction %s 4 kits cannot service 6 targets. The `%sv used` / `kits used`" + % (EM, DELTA)) + L.append("percentiles matching across the two presets is expected (a flat PLACEHOLDER cost") + L.append("takes quantized, catalog-independent values %s not a copy-paste bug). Full per-run" + % EM) + L.append("records and the column schema are in [generated/](../generated/).") + return "\n".join(L) + + +def build_wp6_full(data): + """docs/cost_model.md DOCS-WP6-FULL: the full WP6 amortization/FoM/tornado + table (the original build_wp6_block(), moved here; the schema label now + reads the CSV's own schema_version instead of the template's former + hardcoded, stale "1.0" -- the WP15 content-mapping pass found and fixed + that drift, per R16's whole point).""" + d5, d6 = data.d5, data.d6 + A, B = d5.catalogs[0], d5.catalogs[1] + Ashort, Bshort = CATALOG_SHORT[A], CATALOG_SHORT[B] + n_runs = d5.n_runs + + ns = sorted({int(r["n_kits"]) for r in d6.rows + if r["record_type"] == "amortization" and r["metric"] == "cost_per_removal"}) + cprA = {n: float(d6.q(A, "amortization", "cost_per_removal", n)["p50"]) for n in ns} + cprB = {n: float(d6.q(B, "amortization", "cost_per_removal", n)["p50"]) for n in ns} + nmin = min(ns, key=lambda n: cprA[n]) + nmax = max(ns) + display_ns = sorted(set(n for n in (1, 2, 3, nmin, nmin + 1, nmax) if n in ns)) + + baseline_n = int(d6.first_row(A, "cost_component")["n_kits"]) + + ratio_min = float(d6.q(A, "amortization", "cost_per_removal_ratio_to_n1", nmin)["p50"]) + pct_min = ratio_min * 100.0 + saving_x = 1.0 / ratio_min + catalog_gap_pct = abs(cprA[nmin] - cprB[nmin]) / cprA[nmin] * 100.0 + + fomA_sp = d6.q(A, "fom", "fom", weighting="spatial_density") + fomA_cr = d6.q(A, "fom", "fom", weighting="criticality") + fomB_sp = d6.q(B, "fom", "fom", weighting="spatial_density") + fomB_cr = d6.q(B, "fom", "fom", weighting="criticality") + wA_sp = d6.q(A, "fom", "band_weight", weighting="spatial_density") + wA_cr = d6.q(A, "fom", "band_weight", weighting="criticality") + wB_sp = d6.q(B, "fom", "band_weight", weighting="spatial_density") + wB_cr = d6.q(B, "fom", "band_weight", weighting="criticality") + + tornado = [r for r in d6.rows if r["catalog"] == A + and r["record_type"] == "tornado" and r["metric"] == "cost_per_removal_swing"] + if len(tornado) != 5: + raise RuntimeError( + "expected exactly 5 tornado cost_per_removal_swing rows for %r, " + "got %d -- the DOCS-WP6-FULL template is hardcoded for a " + "5-parameter, 2-line wrap and needs a human rewrite if the " + "parameter count changes" % (A, len(tornado))) + items = [(r["param"], float(r["p50"])) for r in tornado] + + L = [] + L.append("The full sweep (%d runs %s 2 catalogs %s N = %d..%d kits) runs in a few tens of" + % (n_runs, TIMES, TIMES, ns[0], nmax)) + L.append("seconds on a CI runner (see the Actions log of the current run for the actual") + L.append("figure), and the baseline (N = %d) removals reproduce the committed WP5 CSV" + % baseline_n) + L.append("exactly (`MATCH (schema %s consumed)`)." % d6.schema_version) + L.append("") + L.append("**Amortization curve %s cost/removal vs kits carried N (%s class):**" + % (EM, Ashort)) + L.append("") + L.append("| N | cost/removal p50 [CU] | ratio to N=1 | removals p50 |") + L.append("|---:|---:|---:|---:|") + for n in display_ns: + cpr = float(d6.q(A, "amortization", "cost_per_removal", n)["p50"]) + ratio = float(d6.q(A, "amortization", "cost_per_removal_ratio_to_n1", n)["p50"]) + rem = float(d6.q(A, "amortization", "removals", n)["p50"]) + if n == nmin: + L.append("| **%d** | **%s** | **%s** | %s |" % (n, f2(cpr), f3(ratio), f0(rem))) + else: + L.append("| %d | %s | %s | %s |" % (n, f2(cpr), f3(ratio), f0(rem))) + L.append("") + L.append("Batch amortization drives cost/removal down to **%s %% of the single-target" + % f1(pct_min)) + L.append("(N = %d) baseline** %s a **%s%s per-removal saving** %s bottoming at N = %d where the" + % (ns[0], EM, f1(saving_x), TIMES, EM, nmin)) + L.append("**%sv budget (not the kit count)** caps removals at %d; carrying more kits then" + % (DELTA, nmin)) + L.append("adds cost without removals and the curve turns back up. That shape *is* the") + L.append("quantitative installer/batch argument (%s class is within ~%s %%)." + % (Bshort, f1(catalog_gap_pct))) + L.append("") + L.append("**FoM = %s m%s%sw(h%s)/C_campaign at N = %d (p50, kg/CU):**" + % (SIGMA, SUBI, DOT, SUBI, baseline_n)) + L.append("") + L.append("| catalog | spatial-density | criticality |") + L.append("|---|---:|---:|") + L.append("| %s | %s (w = %s) | %s (w = %s) |" + % (CATALOG_FOM_LABEL[A], f2(fomA_sp["p50"]), f2(wA_sp["estimate"]), + f2(fomA_cr["p50"]), f2(wA_cr["estimate"]))) + L.append("| %s | %s (w = %s) | %s (w = %s) |" + % (CATALOG_FOM_LABEL[B], f2(fomB_sp["p50"]), f2(wB_sp["estimate"]), + f2(fomB_cr["p50"]), f2(wB_cr["estimate"]))) + L.append("") + L.append("The ~9 t class outranks the ~1.4 t class under **both** weightings (FoM is") + L.append("mass-dominated), but the **band weight flips** %s spatial density values the" + % EM) + L.append("750 km band more, the criticality index values the 840 km band more %s a genuine" + % EM) + L.append("metric-choice disagreement tracked as **open trade T5**. **Tornado** (%s," + % Ashort) + L.append("%s30 %%, ranked by cost/removal swing): `%s` (%s CU) %s `%s` (%s) >" + % (PM, items[0][0], f1(items[0][1]), GG, items[1][0], f1(items[1][1]))) + L.append("`%s` (%s) > `%s` (%s) > `%s` (%s). All" + % (items[2][0], f1(items[2][1]), items[3][0], f1(items[3][1]), + items[4][0], f1(items[4][1]))) + L.append("values are relative CU; **no absolute-dollar figure is claimed**.") + return "\n".join(L) + + +def build_wp12_ladder(data): + """docs/safety.md DOCS-WP12-LADDER: abort-event re-verification table + + forensic-14/margin-decay summary from generated/wp12_ladder.csv.""" + ladder = data.ladder + catalogs = [CATALOG_A, CATALOG_B] + levels = ["L0", "L1", "L2"] + + n_events = {c: int(round(float(ladder.ar("L0", c, "n_events")["value"]))) + for c in catalogs} + seed = data.d5.master_seed_hex + + L = [] + L.append("Abort-event re-verification (%d catalog-A + %d catalog-B closing-speed-gate" + % (n_events[CATALOG_A], n_events[CATALOG_B])) + L.append("events from the committed campaign; master seed `%s`, same" % seed) + L.append("draws as `wp5_campaign_runs.csv` for `ds-v1`):") + L.append("") + L.append("| level | dispersion set | catalog | n_events | n_violations | " + "violation_rate | wilson 95% CI | clearance floor (m) | p05 (m) | p50 (m) |") + L.append("|---|---|---|---:|---:|---:|---|---:|---:|---:|") + floor = {} + for catalog in catalogs: + for level in levels: + nev = ladder.ar(level, catalog, "n_events") + nvi = ladder.ar(level, catalog, "n_violations") + vr = ladder.ar(level, catalog, "violation_rate") + wlo = ladder.ar(level, catalog, "wilson_low") + whi = ladder.ar(level, catalog, "wilson_high") + cf = ladder.ar(level, catalog, "clearance_floor_m") + cp05 = ladder.ar(level, catalog, "clearance_p05_m") + cp50 = ladder.ar(level, catalog, "clearance_p50_m") + floor[(catalog, level)] = float(cf["value"]) + L.append("| %s | %s | %s | %s | %s | %s | [%s, %s] | %s | %s | %s |" + % (level, ladder.dispersion_set(level, catalog), catalog, + f0(nev["value"]), f0(nvi["value"]), f6(vr["value"]), + f6(wlo["value"]), f6(whi["value"]), f4(cf["value"]), + f4(cp05["value"]), f4(cp50["value"]))) + L.append("") + + items = ladder.forensic_items() + n_cases = len(items) + if not ladder.forensic_all_clear(): + raise RuntimeError( + "DOCS-WP12-LADDER template is hardcoded for an all-clear " + "forensic-14 result and needs a human rewrite -- a violation " + "was found in generated/wp12_ladder.csv") + + erosion = {c: floor[(c, "L0")] - floor[(c, "L2")] for c in catalogs} + tighter = min(catalogs, key=lambda c: floor[(c, "L2")]) + erosion_lo, erosion_hi = sorted(erosion.values()) + + L.append("Forensic-14 per-level clearance: all %d of %d pinned cases clear keep-out at" + % (n_cases, n_cases)) + L.append("**L0, L1 and L2** %s the anticipated CW-safe-but-higher-level-unsafe case does" + % EM) + L.append("**not** materialize for this dispersion set: the WP11 clearance margin") + L.append("absorbs the measured ~%s-%s m of J2+drag coast erosion (clearance floor" + % (f1(erosion_lo), f1(erosion_hi))) + L.append("%s m at L0 %s %s m at L2, %s class, the tighter of the two). A" + % (f1(floor[(tighter, "L0")]), ARROW, f1(floor[(tighter, "L2")]), + CATALOG_SHORT[tighter])) + L.append("negative-negative result, reported with the same discipline as a positive") + L.append("one. Full 14-row per-case table: `generated/wp12_ladder.md`.") + L.append("") + + worst_erosion = max(ladder.margin_decay_erosion("standoff_400m/orbit=5").values()) + L.append("Safety-ellipse margin decay under J2 (the F2 caveat, promised qualitatively") + L.append("since WP1, now measured): worst 5-orbit min-range erosion **%s m** on the" + % f2(worst_erosion)) + L.append("400 m standoff ellipse [L1: two-body+J2]. Full per-orbit table for both") + L.append("geometries and both catalogs: `generated/wp12_ladder.md`.") + return "\n".join(L) + + +def build_gnc_wp2(data): + """docs/gnc.md DOCS-GNC-WP2: the WP2 sync-time/dwell-error pins.""" + ref = data.ref + L = [] + L.append("| metric | value | tolerance |") + L.append("|---|---:|---:|") + L.append("| sync time | **%s s** | criteria first held, then dwelled 30 s |" + % f2(ref.v("wp2_sync_time_s"))) + L.append("| max \\|w_rel\\| after dwell | **%s deg/s** | tol 0.1 deg/s |" + % f6(ref.v("wp2_max_rate_after_dwell_deg_s"))) + L.append("| max attitude error after dwell | **%s%s** | tol 2.0%s |" + % (f6(ref.v("wp2_max_att_after_dwell_deg")), DEG, DEG)) + return "\n".join(L) + + +def build_wp3_decay(data): + """docs/target_selection.md DOCS-WP3-DECAY: the sail-area x solar-activity + decay sweep for catalogs A and B, plus the 25-yr guideline areas.""" + wp3 = data.wp3 + L = [] + for catalog, comment in ((CATALOG_A, ("impractical at the upper end.",)), + (CATALOG_B, ("tens of", "square meters, a practical kit."))): + meta = wp3.meta(catalog) + L.append("**Catalog %s %s %s, %s%s kg, %s km:**" + % (CATALOG_LETTER[catalog], EM, CATALOG_DECAY_LABEL[catalog], + APPROX, fcomma0(meta["mass_kg"]), f0(meta["altitude_km"]))) + L.append("") + L.append("| sail area [m%s] | decay time, solar max [yr] | decay time, solar min [yr] |" + % SUP2) + L.append("|---:|---:|---:|") + for r in wp3.decay_rows(catalog): + L.append("| %s | %s | %s |" % (f0(r["sail_area_m2"]), f1(r["value_solar_max"]), + f1(r["value_solar_min"]))) + L.append("") + area = wp3.area25(catalog) + if len(comment) == 1: + L.append("Sail area for the 25-year guideline (solar max..min): **%s..%s m%s** %s" + % (f0(area["value_solar_max"]), f0(area["value_solar_min"]), SUP2, EM)) + L.append(comment[0]) + else: + L.append("Sail area for the 25-year guideline (solar max..min): **%s..%s m%s** %s %s" + % (f0(area["value_solar_max"]), f0(area["value_solar_min"]), SUP2, EM, + comment[0])) + L.append(comment[1]) + L.append("") + L.append("25-year figure is the **IADC guideline** (IADC-02-01, Space Debris Mitigation") + L.append("Guidelines, guideline 5.3.2). Under a US FCC license the current rule (FCC") + L.append("22-74, adopted 2022) requires **5-year** post-mission disposal for LEO space") + L.append("stations instead %s the class-A sail-only negative gets strictly harder under" + % EM) + L.append("that standard. Both rules are carried separately in the WP8 compliance") + L.append("rulepacks (see [docs/legal_regulatory.md](legal_regulatory.md)); this trade") + L.append("keeps the IADC 25-year reference line.") + return "\n".join(L) + + +def build_wp13_kit(data): + """docs/target_selection.md DOCS-WP13-KIT: per-class kit-trade summary + + mass/deploy-risk/EMF table.""" + wp13 = data.wp13 + catalogs = [CATALOG_A, CATALOG_B, CATALOG_C] + + L = [] + L.append("| catalog | recommended kit | sail area, 25-yr [m%s] (max..min) | " + "EDT deorbit time [yr] | EDT %s(i) band |" % (SUP2, ETA)) + L.append("|---|---|---:|---:|---:|") + for c in catalogs: + sail = wp13.row(c, "sail_area_25yr") + edt_years = wp13.row(c, "edt_years") + edt_eta = wp13.row(c, "edt_eta") + rec = wp13.row(c, "recommended_kit") + L.append("| %s %s %s | %s | %s..%s | %s..%s | %s..%s |" + % (CATALOG_LETTER[c], EM, c, rec["kit_option"], + f0(sail["value_lo"]), f0(sail["value_hi"]), + f1(edt_years["value_lo"]), f1(edt_years["value_hi"]), + f3(edt_eta["value_lo"]), f3(edt_eta["value_hi"]))) + L.append("") + L.append("Kit mass, deploy risk, and diagnostic EMF/power (same source CSV):") + L.append("") + L.append("| catalog | sail kit mass [kg] | EDT kit mass [kg] (PLACEHOLDER) | " + "EDT deploy-failure risk (PLACEHOLDER) | EDT open-circuit EMF [V] |") + L.append("|---|---:|---:|---:|---:|") + for c in catalogs: + kit_sail = wp13.row(c, "kit_mass", "sail") + kit_edt = wp13.row(c, "kit_mass", "edt") + deploy = wp13.row(c, "deploy_risk", "edt") + emf = wp13.row(c, "edt_emf_power", "edt") + L.append("| %s | %s | %s | %s | %s |" + % (CATALOG_LETTER[c], f1(kit_sail["value"]), f1(kit_edt["value"]), + f2(deploy["value"]), f1(emf["value"]))) + L.append("") + L.append("Deploy risk is quoted for the record and is **not** folded into the") + L.append("EDT-years band above.") + return "\n".join(L) + + +def build_wp13_classc(data): + """docs/target_selection.md DOCS-WP13-CLASSC: controlled-deorbit delta-v + comparison for the Class-C candidates.""" + classc = data.classc + L = [] + L.append("Controlled-deorbit %sv (COMPUTED): a single impulsive perigee-lowering burn" + % DELTA) + L.append("from the catalog's circular orbit (radius r_a) to a target perigee r_p =") + L.append("6371+40 km (mean-Earth-radius convention), dv = v_c%s(1 %s %s(2%sr_p/(r_a+r_p)))," + % (DOT, MINUS, SQRT, DOT)) + L.append("v_c = %s(%s/r_a). This is the minimum single-burn %sv to commit the stage to" + % (SQRT, MU, DELTA)) + L.append("reentry; it excludes targeting/footprint-control burns and any margin.") + L.append("") + L.append("| candidate | v_c [m/s] | %sv [m/s] |" % DELTA) + L.append("|---|---:|---:|") + for candidate in CANDIDATE_ORDER: + vc = classc.row(candidate, "v_circular_initial") + dv = classc.row(candidate, "controlled_deorbit_dv") + L.append("| %s | %s | **%s** |" + % (CANDIDATE_LABEL[candidate], f1(vc["value"]), f1(dv["value"]))) + return "\n".join(L) + + +def build_5p_key(data): + """docs/technical_summary_5p.md DOCS-5P-KEY: the pinned key-numbers table, + the same headline figures as README's WP5-NUMBERS/WP6-NUMBERS digest plus + a few GNC pins, in plain (non-bulleted) prose-table form.""" + d5, d6, ref = data.d5, data.d6, data.ref + A, B = d5.catalogs[0], d5.catalogs[1] + + succA, succB = d5.m(A, "success_rate"), d5.m(B, "success_rate") + koA = d5.m(A, "keep_out_violation_rate") + koCountA = count(d5.m(A, "keep_out_violation")) + + nmin, cpr_min, ratio_min = amortization_min(d6, A) + fomA_sp = d6.q(A, "fom", "fom", weighting="spatial_density") + fomA_cr = d6.q(A, "fom", "fom", weighting="criticality") + anchor_low = float(d6.anchor("low")["p50"]) + anchor_mid = float(d6.anchor("mid")["p50"]) + anchor_high = float(d6.anchor("high")["p50"]) + cost_low = cpr_min * anchor_low + cost_mid = cpr_min * anchor_mid + cost_high = cpr_min * anchor_high + + L = [] + L.append("| metric | value | fidelity / notes | source |") + L.append("|---|---|---|---|") + L.append("| Campaign success (productive end), SL-16 / SL-8 | %s / %s (Wilson 95%%) | " + "L0, ds-v1 | `generated/wp5_campaign_summary.csv` |" + % (rate_plain(succA), rate_plain(succB))) + L.append("| Keep-out violations, both classes | %s / %s, Wilson UB %s %s | L0, ds-v1; " + "zero re-verified over every re-verified abort event at L1/L2 | " + "`generated/wp5_campaign_summary.csv`, `generated/wp12_ladder.csv` |" + % (f0(koCountA), f0(d5.n_runs), LE, f4(koA["wilson_high"]))) + L.append("| Amortization minimum (N = %d) | %s CU/removal = %s%s the N = 1 baseline | " + "%sv-limited, measured | `generated/wp6_cost_summary.csv` |" + % (nmin, f2(cpr_min), f3(ratio_min), TIMES, DELTA)) + L.append("| FoM p50, SL-16 class (spatial / criticality) | %s / %s kg/CU | two " + "independent weightings, disagreement tracked (T5) | " + "`generated/wp6_cost_summary.csv` |" + % (f2(fomA_sp["p50"]), f2(fomA_cr["p50"]))) + L.append("| Cost/removal p50, SL-16 class (low / mid / high) | %s / %s / %s MUSD | " + "external anchor, cited range only (D10) | `generated/wp6_cost_summary.csv` " + "(WP14 rows) |" + % (f2(cost_low), f2(cost_mid), f2(cost_high))) + L.append("| Attitude sync (truth-driven pin) | %s s | L0 | `reference_metrics.csv` |" + % f2(ref.v("wp2_sync_time_s"))) + L.append("| Attitude sync, estimate-driven under sensor noise | %s s | L0, " + "estimator-in-the-loop | `reference_metrics.csv` |" + % f2(ref.v("wp4_sync_time_s"))) + L.append("| Guided-approach demo: contact speed / total %sv | %s m/s (gate %s m/s) / " + "%s m/s | L0, truth-fed guidance | evidence pack %s3, `reference_metrics.csv` |" + % (DELTA, f2(ref.v("wp11_guided_contact_speed_m_s")), + f2(ref.v("wp3_contact_speed_m_s")), f2(ref.v("wp11_guided_dv_total_m_s")), + SEC)) + L.append("") + L.append("*This block is generated by `tools/docs/fill_docs_numbers.py` from the same") + L.append("committed CSVs as README's WP5-NUMBERS/WP6-NUMBERS blocks and") + L.append("`reference_metrics.csv`; run with `--check` to verify, in-place to refresh.*") + return "\n".join(L) + + +# --------------------------------------------------------------------------- +# Marker registry: (doc-relative-path, start marker, end marker, builder). +# --------------------------------------------------------------------------- + +REGISTRY = [ + ("docs/safety.md", + "", + "", + build_wp12_ladder), + ("docs/safety.md", + "", + "", + build_wp5_full), + ("docs/cost_model.md", + "", + "", + build_wp6_full), + ("docs/gnc.md", + "", + "", + build_gnc_wp2), + ("docs/target_selection.md", + "", + "", + build_wp3_decay), + ("docs/target_selection.md", + "", + "", + build_wp13_kit), + ("docs/target_selection.md", + "", + "", + build_wp13_classc), + ("docs/technical_summary_5p.md", + "", + "", + build_5p_key), +] + + +def replace_block(text, start_marker, end_marker, new_body): + start_idx = text.index(start_marker) + end_idx = text.index(end_marker, start_idx) + before = text[:start_idx + len(start_marker)] + after = text[end_idx:] + return before + "\n" + new_body + "\n" + after + + +def _first_diff_line(a, b): + la, lb = a.split("\n"), b.split("\n") + for i, (x, y) in enumerate(zip(la, lb)): + if x != y: + return i + 1, x, y + if len(la) != len(lb): + return min(len(la), len(lb)) + 1, "", "" + return None + + +def main(): + argv = sys.argv[1:] + check_mode = "--check" in argv + positional = [a for a in argv if a != "--check"] + if not positional: + print("usage: fill_docs_numbers.py [--check]", file=sys.stderr) + return 2 + root = os.path.abspath(positional[0]) + + data = Data(root) + + by_file = [] + for relpath, start, end, builder in REGISTRY: + if not by_file or by_file[-1][0] != relpath: + by_file.append((relpath, [])) + by_file[-1][1].append((start, end, builder)) + + any_stale = False + for relpath, blocks in by_file: + path = os.path.join(root, *relpath.split("/")) + with open(path, encoding="utf-8") as f: + original = f.read() + new_text = original + for start, end, builder in blocks: + new_text = replace_block(new_text, start, end, builder(data)) + + if check_mode: + if new_text == original: + print("[docs_numbers] %s marker blocks match the committed CSVs" % relpath) + else: + any_stale = True + print("[docs_numbers] %s marker blocks are STALE vs the committed CSVs" + % relpath, file=sys.stderr) + diff = _first_diff_line(original, new_text) + if diff: + lineno, old, new = diff + print(" first difference at line %d:" % lineno, file=sys.stderr) + print(" committed : %r" % old, file=sys.stderr) + print(" from CSVs : %r" % new, file=sys.stderr) + else: + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.write(new_text) + if new_text == original: + print("[docs_numbers] %s marker blocks already up to date" % relpath) + else: + print("[docs_numbers] %s marker blocks rewritten" % relpath) + + if check_mode: + return 1 if any_stale else 0 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/docs/test_docs_numbers.py b/tools/docs/test_docs_numbers.py new file mode 100644 index 0000000..7699537 --- /dev/null +++ b/tools/docs/test_docs_numbers.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""docs/ marker-block filler test (registered with ctest as `docs_numbers`). + + test_docs_numbers.py + +Mirrors tools/readme/test_readme_numbers.py's discipline for the docs/ +DOCS-*-START/END marker blocks filled by fill_docs_numbers.py. Checks +(non-zero exit on any failure, R4): + 1. --check mode passes against the real, committed docs/*.md files -- i.e. + every DOCS-*-START/END marker block already reproduces byte-for-byte + from the committed CSVs it cites (R16: docs/ must not drift from the + generated artifacts it summarizes without this test catching it). + 2. --check mode FAILS when a number inside a marker block is corrupted in + a scratch copy -- proves the check reads block content, not just + marker-comment presence. + 3. Determinism: default (rewrite) mode is idempotent on an already-correct + scratch copy (content-equal, ignoring the CRLF/LF checkout-dependent + line-ending representation the same way test_evidence.py does), and two + independent regenerations from the same inputs are byte-identical. +""" +import hashlib +import os +import shutil +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# The docs/*.md files fill_docs_numbers.py rewrites, and the generated/ CSVs +# it reads -- kept in sync with fill_docs_numbers.py's own REGISTRY / Data +# loaders. Listed explicitly (rather than introspected) so this test fails +# loudly if the two files drift apart instead of silently under-testing. +DOC_FILES = [ + "docs/safety.md", + "docs/cost_model.md", + "docs/gnc.md", + "docs/target_selection.md", + "docs/technical_summary_5p.md", +] +CSV_FILES = [ + "wp5_campaign_summary.csv", + "wp6_cost_summary.csv", + "reference_metrics.csv", + "wp3_decay_trade.csv", + "wp13_kit_trade.csv", + "wp13_classC.csv", + "wp12_ladder.csv", +] + +fails = [] +def check(cond, msg): + if not cond: + fails.append(msg) + + +def sha_all(root): + h = hashlib.sha256() + for relpath in DOC_FILES: + with open(os.path.join(root, *relpath.split("/")), "rb") as f: + h.update(f.read()) + return h.hexdigest() + + +def make_scratch_repo(repo): + """Copy just the inputs fill_docs_numbers.py needs (the docs/*.md files + plus the committed CSVs they cite) into a throwaway directory, so the + corruption test can mutate a copy without touching the real working + tree.""" + tmp = tempfile.mkdtemp(prefix="wp15_docs_") + os.makedirs(os.path.join(tmp, "docs")) + os.makedirs(os.path.join(tmp, "generated")) + for relpath in DOC_FILES: + shutil.copy(os.path.join(repo, *relpath.split("/")), + os.path.join(tmp, *relpath.split("/"))) + for name in CSV_FILES: + shutil.copy(os.path.join(repo, "generated", name), + os.path.join(tmp, "generated", name)) + return tmp + + +def run_tool(root, *args): + return subprocess.run( + [sys.executable, os.path.join(HERE, "fill_docs_numbers.py"), root] + + list(args), + capture_output=True, text=True) + + +def main(): + if len(sys.argv) < 2: + print("usage: test_docs_numbers.py ", file=sys.stderr) + return 2 + repo = os.path.abspath(sys.argv[1]) + + # 1. --check against the real, committed docs/*.md files must pass. + r = run_tool(repo, "--check") + check(r.returncode == 0, + "fill_docs_numbers.py --check failed against the committed docs/: " + "%s" % (r.stdout + r.stderr).strip()) + + # 2. Corrupt a pinned number inside a marker block in a scratch copy; + # --check must now fail. Uses the WP6-FULL block's amortization- + # minimum figure (docs/cost_model.md), the docs/ home of the same + # "44.80" pin README's own digest carries. + tmp1 = make_scratch_repo(repo) + try: + cost_path = os.path.join(tmp1, "docs", "cost_model.md") + with open(cost_path, encoding="utf-8") as f: + text = f.read() + check("44.80" in text, + "fixture assumption stale: pinned number '44.80' not found " + "in docs/cost_model.md") + corrupted = text.replace("44.80", "99.99", 1) + check(corrupted != text, "corruption was a no-op -- fixture stale") + with open(cost_path, "w", encoding="utf-8", newline="\n") as f: + f.write(corrupted) + r2 = run_tool(tmp1, "--check") + check(r2.returncode != 0, + "--check did not catch a corrupted number in a marker block") + finally: + shutil.rmtree(tmp1, ignore_errors=True) + + # 3. Determinism: rewrite-mode is idempotent on an already-correct copy + # (content-equal; CRLF/LF normalized via text-mode read, exactly like + # test_evidence.py's committed-vs-regenerated comparison), and two + # independent regenerations of the same inputs are byte-identical. + tmp2 = make_scratch_repo(repo) + try: + before_texts = {} + for relpath in DOC_FILES: + with open(os.path.join(tmp2, *relpath.split("/")), encoding="utf-8") as f: + before_texts[relpath] = f.read() + r3 = run_tool(tmp2) + check(r3.returncode == 0, + "default-mode run failed: %s" % (r3.stdout + r3.stderr).strip()) + for relpath in DOC_FILES: + with open(os.path.join(tmp2, *relpath.split("/")), encoding="utf-8") as f: + after_text = f.read() + check(before_texts[relpath] == after_text, + "rewriting an already-correct %s changed its content " + "(not idempotent)" % relpath) + after_sha = sha_all(tmp2) + r4 = run_tool(tmp2) + check(r4.returncode == 0, "second default-mode run failed: %s" + % (r4.stdout + r4.stderr).strip()) + after2_sha = sha_all(tmp2) + check(after_sha == after2_sha, + "two regenerations from the same inputs are not " + "byte-identical (nondeterministic)") + finally: + shutil.rmtree(tmp2, ignore_errors=True) + + if fails: + for m in fails: + print("FAIL:", m) + print("docs_numbers: %d failure(s)" % len(fails)) + return 1 + print("docs_numbers: all checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/evidence/test_evidence.py b/tools/evidence/test_evidence.py index 97c98bc..97307fd 100644 --- a/tools/evidence/test_evidence.py +++ b/tools/evidence/test_evidence.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -"""WP7 evidence-pack claim audit (registered with ctest as `evidence`). +"""WP7 evidence-pack claim audit (registered with ctest as `evidence`); +extended in WP15 (R16) to also audit docs/*.md. test_evidence.py @@ -16,6 +17,11 @@ 4. Determinism: two generator runs are byte-identical (SHA-256), and the regenerated pack matches the committed evidence/adsc_evidence_pack.md. 5. Hygiene: ASCII only, no timestamp markers, referenced SVG figures exist. + 6. (WP15/R16) docs/*.md: the same banned-word scan as (1) extends to every + docs/*.md file; every spec-mandated docs/ file exists; the R15 pin list + (the WP15 content map's headline pinned numbers) never appears as bare, + hand-typed prose outside a DOCS-*-START/END generated-include block -- + see scan_docs_r16()'s own docstring for the exact enforcement scope. """ import csv import hashlib @@ -43,6 +49,209 @@ def load_csv(path): return list(csv.DictReader(f)) +# --------------------------------------------------------------------------- +# WP15 (R16): docs/ claims-audit extension. +# --------------------------------------------------------------------------- + +# Every docs/*.md file the spec mandates for WP15 (spec:270-274 core list + +# the WP15 instructions' task 4 deliverables). Listed explicitly so a file +# that goes missing (or gets renamed without updating this list) fails loudly +# instead of being silently skipped. +REQUIRED_DOCS = [ + "concept.md", "technical_architecture.md", "gnc.md", "safety.md", + "target_selection.md", "cost_model.md", "legal_regulatory.md", + "limitations.md", "roadmap.md", "pitch_agencies.md", + "one_pager.md", "technical_summary_5p.md", "pitch_deck.md", + "release_engineering.md", +] + +# R15 pin list (spec R15's named legacy GNC pins, plus the WP15 content map's +# other headline pinned numbers -- amortization/FoM/anchor/sail-area figures) +# that must never appear as bare, hand-typed prose in docs/*.md outside a +# DOCS-*-START/END generated-include block (R16). This is an explicit, +# growing literal-string list checked as exact substrings -- it mirrors the +# R14 pattern-list precedent in check 2 below (an enumerated, hand-maintained +# list that must grow with new headline numbers, not a general "any digit" +# rule, which would false-positive on citation years/DOIs/page ranges -- see +# the WP15 content map section 5, trap 4). +R15_DOCS_PINS = [ + "19.15", "424.3", "16.87", "17.07", "16.28", "0.333", "44.80", "157.07", + "0.285", "156.70", "200.90", "31.35", "21.32", "135..2155", "7..115", + "0.0076", "0.0130", +] + +# Enforcement scope for the pin scan below (documented per the R14 +# pattern-list precedent, per WP15 instructions task 3): +# - A pin occurrence is EXEMPT if the line falls inside a committed +# DOCS-*-START/END marker block (fill_docs_numbers.py's generated-include +# mechanism already keeps that text byte-checked against its source CSV). +# - Otherwise a pin occurrence is EXEMPT if its own markdown paragraph (a +# run of consecutive non-blank lines -- the natural "logical line" of a +# wrapped markdown sentence, since these docs hand-wrap at ~78-80 cols), +# OR the very next paragraph (the "citation trailed after the prose" +# convention used throughout docs/), contains one of the four explicit +# reference phrases: "evidence pack", "generated/", "reference_metrics", +# "regenerate". +# - A pin occurrence is ALSO exempt if its paragraph contains the spec's +# fixed anchor phrase "clearance-verified aborts" -- the mandatory, +# verbatim approach-safety wording formula (spec section 1) pins 0.0076 +# inline BY DESIGN (R15: "any further upgrade of this wording is itself +# an R15-documented change"); it is a fixed, board-approved SENTENCE, not +# a place a generated include belongs. This mirrors the pack's own +# "minimum cost (?!\\s+claim)" carve-out for its own reference sentence. +# - Anything else is a genuine violation: a number that should be sourced +# via the include mechanism or an explicit reference, hand-typed instead. +_EXEMPT_PHRASES = ("evidence pack", "generated/", "reference_metrics", + "regenerate") +_FORMULA_ANCHOR = "clearance-verified aborts" +_DOCS_MARKER_RE = re.compile( + r".*?", + re.DOTALL) + + +def _marker_block_lines(text): + """Set of 0-based line indices that fall inside any committed + DOCS-*-START/END marker block (inclusive of the marker-comment lines + themselves).""" + lines_in_block = set() + for m in _DOCS_MARKER_RE.finditer(text): + start_line = text.count("\n", 0, m.start()) + end_line = text.count("\n", 0, m.end()) + lines_in_block.update(range(start_line, end_line + 1)) + return lines_in_block + + +def _paragraphs(lines): + """List of paragraphs, each a list of 0-based line indices, splitting on + blank lines (a markdown "paragraph" -- a tight bullet list with no blank + lines between items counts as one paragraph, which is deliberate: list + items commonly share a single citation for the whole list).""" + paras = [] + current = [] + for i, line in enumerate(lines): + if line.strip() == "": + if current: + paras.append(current) + current = [] + else: + current.append(i) + if current: + paras.append(current) + return paras + + +# Banned-word families (same concepts as the evidence-pack scan, check 1 +# above). Unlike the evidence pack -- which is written to avoid these words +# ENTIRELY, so a bare re.search over the whole file works -- docs/ is more +# discursive prose that explicitly DISCUSSES these very topics ("the only +# path to TRL 5", "guarantees are model-scoped", "'minimum cost' ... is +# banned"): an honest disclaiming/hedging sentence, not an overclaim. So each +# match is checked against its own paragraph (a run of consecutive non-blank +# lines) for one of these documented hedge/negation markers before being +# treated as a genuine violation -- an explicit, growing allowlist, same +# discipline as the R14 pattern list and the pack's own single +# "minimum cost(?!\\s+claim)" carve-out, just generalized to docs/ prose: +# not / never / no -- plain negation ("is not claimed", "no claim") +# banned -- self-referential ban statement +# reserved -- the WP9-reserved framing (TRL/roadmap prose) +# requires -- "reaching TRL 5 ... requires ..." framing +# only in / only path -- explicit model/track scoping (tightened, not +# bare "only", to avoid exempting a genuine +# overclaim that happens to contain "only") +# model-scoped -- the R14 explicit scoping tag +# lost -- "the ... guarantee is lost" (explicit failure +# admission, the opposite of a claim) +# overclaims -- meta-description of the audit's own banned- +# word list (technical_architecture.md +# describing what ctest `evidence` rejects) +_DOCS_BANNED_PATTERNS = ( + (r"flight[\s-]*(ready|proven)", "flight-ready/-proven"), + (r"proven\s+in\s+flight", "proven in flight"), + (r"guarantee", "guarantee(d/s)"), + (r"approved", "approved"), + (r"licensed", "licensed"), + (r"compliant\b", "compliant"), + (r"minimum[\s-]*cost(?!\s+claim)", "minimum cost (absolute claim)"), + (r"\bTRL\s*[56]\b", "TRL 5/6 claim"), +) +_HEDGE_RE = re.compile( + r"\b(not|never|no|banned|reserved|requires|lost|overclaims)\b" + r"|only (in|path)" + r"|model-scoped", + re.IGNORECASE) + + +def scan_docs_r16(repo): + """Returns a list of human-readable failure strings for the WP15/R16 + docs/ claims-audit extension: banned words (same families as the pack + scan above, hedge-context-aware -- see _DOCS_BANNED_PATTERNS), required + docs/ files present, and the R15 pin list never appearing as bare prose + outside a marker block or an exempt citation (see the enforcement-scope + comment above _EXEMPT_PHRASES).""" + docs_dir = os.path.join(repo, "docs") + problems = [] + + for name in REQUIRED_DOCS: + if not os.path.exists(os.path.join(docs_dir, name)): + problems.append("required docs/%s is missing" % name) + + if not os.path.isdir(docs_dir): + return problems + + for name in sorted(os.listdir(docs_dir)): + if not name.endswith(".md"): + continue + path = os.path.join(docs_dir, name) + with open(path, encoding="utf-8") as f: + text = f.read() + + lines = text.split("\n") + block_lines = _marker_block_lines(text) + paras = _paragraphs(lines) + para_of_line = {} + for pi, para in enumerate(paras): + for li in para: + para_of_line[li] = pi + + def context_for(li, lookahead=1): + pi = para_of_line.get(li) + covering = list(paras[pi]) if pi is not None else [] + for k in range(1, lookahead + 1): + if pi is not None and pi + k < len(paras): + covering = covering + list(paras[pi + k]) + return "\n".join(lines[i] for i in covering) + + for pat, why in _DOCS_BANNED_PATTERNS: + for m in re.finditer(pat, text, re.IGNORECASE): + li = text.count("\n", 0, m.start()) + if li in block_lines: + continue + if _HEDGE_RE.search(context_for(li)) is not None: + continue + problems.append( + "docs/%s:%d contains forbidden claim language with no " + "nearby hedge/negation: %s" % (name, li + 1, why)) + + for pin in R15_DOCS_PINS: + for li, line in enumerate(lines): + if pin not in line: + continue + if li in block_lines: + continue + context = context_for(li) + if _FORMULA_ANCHOR in context: + continue + if any(p in context for p in _EXEMPT_PHRASES): + continue + problems.append( + "docs/%s:%d has pin '%s' outside a DOCS-* marker block " + "with no nearby evidence-pack/generated//" + "reference_metrics/regenerate citation (R16)" + % (name, li + 1, pin)) + + return problems + + def main(): if len(sys.argv) < 2: print("usage: test_evidence.py ", file=sys.stderr) @@ -255,6 +464,11 @@ def must_contain(s, why): check(regen.replace(b"\r\n", b"\n") == pack.replace("\r\n", "\n").encode("utf-8"), "regenerated pack differs from the committed one") + # 6. (WP15/R16) docs/ claims-audit extension: banned words, required + # files present, R15 pin list never bare-typed outside a marker block. + for problem in scan_docs_r16(repo): + check(False, problem) + if fails: for m2 in fails: print("FAIL:", m2) diff --git a/tools/readme/fill_readme_numbers.py b/tools/readme/fill_readme_numbers.py index 74af405..4ac510c 100644 --- a/tools/readme/fill_readme_numbers.py +++ b/tools/readme/fill_readme_numbers.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""README marker-block filler (WP14 review item 0.4b / C4). +"""README marker-block filler (WP14 review item 0.4b / C4; WP15 R16 digest). fill_readme_numbers.py [--check] @@ -22,6 +22,16 @@ README.md was hand-updated and could silently drift from the committed CSVs it claims to summarize. +WP15 (proposal package, R16) shrank README's own blocks to a compact +"Key results" digest (a handful of headline rows, ~4-6 lines each) -- +the full WP5/WP6 tables these blocks used to carry now live in +docs/safety.md's DOCS-WP5-FULL block and docs/cost_model.md's DOCS-WP6-FULL +block respectively, filled by the sibling tool `tools/docs/fill_docs_numbers.py` +(which reuses this file's original, full-table template functions verbatim, +renamed build_wp5_full()/build_wp6_full()). README's digest and docs/'s full +tables are two independently-generated views of the SAME committed CSVs, so +neither can drift from the other without a --check failure somewhere. + Default mode rewrites README.md in place. `--check` instead exits non-zero if the regenerated blocks differ from the committed file (for CI: a stale README fails the build instead of silently drifting -- see ci.yml). @@ -42,16 +52,11 @@ # ---- Unicode glyphs used inside the README blocks, named so the source # ---- file itself stays ASCII-only (spec R11) while the generated text # ---- is not -- each is an explicit \uXXXX escape, never a literal byte. -EM = "\u2014" # em dash -MINUS = "\u2212" # minus sign (distinct from ASCII hyphen) +# (The full-table glyph set -- GE/SUBI/SIGMA/DOT/PM/GG/MINUS -- moved with +# the full-table builders to tools/docs/fill_docs_numbers.py in WP15; only +# the two glyphs the compact digest still uses remain here.) DELTA = "\u0394" # Greek capital delta (used for "Dv") -GE = "\u2265" # >= TIMES = "\u00d7" # multiplication sign -SUBI = "\u1d62" # Latin subscript small letter i -SIGMA = "\u03a3" # Greek capital sigma -DOT = "\u00b7" # middle dot -PM = "\u00b1" # plus-minus sign -GG = "\u226b" # much-greater-than WP5_START = ("") @@ -66,10 +71,6 @@ "SL-16 / Zenit-2 second stage": "SL-16", "SL-8 / Kosmos-3M second stage": "SL-8", } -CATALOG_FOM_LABEL = { - "SL-16 / Zenit-2 second stage": "SL-16 (~9 t, 840 km)", - "SL-8 / Kosmos-3M second stage": "SL-8 (~1.4 t, 750 km)", -} def f0(v): @@ -130,16 +131,15 @@ def q(self, catalog, record_type, metric, n_kits=None, weighting=None): return r raise KeyError((catalog, record_type, metric, n_kits, weighting)) - def first_row(self, catalog, record_type): + def anchor(self, scenario): + """currency_anchor_derived row for cost_scenario in {low, mid, high} + (WP14; catalog-blank, always at the baseline N).""" for r in self.rows: - if r["catalog"] == catalog and r["record_type"] == record_type: + if (r["record_type"] == "currency_anchor_derived" + and r["metric"] == "anchor_musd_per_cu" + and r["cost_scenario"] == scenario): return r - raise KeyError((catalog, record_type)) - - def rows_of(self, catalog, record_type, metric): - return [r for r in self.rows - if r["catalog"] == catalog and r["record_type"] == record_type - and r["metric"] == metric] + raise KeyError(scenario) def rate_cell(row): @@ -157,238 +157,91 @@ def ko_cell(row): return "%s [%s, %s]" % (rate_cell(row), m.group(1), m.group(2)) -def pctl(row, fmt): - return "%s / %s / %s" % (fmt(row["p05"]), fmt(row["p50"]), fmt(row["p95"])) - - -def count(row): - return int(round(float(row["estimate"]))) - - -def build_wp5_block(d5): +def amortization_min(d6, catalog): + """(nmin, cost_per_removal p50 at nmin, ratio_to_n1 p50 at nmin) -- nmin + is the kit count that minimizes cost/removal on the amortization sweep + (currently 4, Dv-budget-limited; computed, never hardcoded, so a future + model change that shifts the curve's minimum is picked up automatically).""" + ns = sorted({int(r["n_kits"]) for r in d6.rows + if r["record_type"] == "amortization" + and r["metric"] == "cost_per_removal"}) + cpr = {n: float(d6.q(catalog, "amortization", "cost_per_removal", n)["p50"]) + for n in ns} + nmin = min(ns, key=lambda n: cpr[n]) + ratio = float(d6.q(catalog, "amortization", "cost_per_removal_ratio_to_n1", + nmin)["p50"]) + return nmin, cpr[nmin], ratio + + +def build_wp5_digest(d5): + """README's compact WP5 'Key results' digest (WP15): 4 headline rows out + of the full WP5 campaign table -- the full table moved to + docs/safety.md's DOCS-WP5-FULL block (tools/docs/fill_docs_numbers.py's + build_wp5_full(), the direct descendant of this function's pre-WP15 + form).""" A, B = d5.catalogs[0], d5.catalogs[1] - n_runs = d5.n_runs - succA, succB = d5.m(A, "success_rate"), d5.m(B, "success_rate") - nptA, nptB = (d5.m(A, "nonproductive_termination_rate"), - d5.m(B, "nonproductive_termination_rate")) - gaA, gaB = d5.m(A, "gate_abort_run_rate"), d5.m(B, "gate_abort_run_rate") koA, koB = (d5.m(A, "keep_out_violation_rate"), d5.m(B, "keep_out_violation_rate")) dvA, dvB = d5.m(A, "dv_used_m_per_s"), d5.m(B, "dv_used_m_per_s") - kuA, kuB = d5.m(A, "kits_used"), d5.m(B, "kits_used") rmA, rmB = (d5.m(A, "removals_per_mission"), d5.m(B, "removals_per_mission")) - syA, syB = d5.m(A, "sync_arrival_time_s"), d5.m(B, "sync_arrival_time_s") - - dvexA, dvexB = count(d5.m(A, "dv_exhausted")), count(d5.m(B, "dv_exhausted")) - kitexA, kitexB = (count(d5.m(A, "kit_exhausted")), - count(d5.m(B, "kit_exhausted"))) - koCountA, koCountB = (count(d5.m(A, "keep_out_violation")), - count(d5.m(B, "keep_out_violation"))) - compA, compB = count(d5.m(A, "completed")), count(d5.m(B, "completed")) - gaCountA, gaCountB = (count(d5.m(A, "gate_abort")), - count(d5.m(B, "gate_abort"))) - stCountA, stCountB = (count(d5.m(A, "sync_timeout")), - count(d5.m(B, "sync_timeout"))) - - m = re.search(r"level tag (\S+), dispersion set (\S+)", koA["notes"]) - l0tag, dstag = m.group(1), m.group(2) - dv_limited_pct = int(round(100.0 * dvexA / n_runs)) L = [] - L.append("The full N = %d campaign (both catalog presets) runs in a few " - "tens of seconds" % n_runs) - L.append("on a CI runner (see the Actions log of the current run for the " - "actual figure).") - L.append("`success` here means a **productive end** %s the mission " - "installed its" % EM) - L.append("full 4-kit complement (`kit_exhausted`) or cleared all 6 " - "targets (`completed`) %s" % EM) - L.append("not one cut short by %sv exhaustion or a keep-out violation." - % DELTA) - L.append("") - L.append("| metric | SL-16 / Zenit-2 class | SL-8 / Kosmos-3M class |") + L.append("| campaign (N = %d/catalog) | %s class | %s class |" + % (d5.n_runs, CATALOG_SHORT[A], CATALOG_SHORT[B])) L.append("|---|---|---|") - L.append("| success rate | %s | %s |" % (rate_cell(succA), rate_cell(succB))) - L.append("| nonproductive-termination rate (= 1 %s success) | %s | %s |" - % (MINUS, rate_cell(nptA), rate_cell(nptB))) - L.append("| gate-abort-run rate (abort-path exposure) | %s | %s |" - % (rate_cell(gaA), rate_cell(gaB))) - L.append("| keep-out-violation rate | %s | %s |" - % (ko_cell(koA), ko_cell(koB))) - L.append("| %sv used p05/p50/p95 [m/s] | %s | %s |" - % (DELTA, pctl(dvA, f0), pctl(dvB, f0))) - L.append("| kits used p05/p50/p95 | %s | %s |" - % (pctl(kuA, f0), pctl(kuB, f0))) - L.append("| removals/mission p05/p50/p95 | %s | %s |" - % (pctl(rmA, f0), pctl(rmB, f0))) - L.append("| sync arrival p05/p50/p95 [s] | %s | %s |" - % (pctl(syA, f2), pctl(syB, f2))) - L.append("| failure counts (runs) | dv_exhausted %d, kit_exhausted %d, " - "keep_out %d, completed %d | dv_exhausted %d, kit_exhausted " - "%d, keep_out %d, completed %d |" - % (dvexA, kitexA, koCountA, compA, dvexB, kitexB, koCountB, compB)) - L.append("| per-target events | gate_abort %d, sync_timeout %d | " - "gate_abort %d, sync_timeout %d |" - % (gaCountA, stCountA, gaCountB, stCountB)) + L.append("| productive-end rate | %s | %s |" + % (rate_cell(succA), rate_cell(succB))) + L.append("| keep-out violations | %s | %s |" % (ko_cell(koA), ko_cell(koB))) + L.append("| %sv used p50 [m/s] | %s | %s |" + % (DELTA, f0(dvA["p50"]), f0(dvB["p50"]))) + L.append("| removals/mission p50 | %s | %s |" + % (f0(rmA["p50"]), f0(rmB["p50"]))) L.append("") - L.append("Two abort-related rates are reported and are deliberately " - "distinct:") - L.append("**`gate-abort-run rate`** is the abort-path exposure (fraction " - "of runs with %s 1" % GE) - L.append("closing-speed gate abort %s what the spec calls the \"abort " - "rate\"), while" % EM) - L.append("**`nonproductive-termination rate`** is 1 %s success. Under " - "the current *flat" % MINUS) - L.append("PLACEHOLDER* %sv cost the two coincide numerically %s every " - "aborting mission needs" % (DELTA, EM)) - L.append("an extra target-slot to still install its kits and so " - "exhausts the 140 m/s") - L.append("budget %s but they are separate concepts and will diverge " - "once the cost model" % EM) - L.append("gains structure. The honest campaign finding: the servicer is " - "**%sv-limited about" % DELTA) - L.append("%d%% of the time** (`dv_exhausted` = %d/%d) and installs its " - "full kit" % (dv_limited_pct, dvexA, n_runs)) - L.append("complement the rest; the attitude sync **never** times out " - "across the sampled") - L.append("tumble/attitude/actuator dispersions, and keep-out violations " - "are **%d of %d**" % (koCountA, n_runs)) - L.append("per catalog [%s: linear CW, dispersion set %s, Wilson 95%% " - "upper bound" % (l0tag, dstag)) - L.append("%s] %s the WP11 clearing-abort law accepts an abort only when " - "the analytic" % (f4(koA["wilson_high"]), EM)) - L.append("post-burn ellipse clears keep-out plus a design margin " - "(mechanism forensics:") - L.append("`generated/wp10_violation_forensics.md`; audit:") - L.append("`generated/wp11_abort_audit.md`). `completed` (all 6 targets) " - "is 0 by") - L.append("construction %s 4 kits cannot service 6 targets. The `%sv " - "used` / `kits used`" % (EM, DELTA)) - L.append("percentiles matching across the two presets is expected (a " - "flat PLACEHOLDER cost") - L.append("takes quantized, catalog-independent values %s not a " - "copy-paste bug). Full per-run" % EM) - L.append("records and the column schema are in " - "[generated/](generated/).") + L.append("Full tables + failure taxonomy: [docs/safety.md](docs/safety.md).") return "\n".join(L) -def build_wp6_block(d5, d6): - A, B = d5.catalogs[0], d5.catalogs[1] - Ashort, Bshort = CATALOG_SHORT[A], CATALOG_SHORT[B] - n_runs = d5.n_runs +def build_wp6_digest(d5, d6): + """README's compact WP6 'Key results' digest (WP15): 4 headline rows out + of the full amortization/FoM/tornado table -- the full table moved to + docs/cost_model.md's DOCS-WP6-FULL block (tools/docs/fill_docs_numbers.py's + build_wp6_full(), the direct descendant of this function's pre-WP15 + form). The cost/removal-at-anchor row is computed here from the + amortization curve's own p50 (not from the CSV's separately-rounded + cost_per_removal_musd rows) so this digest and the anchor derivation + agree to the same %.2f -- the two are mathematically identical but were + rounded through slightly different intermediate values upstream.""" + A = d5.catalogs[0] + Ashort = CATALOG_SHORT[A] - ns = sorted({int(r["n_kits"]) for r in d6.rows - if r["record_type"] == "amortization" - and r["metric"] == "cost_per_removal"}) - cprA = {n: float(d6.q(A, "amortization", "cost_per_removal", n)["p50"]) - for n in ns} - cprB = {n: float(d6.q(B, "amortization", "cost_per_removal", n)["p50"]) - for n in ns} - nmin = min(ns, key=lambda n: cprA[n]) - nmax = max(ns) - # Curated display subset (first 3 points, the curve minimum, one past - # it, and the sweep max) -- matches the committed table exactly and - # keeps it short; the full sweep is in the committed CSV. - display_ns = sorted(set(n for n in (1, 2, 3, nmin, nmin + 1, nmax) - if n in ns)) - - baseline_n = int(d6.first_row(A, "cost_component")["n_kits"]) - - ratio_min = float(d6.q(A, "amortization", "cost_per_removal_ratio_to_n1", - nmin)["p50"]) - pct_min = ratio_min * 100.0 - saving_x = 1.0 / ratio_min - catalog_gap_pct = abs(cprA[nmin] - cprB[nmin]) / cprA[nmin] * 100.0 + nmin, cpr_min, ratio_min = amortization_min(d6, A) fomA_sp = d6.q(A, "fom", "fom", weighting="spatial_density") fomA_cr = d6.q(A, "fom", "fom", weighting="criticality") - fomB_sp = d6.q(B, "fom", "fom", weighting="spatial_density") - fomB_cr = d6.q(B, "fom", "fom", weighting="criticality") - wA_sp = d6.q(A, "fom", "band_weight", weighting="spatial_density") - wA_cr = d6.q(A, "fom", "band_weight", weighting="criticality") - wB_sp = d6.q(B, "fom", "band_weight", weighting="spatial_density") - wB_cr = d6.q(B, "fom", "band_weight", weighting="criticality") - - tornado = [r for r in d6.rows if r["catalog"] == A - and r["record_type"] == "tornado" - and r["metric"] == "cost_per_removal_swing"] - if len(tornado) != 5: - raise RuntimeError( - "expected exactly 5 tornado cost_per_removal_swing rows for %r, " - "got %d -- the WP6-NUMBERS tornado prose template is hardcoded " - "for a 5-parameter, 2-line wrap and needs a human rewrite if the " - "parameter count changes" % (A, len(tornado))) - items = [(r["param"], float(r["p50"])) for r in tornado] + + anchor_low = float(d6.anchor("low")["p50"]) + anchor_mid = float(d6.anchor("mid")["p50"]) + anchor_high = float(d6.anchor("high")["p50"]) + cost_low = cpr_min * anchor_low + cost_mid = cpr_min * anchor_mid + cost_high = cpr_min * anchor_high L = [] - L.append("The full sweep (%d runs %s 2 catalogs %s N = %d..%d kits) runs " - "in a few tens of" % (n_runs, TIMES, TIMES, ns[0], nmax)) - L.append("seconds on a CI runner (see the Actions log of the current run " - "for the actual") - L.append("figure), and the baseline (N = %d) removals reproduce the " - "committed WP5 CSV" % baseline_n) - L.append("exactly (`MATCH (schema 1.0 consumed)`).") - L.append("") - L.append("**Amortization curve %s cost/removal vs kits carried N (%s " - "class):**" % (EM, Ashort)) - L.append("") - L.append("| N | cost/removal p50 [CU] | ratio to N=1 | removals p50 |") - L.append("|---:|---:|---:|---:|") - for n in display_ns: - cpr = float(d6.q(A, "amortization", "cost_per_removal", n)["p50"]) - ratio = float(d6.q(A, "amortization", "cost_per_removal_ratio_to_n1", - n)["p50"]) - rem = float(d6.q(A, "amortization", "removals", n)["p50"]) - if n == nmin: - L.append("| **%d** | **%s** | **%s** | %s |" - % (n, f2(cpr), f3(ratio), f0(rem))) - else: - L.append("| %d | %s | %s | %s |" % (n, f2(cpr), f3(ratio), f0(rem))) - L.append("") - L.append("Batch amortization drives cost/removal down to **%s %% of the " - "single-target" % f1(pct_min)) - L.append("(N = %d) baseline** %s a **%s%s per-removal saving** %s " - "bottoming at N = %d where the" - % (ns[0], EM, f1(saving_x), TIMES, EM, nmin)) - L.append("**%sv budget (not the kit count)** caps removals at %d; " - "carrying more kits then" % (DELTA, nmin)) - L.append("adds cost without removals and the curve turns back up. That " - "shape *is* the") - L.append("quantitative installer/batch argument (%s class is within " - "~%s %%)." % (Bshort, f1(catalog_gap_pct))) - L.append("") - L.append("**FoM = %s m%s%sw(h%s)/C_campaign at N = %d (p50, kg/CU):**" - % (SIGMA, SUBI, DOT, SUBI, baseline_n)) - L.append("") - L.append("| catalog | spatial-density | criticality |") - L.append("|---|---:|---:|") - L.append("| %s | %s (w = %s) | %s (w = %s) |" - % (CATALOG_FOM_LABEL[A], f2(fomA_sp["p50"]), f2(wA_sp["estimate"]), - f2(fomA_cr["p50"]), f2(wA_cr["estimate"]))) - L.append("| %s | %s (w = %s) | %s (w = %s) |" - % (CATALOG_FOM_LABEL[B], f2(fomB_sp["p50"]), f2(wB_sp["estimate"]), - f2(fomB_cr["p50"]), f2(wB_cr["estimate"]))) + L.append("| cost/FoM (relative CU; WP14 MUSD ranges) | value |") + L.append("|---|---|") + L.append("| amortization minimum (N = %d) | **%s CU/removal = %s%s the " + "N = 1 baseline** |" % (nmin, f2(cpr_min), f3(ratio_min), TIMES)) + L.append("| FoM p50, %s (spatial / criticality) | %s / %s kg/CU |" + % (Ashort, f2(fomA_sp["p50"]), f2(fomA_cr["p50"]))) + L.append("| derived anchor (low / mid / high) | %s / %s / %s MUSD/CU |" + % (f4(anchor_low), f4(anchor_mid), f4(anchor_high))) + L.append("| cost/removal p50 at the anchor | %s / %s / %s MUSD |" + % (f2(cost_low), f2(cost_mid), f2(cost_high))) L.append("") - L.append("The ~9 t class outranks the ~1.4 t class under **both** " - "weightings (FoM is") - L.append("mass-dominated), but the **band weight flips** %s spatial " - "density values the" % EM) - L.append("750 km band more, the criticality index values the 840 km " - "band more %s a genuine" % EM) - L.append("metric-choice disagreement tracked as **open trade T5**. " - "**Tornado** (%s," % Ashort) - L.append("%s30 %%, ranked by cost/removal swing): `%s` (%s CU) %s `%s` " - "(%s) >" - % (PM, items[0][0], f1(items[0][1]), GG, items[1][0], - f1(items[1][1]))) - L.append("`%s` (%s) > `%s` (%s) > `%s` (%s). All" - % (items[2][0], f1(items[2][1]), items[3][0], f1(items[3][1]), - items[4][0], f1(items[4][1]))) - L.append("values are relative CU; **no absolute-dollar figure is " - "claimed**.") + L.append("Full model + sources-or-PLACEHOLDER itemization:") + L.append("[docs/cost_model.md](docs/cost_model.md).") return "\n".join(L) @@ -401,8 +254,8 @@ def replace_block(text, start_marker, end_marker, new_body): def rewrite(text, d5, d6): - text = replace_block(text, WP5_START, WP5_END, build_wp5_block(d5)) - text = replace_block(text, WP6_START, WP6_END, build_wp6_block(d5, d6)) + text = replace_block(text, WP5_START, WP5_END, build_wp5_digest(d5)) + text = replace_block(text, WP6_START, WP6_END, build_wp6_digest(d5, d6)) return text