diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa1f843..26e0e06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,11 +43,22 @@ jobs: - name: Regenerate ALL committed artifacts (tools/regenerate_all.sh) run: time bash tools/regenerate_all.sh build - - name: Reproducibility gate (committed generated+evidence == regenerated, SHA-stable) + # WP14 (review C4): fills README.md's WP5-NUMBERS/WP6-NUMBERS marker + # blocks from the committed CSVs. This runs as its OWN step rather than + # being folded into tools/regenerate_all.sh, because README.md lives at + # the repo root, not under generated/ or evidence/ -- regenerate_all.sh's + # documented contract is "regenerates generated/ + evidence/" only (see + # its own header comment), and folding README in would blur that + # contract for no benefit: this step just needs to run before the + # reproducibility gate below, same as regenerate_all.sh does. + - 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) run: | - git diff --exit-code -- generated/ evidence/ \ - || (echo "::error::generated/ or evidence/ drifted from committed bytes (R6)"; \ - git --no-pager diff --stat -- generated/ evidence/; exit 1) + 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) # 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 3e1cde3..175df12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,6 +180,22 @@ if(PYTHON3_EXECUTABLE) add_test(NAME forensics COMMAND ${PYTHON3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/forensics/test_forensics.py ${CMAKE_CURRENT_SOURCE_DIR}) + # WP14 prioritization table: a pure derived/joined view over + # wp6_cost_summary.csv + wp13_kit_trade.csv + wp5_campaign_summary.csv + # (Python 3 stdlib only, R9). Checks regeneration, hygiene/claims + # discipline, D10/R6 currency-range discipline, content, and + # determinism/committed-bytes reproducibility. + add_test(NAME prioritization COMMAND ${PYTHON3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/prioritization/test_prioritization.py + ${CMAKE_CURRENT_SOURCE_DIR}) + # WP14 README marker-block filler (review C4): regenerates the + # WP5-NUMBERS/WP6-NUMBERS marker blocks in README.md from the committed + # wp5_campaign_summary.csv/wp6_cost_summary.csv and checks --check mode + # passes against the committed file, catches a corrupted number, and is + # deterministic/idempotent. + add_test(NAME readme_numbers COMMAND ${PYTHON3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/readme/test_readme_numbers.py + ${CMAKE_CURRENT_SOURCE_DIR}) else() - message(WARNING "python3 not found; WP7a viz, WP8 compliance, WP7 evidence and WP10c forensics tests not registered") + message(WARNING "python3 not found; WP7a viz, WP8 compliance, WP7 evidence, WP10c forensics and WP14 prioritization tests not registered") endif() diff --git a/evidence/adsc_evidence_pack.md b/evidence/adsc_evidence_pack.md index 3e32b15..235e3b5 100644 --- a/evidence/adsc_evidence_pack.md +++ b/evidence/adsc_evidence_pack.md @@ -343,9 +343,10 @@ since WP11 none ends in a keep-out violation); they are distinct concepts and separate columns. The amortization curve (section 1) bottoms at N=4 because the **Delta-v budget, not the kit count, caps removals** - the honest capacity story a -mission designer needs. Cost is RELATIVE (CU) throughout: **no absolute -cost is predicted anywhere in this package**; the CU-to-currency anchor is -a deliberately unfilled cited-range PLACEHOLDER. Parameter sensitivity is +mission designer needs. Cost is RELATIVE (CU) throughout and stays the +PRIMARY metric; WP14 additionally derives a cited absolute-cost RANGE +(never a point value) from an itemized cost table, detailed in the +'Absolute cost ranges (WP14)' subsection below. Parameter sensitivity is ranked by a one-at-a-time tornado (development cost dominates): ![WP6 tornado](../generated/viz/wp6_tornado.svg) @@ -358,6 +359,55 @@ real and is kept visible rather than resolved by fiat: ![WP6 FoM weightings](../generated/viz/wp6_fom_weightings.svg) +### 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." (adsc-specification-v5.md section 6, WP14). Every +number in this subsection is read from the new WP14 rows of the +committed `generated/wp6_cost_summary.csv` (schema 1.1) - nothing +hand-written. + +**CU->currency anchor**, derived at runtime from a 5-component core of +the itemized cost table (development + manufacturing_bus_unit + +kit_unit_scaled_9t x baseline-N-kits + launch + operations_3yr): low +**0.062781** MUSD/CU .. high **0.421328** MUSD/CU (mid 0.154580 MUSD/CU); low+high anchor +values themselves come only from cited itemized rows (D10) - never a +point-value dollar figure. + +| cost_scenario | anchor [MUSD/CU] | campaign base_total [MUSD] | campaign with_reserves [MUSD] | +|---|---:|---:|---:| +| low | 0.062781 | 11.38 | 13.76 | +| mid | 0.154580 | 27.89 | 35.32 | +| high | 0.421328 | 75.76 | 101.07 | + +`base_total` is the 5-component anchor core plus the additive +ground-segment/licensing/SSA-tracking rows (all outside the CU model); +`with_reserves` additionally applies the contingency-reserve percentage +and an asset+launch-scaled insurance line (arithmetic provenance for +both is printed in full in each row's own `notes` field, see the CSV). + +**Cost per removal (SL-16 class, baseline N, MUSD, p50 per +cost_scenario):** low **2.812** / mid **6.925** / high **18.875** MUSD/removal. +**Cost per risk-equivalent mass (criticality weighting), p50:** 0.004978 +CU/kg (0.000769 MUSD/kg, mid cost_scenario) - the exact inverse orientation +of the FoM above (1/FoM by construction, not an independent +computation - see the CSV row's own notes for the reciprocal-order- +statistics caveat). + +**PLACEHOLDER/extrapolation flags, extracted (never hand-retyped) +from the committed CSV's own notes -- the leading clause plus the +trailing bracketed provenance tag, D10:** + +- `ssa_tracking`: "LeoLabs declines to publicly quote; subscription is customized by fleet size and monitoring cadence (SpaceNews; CNBC)." ... [PLACEHOLDER] +- `kit_unit_scaled_9t`: "9-t-class installer deorbit kit; no citable product exists at this scale." ... [training-data extrapolation] + +Honesty framing, quoted verbatim from `generated/wp6_cost_schema.md` +(never hand-retyped, so the generator and the schema doc 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. + ## 6. Limitations - stated plainly, none hidden - **Linear CW scope (F2):** every passive-safety statement is exact only in @@ -459,9 +509,10 @@ git diff --exit-code -- generated/ evidence/ # byte-identical == reproduced ``` `tools/regenerate_all.sh` is the single source of truth for regeneration -order (campaign -> cost -> decay -> flux -> reference metrics -> figures -> -compliance -> this document); CI runs exactly this script and enforces the -byte-identity gate on every push. Measured wall time is printed to the CI +order (campaign -> cost -> decay -> kit trade -> prioritization -> flux -> +reference metrics -> figures -> compliance -> this document); CI runs +exactly this script and enforces the byte-identity gate on every push. +Measured wall time is printed to the CI log (see the latest `build-and-test` run); it is intentionally never embedded in artifacts, and it is well under the one-hour reproduction budget (D10). Requirements: C++17 compiler, Eigen 3.3+, CMake, Python 3 @@ -474,142 +525,161 @@ in `include/`, `src/` and `tools/` carrying the uppercase PLACEHOLDER mark (R10), collected automatically by the generator of this document (`tools/evidence/` itself is excluded from the scan - it names the marker only in order to collect and audit it). If a value is listed here, treat -it as unvalidated until a cited source replaces it. - -Total marks: **130** - -| location | line | -|---|---| -| `include/adsc/campaign.hpp:17` | // as a parameterized Delta-v / time cost (PLACEHOLDER, no plane-change | -| `include/adsc/campaign.hpp:22` | // Dispersions (all PLACEHOLDER where not physically validated, centralized in | -| `include/adsc/campaign.hpp:115` | // All PLACEHOLDER values are marked; none is a physically validated figure. | -| `include/adsc/campaign.hpp:128` | double dv_budget_m_s = 140.0; // PLACEHOLDER servicer Delta-v budget [m/s] | -| `include/adsc/campaign.hpp:130` | // Per-leg Delta-v cost model (PLACEHOLDER; parameterized, no plane-change | -| `include/adsc/campaign.hpp:132` | double dv_approach_m_s = 8.0; // PLACEHOLDER rendezvous/approach [m/s] | -| `include/adsc/campaign.hpp:133` | double dv_sync_m_s = 3.0; // PLACEHOLDER proximity + attitude sync [m/s] | -| `include/adsc/campaign.hpp:134` | double dv_depart_m_s = 5.0; // PLACEHOLDER safe departure [m/s] | -| `include/adsc/campaign.hpp:135` | double dv_abort_m_s = 4.0; // PLACEHOLDER safe-abort maneuver [m/s] | -| `include/adsc/campaign.hpp:136` | double dv_phasing_m_s = 15.0; // PLACEHOLDER inter-target phasing hop [m/s] | -| `include/adsc/campaign.hpp:138` | // Per-leg time model (PLACEHOLDER), for the mission elapsed-time metric. | -| `include/adsc/campaign.hpp:139` | double t_attach_s = 300.0; // PLACEHOLDER clamp + install [s] | -| `include/adsc/campaign.hpp:140` | double t_depart_s = 600.0; // PLACEHOLDER departure settle [s] | -| `include/adsc/campaign.hpp:141` | double t_phasing_s = 86400.0; // PLACEHOLDER inter-target phasing [s] | -| `include/adsc/campaign.hpp:143` | // --- dispersions (PLACEHOLDER 1-sigma unless noted) --- | -| `include/adsc/campaign.hpp:147` | double nominal_closing_m_s = 0.10; // PLACEHOLDER nominal capture closing speed [m/s] | -| `include/adsc/campaign.hpp:148` | double disp_closing_sigma_m_s = 0.045; // PLACEHOLDER (P(>0.15) ~ 0.13) [m/s] | -| `include/adsc/campaign.hpp:151` | double disp_rel_pos_m = 40.0; // PLACEHOLDER per-axis initial rel-position [m] | -| `include/adsc/campaign.hpp:152` | double disp_rel_vel_m_s = 0.02; // PLACEHOLDER per-axis initial rel-velocity [m/s] | -| `include/adsc/campaign.hpp:155` | double disp_tumble_rate_frac = 0.40; // PLACEHOLDER fractional 1-sigma on /w_t/ [-] | -| `include/adsc/campaign.hpp:156` | double disp_tumble_axis_rad = 0.30; // PLACEHOLDER tumble-axis tilt 1-sigma [rad] | -| `include/adsc/campaign.hpp:157` | double disp_init_att_rad = 0.35; // PLACEHOLDER servicer att-offset 1-sigma [rad] | -| `include/adsc/campaign.hpp:160` | double disp_actuator_scale = 0.10; // PLACEHOLDER ~+/-10% torque-scale 1-sigma [-] | -| `include/adsc/campaign.hpp:161` | double disp_actuator_misalign_rad = 0.01; // PLACEHOLDER axis-misalignment 1-sigma [rad] | -| `include/adsc/campaign.hpp:165` | double disp_sensor_noise_frac = 0.30; // PLACEHOLDER fractional 1-sigma on sensor sigmas [-] | -| `include/adsc/campaign.hpp:166` | double disp_sensor_bias_rad = 5.0e-4; // PLACEHOLDER sensor bias 1-sigma [rad] | -| `include/adsc/campaign.hpp:170` | double nominal_solar_factor = 1.0; // PLACEHOLDER mean atmospheric-density factor [-] | -| `include/adsc/campaign.hpp:171` | double disp_solar_factor_frac = 0.50; // PLACEHOLDER fractional 1-sigma [-] | -| `include/adsc/cost.hpp:25` | // emitted as a point value (R6/D10): only a cited RANGE via the PLACEHOLDER | -| `include/adsc/cost.hpp:36` | // All parameters live in CostConfig and are marked PLACEHOLDER (R10). WP6 | -| `include/adsc/cost.hpp:44` | // `altitude_km` under each weighting. PLACEHOLDER values -- fill with citations | -| `include/adsc/cost.hpp:52` | double spatial; // PLACEHOLDER normalized spatial-density weight | -| `include/adsc/cost.hpp:53` | double criticality; // PLACEHOLDER normalized criticality-style weight | -| `include/adsc/cost.hpp:59` | // All PLACEHOLDER; grouped so nothing is a bare literal in the cost logic (R10). | -| `include/adsc/cost.hpp:62` | double c_dev_cu = 100.0; // PLACEHOLDER program development (per-campaign allocati... | -| `include/adsc/cost.hpp:63` | double c_bus_cu = 3.0; // PLACEHOLDER bus mass-CER coefficient [CU] | -| `include/adsc/cost.hpp:64` | double c_bus_exponent = 0.7; // PLACEHOLDER bus mass-CER exponent [-] | -| `include/adsc/cost.hpp:65` | double c_kit_cu = 4.0; // PLACEHOLDER per-kit cost [CU] | -| `include/adsc/cost.hpp:66` | double c_launch_cu_per_kg = 0.5; // PLACEHOLDER launch cost coefficient [CU/kg] | -| `include/adsc/cost.hpp:67` | double c_ops_cu_per_day = 2.0; // PLACEHOLDER operations cost [CU/day] | -| `include/adsc/cost.hpp:69` | // Launch band factor (higher / more-inclined orbits cost more). PLACEHOLDER. | -| `include/adsc/cost.hpp:70` | double launch_band_ref_km = 700.0; // PLACEHOLDER reference altitude [km] | -| `include/adsc/cost.hpp:71` | double launch_band_per_100km = 0.06; // PLACEHOLDER cost slope [-/100 km] | -| `include/adsc/cost.hpp:72` | double launch_band_ref_incl_deg = 60.0; // PLACEHOLDER reference inclination [deg] | -| `include/adsc/cost.hpp:73` | double launch_band_per_deg = 0.004; // PLACEHOLDER cost slope [-/deg] | -| `include/adsc/cost.hpp:80` | double tornado_delta_frac = 0.30; // PLACEHOLDER +/-30% | -| `include/adsc/cost.hpp:85` | double cu_to_musd_low = 0.0; // PLACEHOLDER (cited range, WP7) | -| `include/adsc/cost.hpp:86` | double cu_to_musd_high = 0.0; // PLACEHOLDER (cited range, WP7) | -| `include/adsc/cost.hpp:88` | // Normalized congestion-weight table (PLACEHOLDER; cite on fill). Peaks | -| `include/adsc/cost.hpp:101` | // Launch band factor for a target band (PLACEHOLDER model). | -| `include/adsc/decay.hpp:34` | DebrisCatalog catalog_D(); // US Delta-class stage (PLACEHOLDER) | -| `include/adsc/decay.hpp:111` | double kit_mass_kg = 20.0; // PLACEHOLDER EDT kit mass (tether + deployer + electron... | -| `include/adsc/decay.hpp:112` | double avg_current_a = 2.0; // PLACEHOLDER average bare-tether collected current, BET... | -| `include/adsc/decay.hpp:113` | double eta_libration = 0.75; // PLACEHOLDER libration/duty-cycle efficiency penalty [-... | -| `include/adsc/decay.hpp:114` | double deploy_failure_prob = 0.05; // PLACEHOLDER deployment-failure probability [-] (report... | -| `include/adsc/flux.hpp:21` | // them. All debris-population figures are PLACEHOLDER, marked below and to be | -| `include/adsc/flux.hpp:26` | // PLACEHOLDER-marked flux parameters (R10); the physical constants (aluminium | -| `include/adsc/flux.hpp:34` | // Spatial number density of >=1 cm debris [objects / km^3]. PLACEHOLDER -- | -| `include/adsc/flux.hpp:36` | double density_avg_per_km3 = 1.2e-6; // PLACEHOLDER LEO-average | -| `include/adsc/flux.hpp:37` | double density_peak_per_km3 = 1.0e-5; // PLACEHOLDER peak congested band | -| `include/adsc/flux.hpp:39` | // >=1 cm population and the removal-fraction target. PLACEHOLDER (MASTER-8 | -| `include/adsc/flux.hpp:41` | double population_ge_1cm = 1.2e6; // PLACEHOLDER object count | -| `include/adsc/mission.hpp:29` | double target_altitude_km = 825.0; // PLACEHOLDER: SL-16-class band (D2) [km] | -| `include/adsc/mission.hpp:46` | double sync_target_rate_deg_s = 2.0; // PLACEHOLDER: tumble rate, spec range 0.5-5 [deg/s] | -| `include/adsc/mission.hpp:47` | Eigen::Vector3d target_inertia_diag{1.0, 0.6, 0.3}; // PLACEHOLDER principal moments [kg m^2] | -| `include/adsc/mission.hpp:57` | // acceleration, so the deadband bounds the hold error). PLACEHOLDER values. | -| `include/adsc/mission.hpp:60` | // WP3: kit + deorbit-decay trades. PLACEHOLDER physical values (R10). | -| `include/adsc/mission.hpp:66` | // catalog (bare-stage) areas are kept as PLACEHOLDER constants local to | -| `include/adsc/mission.hpp:68` | double servicer_drag_area_m2 = 0.35; // PLACEHOLDER servicer bus drag cross-section [m^2] | -| `include/adsc/mission.hpp:74` | // min..max range, never a point value. PLACEHOLDER values: a single | -| `include/adsc/mission.hpp:80` | double solar_min_density_factor = 0.5; // PLACEHOLDER solar-min scaling | -| `include/adsc/mission.hpp:81` | double solar_max_density_factor = 8.0; // PLACEHOLDER solar-max scaling | -| `include/adsc/mission.hpp:89` | // WP4: sensor + estimator abstractions. All PLACEHOLDER values (R10); | -| `include/adsc/mission.hpp:134` | double sensor_dropout_prob = 0.05; // PLACEHOLDER per-sample Bernoulli missed-det... | -| `include/adsc/mission.hpp:135` | double range_bias_walk_m_per_sqrt_s = 1.0e-3; // PLACEHOLDER unestimated range-bias random-w... | -| `include/adsc/mission.hpp:265` | double min_impulse_bit_nms = 0.0; // PLACEHOLDER MIB angular-impulse quantum [N m s], per ax... | -| `include/adsc/propagation.hpp:84` | // ---- Physical constants (cited, not PLACEHOLDER) ---- | -| `src/campaign.cpp:286` | // Inter-target phasing to the next target (PLACEHOLDER flat cost). | -| `src/campaign.cpp:400` | // Under the current flat PLACEHOLDER Delta-v cost these coincide numerically | -| `src/campaign.cpp:426` | "includes PLACEHOLDER phasing/attach/depart time"); | -| `src/campaign.cpp:571` | "PLACEHOLDER Delta-v cost these two coincide numerically -- every " | -| `src/campaign.cpp:587` | "PLACEHOLDER, so those quantities take a small set of quantized values " | -| `src/campaign.cpp:626` | "/ dv_budget_m_per_s / m/s / WP5-native, PLACEHOLDER / mission Delta-v budget /\n" | -| `src/campaign.cpp:627` | "/ dv_used_m_per_s / m/s / WP5-native, PLACEHOLDER-derived / sum of leg costs /\n" | -| `src/campaign.cpp:628` | "/ dv_remaining_m_per_s / m/s / WP5-native, PLACEHOLDER-derived / budget minus used /\n" | -| `src/campaign.cpp:633` | "/ mission_time_s / s / WP5-native, PLACEHOLDER-derived / elapsed incl. placeholder phasing /\n" | -| `src/campaign.cpp:640` | "/ first_closing_speed_m_per_s / m/s / WP5-native, PLACEHOLDER-derived / target-0 capture clo... | -| `src/campaign.cpp:641` | "/ tumble_rate_deg_per_s / deg/s / WP5-native, PLACEHOLDER-derived / realized /w_t/ of first ... | -| `src/campaign.cpp:642` | "/ solar_factor / - / WP5-native, PLACEHOLDER-derived / realized atmospheric-density factor (... | -| `src/cost.cpp:321` | "debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)"); | -| `src/cost.cpp:324` | fr.band_weight, "normalized", "w(h) PLACEHOLDER; cite on fill"); | -| `src/cost.cpp:350` | "PLACEHOLDER: fill a CITED range in WP7; no point-value dollar figure is claimed"); | -| `src/cost.cpp:365` | "PLACEHOLDER cited range, filled in WP7. All cost/FoM parameters are " | -| `src/cost.cpp:366` | "PLACEHOLDER (see `CostConfig`). Cost, cost/removal and FoM are " | -| `src/cost.cpp:414` | "tables are PLACEHOLDER and must be filled with citations.\n\n" | -| `src/cost.cpp:448` | "/ notes / provenance / PLACEHOLDER caveats /\n" | -| `src/cost.cpp:460` | " sum m_i*w(h_i)/C_campaign (spec ?4). The two weightings are PLACEHOLDER and\n" | -| `src/cost.cpp:467` | "- **currency_anchor** (global): `cu_to_musd_range` -- a PLACEHOLDER cited\n" | -| `src/decay.cpp:83` | return {"US Delta-class stage (PLACEHOLDER)", 0.0, 0.0, 0.0, true}; | -| `src/flux.cpp:81` | "figures are PLACEHOLDER (MASTER-8 / ESA spatial-density class; cite at " | -| `src/flux.cpp:94` | std::fprintf(f, "PLACEHOLDER spatial densities: average %.1e /km^3, peak " | -| `src/main.cpp:137` | std::printf(" target inertia diag: %.2f / %.2f / %.2f kg m^2 (PLACEHOLDER ratios)\n", | -| `src/main_flux.cpp:33` | std::printf("\n[T6] collector exposure (>= 1 cm; PLACEHOLDER densities, cite MASTER-8)\n"); | -| `src/main_kit_trade.cpp:38` | "/cos i/..cos^2 i; libration T7 OPEN - PLACEHOLDER duty factor]"; | -| `src/main_kit_trade.cpp:117` | // kit_mass (edt): NEW EdtConfig::kit_mass_kg, PLACEHOLDER. | -| `src/main_kit_trade.cpp:120` | "\"installed EDT kit mass, PLACEHOLDER (EdtConfig::kit_mass_kg)\"\n", | -| `src/main_kit_trade.cpp:124` | // deploy_risk: EdtConfig::deploy_failure_prob, PLACEHOLDER. Reported | -| `src/main_kit_trade.cpp:128` | "\"PLACEHOLDER EDT deployment-failure probability " | -| `src/main_kit_trade.cpp:203` | " `EdtConfig::kit_mass_kg` (PLACEHOLDER). value = value_lo = value_hi (a\n" | -| `src/main_kit_trade.cpp:205` | "- `deploy_risk`: `EdtConfig::deploy_failure_prob`, PLACEHOLDER. Reported\n" | -| `src/main_kit_trade.cpp:219` | "libration T7 OPEN - PLACEHOLDER duty factor]`. This states the model scope\n" | -| `src/main_kit_trade.cpp:228` | "PLACEHOLDER duty-cycle knob (`EdtConfig::eta_libration`) -- never claimed\n" | -| `src/main_kit_trade.cpp:229` | "solved. Plasma electron density is a cited PLACEHOLDER parameter, not an\n" | -| `src/main_kit_trade.cpp:366` | "**[CITATION NEEDED - PLACEHOLDER: per-object Ec analysis]**. Both " | -| `src/main_kit_trade.cpp:427` | std::fprintf(f, "/ Zenit-2 / SL-16 (catalog_A) / PLACEHOLDER until WP14 / WP14 cost-range ite... | -| `src/main_kit_trade.cpp:428` | std::fprintf(f, "/ Envisat-class (catalog_C) / PLACEHOLDER until WP14 / WP14 cost-range itemi... | -| `src/main_kit_trade.cpp:466` | // open risks (T7 libration, plasma Ne PLACEHOLDER). ------------------- | -| `src/main_kit_trade.cpp:479` | "(T7, Pelaez et al. 2000) and plasma Ne (PLACEHOLDER solar-min/" | -| `src/main_ladder.cpp:75` | // PLACEHOLDER bare-stage drag cross-sections (WP12 L2). Kept local to this | -| `src/main_ladder.cpp:78` | constexpr double kSL16AreaM2 = 33.0; // PLACEHOLDER SL-16 / Zenit-2 bare-stage cross-section... | -| `src/main_ladder.cpp:79` | constexpr double kSL8AreaM2 = 7.5; // PLACEHOLDER SL-8 / Kosmos-3M bare-stage cross-sectio... | -| `src/main_ladder.cpp:82` | // PLACEHOLDER statistical/numerical-safety constants are named here. | -| `src/main_ladder.cpp:83` | constexpr double kBcDispersionFrac = 0.30; // PLACEHOLDER +/-30% 1-sigma per-craft ballistic... | -| `src/main_metrics.cpp:329` | l5_act.min_impulse_bit_nms = 2.0e-4; // PLACEHOLDER MIB angular impulse [N m s] | -| `src/main_metrics.cpp:330` | l5_act.delay_steps = 1; // PLACEHOLDER one-control-step actuator lag | -| `src/main_metrics.cpp:331` | l5_act.fault_axis = 0; // PLACEHOLDER: body x-axis | -| `src/main_metrics.cpp:332` | l5_act.fault_axis_scale = 0.5; // PLACEHOLDER: 50% torque authority on that axis | -| `src/main_metrics.cpp:346` | // at a 1e-3 m/s PLACEHOLDER translation MIB and re-checking the | -| `src/main_metrics.cpp:351` | const double mib_m_s = 1.0e-3; // PLACEHOLDER translation MIB [m/s] | -| `src/main_metrics.cpp:355` | "contact velocity quantized at a 1e-3 m/s PLACEHOLDER translation " | +it as unvalidated until a cited source replaces it. Each mark also +carries a machine-assigned **importance** (spec:260-264): whether filling +it in with a citation would directly re-base a headline CU/MUSD/FoM +number or campaign safety/outcome rate (`decision-critical`), whether it +is a lower-priority or secondary-fidelity parameter (`moderate`), or +whether the mark appears only in a print/log statement or doc-generator +string, never on the value itself (`cosmetic`). Classification rule +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**) + +| 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/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 | +| `include/adsc/cost.hpp:58` | decision-critical | double criticality; // PLACEHOLDER normalized criticality-style weight | +| `include/adsc/cost.hpp:64` | decision-critical | // WP14 itemized absolute-cost row (low/mid/high, cited or PLACEHOLDER). Source: | +| `include/adsc/cost.hpp:67` | decision-critical | // (analog) 2026-07-11]" / "[training-data extrapolation]" / "[PLACEHOLDER]" | +| `include/adsc/cost.hpp:68` | decision-critical | // (D10: sourced-or-PLACEHOLDER, never fabricated). This table is separate | +| `include/adsc/cost.hpp:87` | decision-critical | // All PLACEHOLDER; grouped so nothing is a bare literal in the cost logic (R10). | +| `include/adsc/cost.hpp:90` | decision-critical | double c_dev_cu = 100.0; // PLACEHOLDER program development (per-campaign allocati... | +| `include/adsc/cost.hpp:91` | decision-critical | double c_bus_cu = 3.0; // PLACEHOLDER bus mass-CER coefficient [CU] | +| `include/adsc/cost.hpp:92` | decision-critical | double c_bus_exponent = 0.7; // PLACEHOLDER bus mass-CER exponent [-] | +| `include/adsc/cost.hpp:93` | decision-critical | double c_kit_cu = 4.0; // PLACEHOLDER per-kit cost [CU] | +| `include/adsc/cost.hpp:94` | decision-critical | double c_launch_cu_per_kg = 0.5; // PLACEHOLDER launch cost coefficient [CU/kg] | +| `include/adsc/cost.hpp:95` | decision-critical | double c_ops_cu_per_day = 2.0; // PLACEHOLDER operations cost [CU/day] | +| `include/adsc/cost.hpp:97` | decision-critical | // Launch band factor (higher / more-inclined orbits cost more). PLACEHOLDER. | +| `include/adsc/cost.hpp:98` | decision-critical | double launch_band_ref_km = 700.0; // PLACEHOLDER reference altitude [km] | +| `include/adsc/cost.hpp:99` | decision-critical | double launch_band_per_100km = 0.06; // PLACEHOLDER cost slope [-/100 km] | +| `include/adsc/cost.hpp:100` | decision-critical | double launch_band_ref_incl_deg = 60.0; // PLACEHOLDER reference inclination [deg] | +| `include/adsc/cost.hpp:101` | decision-critical | double launch_band_per_deg = 0.004; // PLACEHOLDER cost slope [-/deg] | +| `include/adsc/cost.hpp:108` | decision-critical | double tornado_delta_frac = 0.30; // PLACEHOLDER +/-30% | +| `include/adsc/cost.hpp:122` | decision-critical | // Normalized congestion-weight table (PLACEHOLDER; cite on fill). Peaks | +| `include/adsc/cost.hpp:135` | decision-critical | // Launch band factor for a target band (PLACEHOLDER model). | +| `include/adsc/decay.hpp:34` | moderate | DebrisCatalog catalog_D(); // US Delta-class stage (PLACEHOLDER) | +| `include/adsc/decay.hpp:111` | decision-critical | double kit_mass_kg = 20.0; // PLACEHOLDER EDT kit mass (tether + deployer + electron... | +| `include/adsc/decay.hpp:112` | decision-critical | double avg_current_a = 2.0; // PLACEHOLDER average bare-tether collected current, BET... | +| `include/adsc/decay.hpp:113` | decision-critical | double eta_libration = 0.75; // PLACEHOLDER libration/duty-cycle efficiency penalty [-... | +| `include/adsc/decay.hpp:114` | decision-critical | double deploy_failure_prob = 0.05; // PLACEHOLDER deployment-failure probability [-] (report... | +| `include/adsc/flux.hpp:21` | moderate | // them. All debris-population figures are PLACEHOLDER, marked below and to be | +| `include/adsc/flux.hpp:26` | moderate | // PLACEHOLDER-marked flux parameters (R10); the physical constants (aluminium | +| `include/adsc/flux.hpp:34` | moderate | // Spatial number density of >=1 cm debris [objects / km^3]. PLACEHOLDER -- | +| `include/adsc/flux.hpp:36` | moderate | double density_avg_per_km3 = 1.2e-6; // PLACEHOLDER LEO-average | +| `include/adsc/flux.hpp:37` | moderate | double density_peak_per_km3 = 1.0e-5; // PLACEHOLDER peak congested band | +| `include/adsc/flux.hpp:39` | moderate | // >=1 cm population and the removal-fraction target. PLACEHOLDER (MASTER-8 | +| `include/adsc/flux.hpp:41` | moderate | double population_ge_1cm = 1.2e6; // PLACEHOLDER object count | +| `include/adsc/mission.hpp:29` | decision-critical | double target_altitude_km = 825.0; // PLACEHOLDER: SL-16-class band (D2) [km] | +| `include/adsc/mission.hpp:46` | decision-critical | double sync_target_rate_deg_s = 2.0; // PLACEHOLDER: tumble rate, spec range 0.5-5 [deg/s] | +| `include/adsc/mission.hpp:47` | decision-critical | Eigen::Vector3d target_inertia_diag{1.0, 0.6, 0.3}; // PLACEHOLDER principal moments [kg m^2] | +| `include/adsc/mission.hpp:57` | moderate | // acceleration, so the deadband bounds the hold error). PLACEHOLDER values. | +| `include/adsc/mission.hpp:60` | moderate | // WP3: kit + deorbit-decay trades. PLACEHOLDER physical values (R10). | +| `include/adsc/mission.hpp:66` | moderate | // catalog (bare-stage) areas are kept as PLACEHOLDER constants local to | +| `include/adsc/mission.hpp:68` | decision-critical | double servicer_drag_area_m2 = 0.35; // PLACEHOLDER servicer bus drag cross-section [m^2] | +| `include/adsc/mission.hpp:74` | moderate | // min..max range, never a point value. PLACEHOLDER values: a single | +| `include/adsc/mission.hpp:80` | decision-critical | double solar_min_density_factor = 0.5; // PLACEHOLDER solar-min scaling | +| `include/adsc/mission.hpp:81` | decision-critical | double solar_max_density_factor = 8.0; // PLACEHOLDER solar-max scaling | +| `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/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 | +| `src/campaign.cpp:426` | cosmetic | "includes PLACEHOLDER phasing/attach/depart time"); | +| `src/campaign.cpp:571` | cosmetic | "PLACEHOLDER Delta-v cost these two coincide numerically -- every " | +| `src/campaign.cpp:587` | cosmetic | "PLACEHOLDER, so those quantities take a small set of quantized values " | +| `src/campaign.cpp:626` | cosmetic | "/ dv_budget_m_per_s / m/s / WP5-native, PLACEHOLDER / mission Delta-v budget /\n" | +| `src/campaign.cpp:627` | cosmetic | "/ dv_used_m_per_s / m/s / WP5-native, PLACEHOLDER-derived / sum of leg costs /\n" | +| `src/campaign.cpp:628` | cosmetic | "/ dv_remaining_m_per_s / m/s / WP5-native, PLACEHOLDER-derived / budget minus used /\n" | +| `src/campaign.cpp:633` | cosmetic | "/ mission_time_s / s / WP5-native, PLACEHOLDER-derived / elapsed incl. placeholder phasing /\n" | +| `src/campaign.cpp:640` | cosmetic | "/ first_closing_speed_m_per_s / m/s / WP5-native, PLACEHOLDER-derived / target-0 capture clo... | +| `src/campaign.cpp:641` | cosmetic | "/ tumble_rate_deg_per_s / deg/s / WP5-native, PLACEHOLDER-derived / realized /w_t/ of first ... | +| `src/campaign.cpp:642` | cosmetic | "/ solar_factor / - / WP5-native, PLACEHOLDER-derived / realized atmospheric-density factor (... | +| `src/cost.cpp:18` | moderate | // ends with its verified-status flag; D10 (sourced-or-PLACEHOLDER, never | +| `src/cost.cpp:74` | decision-critical | "do not invent a subscription figure. [PLACEHOLDER]"}, | +| `src/cost.cpp:470` | moderate | // licensing_fcc_annual x3 yr, and the PLACEHOLDER ssa_tracking row | +| `src/cost.cpp:524` | cosmetic | "debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)"); | +| `src/cost.cpp:527` | moderate | fr.band_weight, "normalized", "w(h) PLACEHOLDER; cite on fill"); | +| `src/cost.cpp:590` | cosmetic | "licensing_fcc_annual*3yr and ssa_tracking (PLACEHOLDER) are " | +| `src/cost.cpp:628` | cosmetic | "ssa_tracking (PLACEHOLDER, 0); outside the CU model and outside " | +| `src/cost.cpp:720` | cosmetic | "sentinels left at 0.0. All cost/FoM parameters are PLACEHOLDER (see " | +| `src/cost.cpp:769` | cosmetic | "tables are PLACEHOLDER and must be filled with citations.\n\n" | +| `src/cost.cpp:819` | cosmetic | "/ notes / provenance / PLACEHOLDER caveats / WP14 arithmetic strings /\n" | +| `src/cost.cpp:834` | cosmetic | " sum m_i*w(h_i)/C_campaign (spec ?4). The two weightings are PLACEHOLDER and\n" | +| `src/cost.cpp:859` | cosmetic | " note (D10: every row is sourced-or-PLACEHOLDER; a PLACEHOLDER row's note\n" | +| `src/cost.cpp:860` | cosmetic | " ends `[PLACEHOLDER]` and its value is 0, never fabricated).\n" | +| `src/cost.cpp:868` | cosmetic | " licensing_fcc_application, licensing_fcc_annual, and the PLACEHOLDER\n" | +| `src/cost.cpp:877` | cosmetic | " licensing_fcc_annual x3 yr + ssa_tracking (PLACEHOLDER, 0), outside the CU\n" | +| `src/decay.cpp:83` | decision-critical | return {"US Delta-class stage (PLACEHOLDER)", 0.0, 0.0, 0.0, true}; | +| `src/flux.cpp:81` | cosmetic | "figures are PLACEHOLDER (MASTER-8 / ESA spatial-density class; cite at " | +| `src/flux.cpp:94` | cosmetic | std::fprintf(f, "PLACEHOLDER spatial densities: average %.1e /km^3, peak " | +| `src/main.cpp:137` | cosmetic | std::printf(" target inertia diag: %.2f / %.2f / %.2f kg m^2 (PLACEHOLDER ratios)\n", | +| `src/main_flux.cpp:33` | cosmetic | std::printf("\n[T6] collector exposure (>= 1 cm; PLACEHOLDER densities, cite MASTER-8)\n"); | +| `src/main_kit_trade.cpp:38` | cosmetic | "/cos i/..cos^2 i; libration T7 OPEN - PLACEHOLDER duty factor]"; | +| `src/main_kit_trade.cpp:117` | moderate | // kit_mass (edt): NEW EdtConfig::kit_mass_kg, PLACEHOLDER. | +| `src/main_kit_trade.cpp:120` | cosmetic | "\"installed EDT kit mass, PLACEHOLDER (EdtConfig::kit_mass_kg)\"\n", | +| `src/main_kit_trade.cpp:124` | moderate | // deploy_risk: EdtConfig::deploy_failure_prob, PLACEHOLDER. Reported | +| `src/main_kit_trade.cpp:128` | cosmetic | "\"PLACEHOLDER EDT deployment-failure probability " | +| `src/main_kit_trade.cpp:203` | cosmetic | " `EdtConfig::kit_mass_kg` (PLACEHOLDER). value = value_lo = value_hi (a\n" | +| `src/main_kit_trade.cpp:205` | cosmetic | "- `deploy_risk`: `EdtConfig::deploy_failure_prob`, PLACEHOLDER. Reported\n" | +| `src/main_kit_trade.cpp:219` | cosmetic | "libration T7 OPEN - PLACEHOLDER duty factor]`. This states the model scope\n" | +| `src/main_kit_trade.cpp:228` | cosmetic | "PLACEHOLDER duty-cycle knob (`EdtConfig::eta_libration`) -- never claimed\n" | +| `src/main_kit_trade.cpp:229` | cosmetic | "solved. Plasma electron density is a cited PLACEHOLDER parameter, not an\n" | +| `src/main_kit_trade.cpp:366` | cosmetic | "**[CITATION NEEDED - PLACEHOLDER: per-object Ec analysis]**. Both " | +| `src/main_kit_trade.cpp:466` | moderate | // open risks (T7 libration, plasma Ne PLACEHOLDER). ------------------- | +| `src/main_kit_trade.cpp:479` | cosmetic | "(T7, Pelaez et al. 2000) and plasma Ne (PLACEHOLDER solar-min/" | +| `src/main_ladder.cpp:75` | moderate | // PLACEHOLDER bare-stage drag cross-sections (WP12 L2). Kept local to this | +| `src/main_ladder.cpp:78` | moderate | constexpr double kSL16AreaM2 = 33.0; // PLACEHOLDER SL-16 / Zenit-2 bare-stage cross-section... | +| `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 " | +| `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") | ## 11. Changelog - R15 pin supersessions diff --git a/generated/viz/wp5_dashboard.html b/generated/viz/wp5_dashboard.html index 04992ae..406c82e 100644 --- a/generated/viz/wp5_dashboard.html +++ b/generated/viz/wp5_dashboard.html @@ -14,7 +14,7 @@

ADSC - Visualization Pack (WP7a) - static report page

Regenerated from committed CSVs by tools/viz/make_viz.py (Python 3 stdlib only). Source: wp5_campaign_summary.csv (schema 1.1), -wp6_cost_summary.csv (schema 1.0), wp3_decay_trade.csv. Master seed 0x5AD5C0DECAFE2026, +wp6_cost_summary.csv (schema 1.1), wp3_decay_trade.csv. Master seed 0x5AD5C0DECAFE2026, N=500. No timestamps; every number read from the CSVs.

Keep-out figure: simplified research visualization, not a flight safety certificate.

diff --git a/generated/viz/wp6_amortization.svg b/generated/viz/wp6_amortization.svg index 9d4eafd..0fb73c3 100644 --- a/generated/viz/wp6_amortization.svg +++ b/generated/viz/wp6_amortization.svg @@ -1 +1 @@ -WP6 amortization: cost/removal vs kits carried N0357010514017512345678kits carried NCU / removalN=4: min 44.8 CU (0.285x of N=1)Delta-v-limited; curve turns up beyondSL-16SL-8line = p50 cost/removal; shaded = catalog-A p05..p95. Ratio-to-N1 read from CSV.source: wp6_cost_summary.csv schema 1.0 seed 0x5AD5C0DECAFE2026 (WP6 re-runs the WP5 engine; relative cost units CU) +WP6 amortization: cost/removal vs kits carried N0357010514017512345678kits carried NCU / removalN=4: min 44.8 CU (0.285x of N=1)Delta-v-limited; curve turns up beyondSL-16SL-8line = p50 cost/removal; shaded = catalog-A p05..p95. Ratio-to-N1 read from CSV.source: wp6_cost_summary.csv schema 1.1 seed 0x5AD5C0DECAFE2026 (WP6 re-runs the WP5 engine; relative cost units CU) diff --git a/generated/viz/wp6_fom_weightings.svg b/generated/viz/wp6_fom_weightings.svg index e3d74e0..9433d7f 100644 --- a/generated/viz/wp6_fom_weightings.svg +++ b/generated/viz/wp6_fom_weightings.svg @@ -1 +1 @@ -WP6 FoM under two weightings (T5 band-priority flip)04692139185231FoM [kg/CU]spatial_density156.7w=0.7831.4w=1.00criticality200.9w=1.0021.3w=0.68SL-16SL-8bar = FoM p50, whisker = p05..p95; w = band weight w(h). T5 flip: spatial ranks SL-8 band > SL-16 band (w 1.00 > 0.78), criticality flips it (1.00 > 0.68).absolute FoM is mass-dominated (SL-16 > SL-8 under both). weightings are illustrative (cite on fill).source: wp6_cost_summary.csv schema 1.0 seed 0x5AD5C0DECAFE2026 +WP6 FoM under two weightings (T5 band-priority flip)04692139185231FoM [kg/CU]spatial_density156.7w=0.7831.4w=1.00criticality200.9w=1.0021.3w=0.68SL-16SL-8bar = FoM p50, whisker = p05..p95; w = band weight w(h). T5 flip: spatial ranks SL-8 band > SL-16 band (w 1.00 > 0.78), criticality flips it (1.00 > 0.68).absolute FoM is mass-dominated (SL-16 > SL-8 under both). weightings are illustrative (cite on fill).source: wp6_cost_summary.csv schema 1.1 seed 0x5AD5C0DECAFE2026 diff --git a/generated/viz/wp6_tornado.svg b/generated/viz/wp6_tornado.svg index 25ff41a..a61e2d3 100644 --- a/generated/viz/wp6_tornado.svg +++ b/generated/viz/wp6_tornado.svg @@ -1 +1 @@ -WP6 tornado: cost/removal sensitivity (+/-30%)c_dev_cu15.00 CUc_bus_cu4.54 CUc_launch_cu_per_kg3.72 CUc_kit_cu2.40 CUc_ops_cu_per_day1.21 CU0swing [CU/removal]one-at-a-time +/-30% perturbation of each cost parameter; ranked by swing.source: wp6_cost_summary.csv schema 1.0 seed 0x5AD5C0DECAFE2026 catalog SL-16 +WP6 tornado: cost/removal sensitivity (+/-30%)c_dev_cu15.00 CUc_bus_cu4.54 CUc_launch_cu_per_kg3.72 CUc_kit_cu2.40 CUc_ops_cu_per_day1.21 CU0swing [CU/removal]one-at-a-time +/-30% perturbation of each cost parameter; ranked by swing.source: wp6_cost_summary.csv schema 1.1 seed 0x5AD5C0DECAFE2026 catalog SL-16 diff --git a/generated/wp13_classC.md b/generated/wp13_classC.md index 7cf0769..97d9b65 100644 --- a/generated/wp13_classC.md +++ b/generated/wp13_classC.md @@ -34,8 +34,8 @@ Steelmanned: a tug architecture also amortizes fixed costs (launch, ground segme | candidate | cost | source | |---|---|---| -| Zenit-2 / SL-16 (catalog_A) | PLACEHOLDER until WP14 | WP14 cost-range itemization | -| Envisat-class (catalog_C) | PLACEHOLDER until WP14 | WP14 cost-range itemization | +| Zenit-2 / SL-16 (catalog_A) | itemized low/mid/high ranges | wp6_cost_summary.csv cost_component_musd + campaign_cost_musd rows (WP14) | +| Envisat-class (catalog_C) | not separately itemized | WP14 ranges are servicer-campaign-scale; a controlled-reentry mission needs its own estimate (open) | ## `wp13_classC.csv` columns diff --git a/generated/wp14_prioritization.csv b/generated/wp14_prioritization.csv new file mode 100644 index 0000000..f66c6bf --- /dev/null +++ b/generated/wp14_prioritization.csv @@ -0,0 +1,32 @@ +schema_version,catalog,record_type,metric,weighting,cost_scenario,kit_option,estimate,p05,p50,p95,value_lo,value_hi,units,notes +1.0,SL-16 / Zenit-2 second stage,fom,fom,spatial_density,,,156.700654,117.539872,156.700654,156.700781,,,kg_per_CU,debris-risk-reduction per cost; weighting is PLACEHOLDER (T5) +1.0,SL-16 / Zenit-2 second stage,fom,band_weight,spatial_density,,,0.780000,,,,,,normalized,w(h) PLACEHOLDER; cite on fill +1.0,SL-16 / Zenit-2 second stage,fom,fom,criticality,,,200.898275,150.692144,200.898275,200.898438,,,kg_per_CU,debris-risk-reduction per cost; weighting is PLACEHOLDER (T5) +1.0,SL-16 / Zenit-2 second stage,fom,band_weight,criticality,,,1.000000,,,,,,normalized,w(h) PLACEHOLDER; cite on fill +1.0,SL-16 / Zenit-2 second stage,fom,removed_mass_per_mission,,,,36000.000000,,,,,,kg,per mission at baseline N +1.0,SL-16 / Zenit-2 second stage,cost_per_removal_cu,cost_per_removal,,,,,44.798756,44.798792,59.724414,,,CU_per_removal,"baseline N=4 (the Delta-v-limited amortization minimum, generated/wp6_cost_summary.csv record_type=amortization)" +1.0,SL-16 / Zenit-2 second stage,cost_per_removal_musd,cost_per_removal,,low,,2.812513,2.812511,2.812513,3.749558,,,MUSD_per_removal,"= cost_per_removal_CU(catalog=SL-16 / Zenit-2 second stage;baseline N=4) x anchor_musd_per_cu[low]=0.062781 MUSD/CU (generated/wp6_cost_summary.csv record_type=currency_anchor_derived); cross-checked against that same file's own record_type=cost_per_removal_musd row for this catalog/scenario -- consistent to display precision (this tool multiplies by the 6dp-rounded anchor while the wp6 row uses the full-precision anchor, so the last printed digit can differ by rounding path)" +1.0,SL-16 / Zenit-2 second stage,cost_per_removal_musd,cost_per_removal,,mid,,6.924997,6.924992,6.924997,9.232200,,,MUSD_per_removal,"= cost_per_removal_CU(catalog=SL-16 / Zenit-2 second stage;baseline N=4) x anchor_musd_per_cu[mid]=0.154580 MUSD/CU (generated/wp6_cost_summary.csv record_type=currency_anchor_derived); cross-checked against that same file's own record_type=cost_per_removal_musd row for this catalog/scenario -- consistent to display precision (this tool multiplies by the 6dp-rounded anchor while the wp6 row uses the full-precision anchor, so the last printed digit can differ by rounding path)" +1.0,SL-16 / Zenit-2 second stage,cost_per_removal_musd,cost_per_removal,,high,,18.874985,18.874970,18.874985,25.163568,,,MUSD_per_removal,"= cost_per_removal_CU(catalog=SL-16 / Zenit-2 second stage;baseline N=4) x anchor_musd_per_cu[high]=0.421328 MUSD/CU (generated/wp6_cost_summary.csv record_type=currency_anchor_derived); cross-checked against that same file's own record_type=cost_per_removal_musd row for this catalog/scenario -- consistent to display precision (this tool multiplies by the 6dp-rounded anchor while the wp6 row uses the full-precision anchor, so the last printed digit can differ by rounding path)" +1.0,SL-8 / Kosmos-3M second stage,fom,fom,spatial_density,,,31.350916,23.516074,31.350916,31.350940,,,kg_per_CU,debris-risk-reduction per cost; weighting is PLACEHOLDER (T5) +1.0,SL-8 / Kosmos-3M second stage,fom,band_weight,spatial_density,,,1.000000,,,,,,normalized,w(h) PLACEHOLDER; cite on fill +1.0,SL-8 / Kosmos-3M second stage,fom,fom,criticality,,,21.318623,15.990930,21.318623,21.318639,,,kg_per_CU,debris-risk-reduction per cost; weighting is PLACEHOLDER (T5) +1.0,SL-8 / Kosmos-3M second stage,fom,band_weight,criticality,,,0.680000,,,,,,normalized,w(h) PLACEHOLDER; cite on fill +1.0,SL-8 / Kosmos-3M second stage,fom,removed_mass_per_mission,,,,5600.000000,,,,,,kg,per mission at baseline N +1.0,SL-8 / Kosmos-3M second stage,cost_per_removal_cu,cost_per_removal,,,,,44.655758,44.655793,59.533747,,,CU_per_removal,"baseline N=4 (the Delta-v-limited amortization minimum, generated/wp6_cost_summary.csv record_type=amortization)" +1.0,SL-8 / Kosmos-3M second stage,cost_per_removal_musd,cost_per_removal,,low,,2.803535,2.803533,2.803535,3.737588,,,MUSD_per_removal,"= cost_per_removal_CU(catalog=SL-8 / Kosmos-3M second stage;baseline N=4) x anchor_musd_per_cu[low]=0.062781 MUSD/CU (generated/wp6_cost_summary.csv record_type=currency_anchor_derived); NOTE: the anchor itself was derived from catalog A's (SL-16) baseline campaign cost (Sec.10 Method), not this catalog's own -- applying it here assumes the CU->MUSD conversion factor is campaign-structure-invariant across catalogs rather than catalog-specific; a stated simplifying assumption, not a hidden one" +1.0,SL-8 / Kosmos-3M second stage,cost_per_removal_musd,cost_per_removal,,mid,,6.902892,6.902887,6.902892,9.202727,,,MUSD_per_removal,"= cost_per_removal_CU(catalog=SL-8 / Kosmos-3M second stage;baseline N=4) x anchor_musd_per_cu[mid]=0.154580 MUSD/CU (generated/wp6_cost_summary.csv record_type=currency_anchor_derived); NOTE: the anchor itself was derived from catalog A's (SL-16) baseline campaign cost (Sec.10 Method), not this catalog's own -- applying it here assumes the CU->MUSD conversion factor is campaign-structure-invariant across catalogs rather than catalog-specific; a stated simplifying assumption, not a hidden one" +1.0,SL-8 / Kosmos-3M second stage,cost_per_removal_musd,cost_per_removal,,high,,18.814736,18.814721,18.814736,25.083235,,,MUSD_per_removal,"= cost_per_removal_CU(catalog=SL-8 / Kosmos-3M second stage;baseline N=4) x anchor_musd_per_cu[high]=0.421328 MUSD/CU (generated/wp6_cost_summary.csv record_type=currency_anchor_derived); NOTE: the anchor itself was derived from catalog A's (SL-16) baseline campaign cost (Sec.10 Method), not this catalog's own -- applying it here assumes the CU->MUSD conversion factor is campaign-structure-invariant across catalogs rather than catalog-specific; a stated simplifying assumption, not a hidden one" +1.0,Envisat-class massive SSO payload,mission_class,not_kit_ranked,,,,,,,,,,,"Envisat-class massive SSO payload has no WP5/WP6 campaign rows (it is not part of the kit-installer Monte Carlo sweep); FoM and cost_per_removal are undefined for this class in this joined view. It is evaluated instead as a separate controlled-reentry mission class (spec WP13 Class C; see generated/wp13_classC.md), not kit-ranked against SL-16/SL-8." +1.0,SL-16 / Zenit-2 second stage,recommended_kit,,,,edt-candidate-open-risks,,,,,,,,"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, wp13-literature.md Topic 3) remain open -- an honest EDT candidate, not a closed recommendation" +1.0,SL-16 / Zenit-2 second stage,sail_area_25yr,,,,,,,,,134.6904,2155.0459,m2,"sail area for the 25-yr IADC guideline (solar max .. solar min); existing area_for_target_years model, see wp3_decay_trade.csv area_for_25yr" +1.0,SL-16 / Zenit-2 second stage,edt_years,,,,,,,,,2.9844,9.1668,years,"[model: EDT-v1 aligned-dipole, capped-current; eta band |cos i|..cos^2 i; libration T7 OPEN - PLACEHOLDER duty factor]" +1.0,SL-8 / Kosmos-3M second stage,recommended_kit,,,,sail,,,,,,,,"sail closes (7..115 m2 for the 25-yr guideline, solar max..min); no EDT kit needed for this class" +1.0,SL-8 / Kosmos-3M second stage,sail_area_25yr,,,,,,,,,7.2046,115.2728,m2,"sail area for the 25-yr IADC guideline (solar max .. solar min); existing area_for_target_years model, see wp3_decay_trade.csv area_for_25yr" +1.0,SL-8 / Kosmos-3M second stage,edt_years,,,,,,,,,0.6291,3.0259,years,"[model: EDT-v1 aligned-dipole, capped-current; eta band |cos i|..cos^2 i; libration T7 OPEN - PLACEHOLDER duty factor]" +1.0,Envisat-class massive SSO payload,recommended_kit,,,,controlled-reentry-mission-class,,,,,,,,massive (8211 kg) high-inclination payload -- sail/EDT kit-only deorbit is not the recommended mission class; see generated/wp13_classC.md for the controlled-reentry comparison +1.0,Envisat-class massive SSO payload,sail_area_25yr,,,,,,,,,61.4390,983.0247,m2,"sail area for the 25-yr IADC guideline (solar max .. solar min); existing area_for_target_years model, see wp3_decay_trade.csv area_for_25yr" +1.0,Envisat-class massive SSO payload,edt_years,,,,,,,,,5.4814,37.5223,years,"[model: EDT-v1 aligned-dipole, capped-current; eta band |cos i|..cos^2 i; libration T7 OPEN - PLACEHOLDER duty factor]" +1.0,SL-16 / Zenit-2 second stage,priority_rank,rank,,,,1,,,,,,,mass-dominant FoM under BOTH weightings + McKnight top-20-all-SL-16 citation; see the .md defense paragraph +1.0,SL-8 / Kosmos-3M second stage,priority_rank,rank,,,,2,,,,,,,"cheap closing sail kit, high per-class population; see the .md defense paragraph" +1.0,Envisat-class massive SSO payload,priority_rank,rank,,,not-kit-ranked,,,,,,,,"separate controlled-reentry mission class (spec WP13 Class C), not ranked against the two kit-installer classes above; see the .md defense paragraph" diff --git a/generated/wp14_prioritization.md b/generated/wp14_prioritization.md new file mode 100644 index 0000000..4f5d276 --- /dev/null +++ b/generated/wp14_prioritization.md @@ -0,0 +1,91 @@ +# WP14 Prioritization: target-class ranking (cost + FoM + kit trade) + +A pure derived/joined view over three already-committed, already- +regenerated CSVs -- `generated/wp6_cost_summary.csv` (schema 1.1), `generated/wp13_kit_trade.csv` (schema 1.0), and `generated/wp5_campaign_summary.csv` (schema 1.1). This tool runs no new simulation and adds no new statistics; every number below is read directly from those files, or is a single elementwise arithmetic combination of two already-committed numbers (noted inline wherever that occurs). Regenerated by `tools/prioritization/make_prioritization.py` (Python 3 stdlib only, R9). + +## Ranking (spec:265-266) + +1. **SL-16 / Zenit-2 second stage** -- mass-dominant FoM under both weightings; the McKnight top-of-ranking class. +2. **SL-8 / Kosmos-3M second stage** -- cheap closing sail kit, high per-class population. +3. **Envisat-class massive SSO payload** -- a separate controlled-reentry mission class, NOT kit-ranked against the two classes above. + +## Legal/consent gate (D12) + +Legal accessibility enters as a gate and metadata flags, never a multiplier (D12, adsc-specification-v5.md section 3): the WP8 compliance engine (`tools/compliance/check_compliance.py`, policy `ADSC-POL-01`) BLOCKs unconsented active debris removal, and this prioritization table consumes its PASS/BLOCK output and metadata flags rather than folding a subjective legal weight into the FoM or into any rank below. No catalog in this document is ranked, up or down, on legal/consent grounds -- consent is a gate, not a scoring input. + +## 1. SL-16 / Zenit-2 second stage (priority 1) + +### Figure of merit (debris-risk reduction per cost), p50 [kg/CU] + +| weighting | FoM p50 | FoM p05..p95 | band weight w(h) | +|---|---:|---|---:| +| spatial_density | 156.7 | 117.5..156.7 | 0.78 | +| criticality | 200.9 | 150.7..200.9 | 1.00 | + +### Cost per removal (baseline N=4, the Delta-v-limited amortization minimum) + +- CU: p05 44.80 / p50 44.80 / p95 59.72 CU/removal (relative cost units, primary metric). +- MUSD (external, low-standing -- see the honesty framing quoted in the WP14 evidence-pack cost section), p50 per cost_scenario: + - low: p05 2.813 / p50 2.813 / p95 3.750 MUSD/removal + - mid: p05 6.925 / p50 6.925 / p95 9.232 MUSD/removal + - high: p05 18.875 / p50 18.875 / p95 25.164 MUSD/removal + +### Recommended kit + +- kit_option: `edt-candidate-open-risks` +- sail area, 25-yr guideline (solar max..min): 135..2155 m^2 -- impractical at the upper end. +- EDT deorbit-time band: 3.0..9.2 yr [model: EDT-v1 aligned-dipole, capped-current; eta band |cos i|..cos^2 i; libration T7 OPEN - PLACEHOLDER duty factor] +- verbatim rationale, quoted from `generated/wp13_kit_trade.csv` (never hand-transcribed): "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, wp13-literature.md Topic 3) remain open -- an honest EDT candidate, not a closed recommendation" + +### Prioritization defense (one paragraph) + +The SL-16 class ranks first because its FoM dominates the SL-8 class under BOTH weightings (spatial-density 156.7 vs 31.4 kg/CU p50; criticality 200.9 vs 21.3 kg/CU p50 -- computed from generated/wp6_cost_summary.csv), which is the mass-dominance result the WP6/WP14 cost model already establishes; that dominance is corroborated, not merely asserted, by the published debris-ranking literature: 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 (wp13-literature.md Topic 6): 11 expert teams' consensus top-50 most-concerning LEO objects is dominated by rocket bodies, and 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). The class is the ADSC catalog-A anchor for exactly this reason. Its recommended kit is an honest EDT candidate, not a closed recommendation: sail-only does not close the 25-year guideline for this class (135..2155 m^2, impractical), so the electrodynamic-tether branch is carried instead, and that branch remains open on the libration/dynamic-tether-stability question (T7, Pelaez et al. 2000, cited in wp13-literature.md) -- the EDT years band above folds libration in only as a flat PLACEHOLDER duty-cycle knob, never claimed solved. Legal accessibility is a gate, not a factor in this ranking (D12, above). + +## 2. SL-8 / Kosmos-3M second stage (priority 2) + +### Figure of merit (debris-risk reduction per cost), p50 [kg/CU] + +| weighting | FoM p50 | FoM p05..p95 | band weight w(h) | +|---|---:|---|---:| +| spatial_density | 31.4 | 23.5..31.4 | 1.00 | +| criticality | 21.3 | 16.0..21.3 | 0.68 | + +### Cost per removal (baseline N=4, the Delta-v-limited amortization minimum) + +- CU: p05 44.66 / p50 44.66 / p95 59.53 CU/removal (relative cost units, primary metric). +- MUSD (external, low-standing; see the honesty framing quoted in the WP14 evidence-pack cost section), p50 per cost_scenario -- note this catalog reuses catalog A's currency anchor (see the CSV notes column for the stated simplifying assumption): + - low: p05 2.804 / p50 2.804 / p95 3.738 MUSD/removal + - mid: p05 6.903 / p50 6.903 / p95 9.203 MUSD/removal + - high: p05 18.815 / p50 18.815 / p95 25.083 MUSD/removal + +### Recommended kit + +- kit_option: `sail` +- sail area, 25-yr guideline (solar max..min): 7..115 m^2 -- tens of square meters, a practical kit; no EDT kit needed for this class. +- verbatim rationale, quoted from `generated/wp13_kit_trade.csv` (never hand-transcribed): "sail closes (7..115 m2 for the 25-yr guideline, solar max..min); no EDT kit needed for this class" + +### Prioritization defense (one paragraph) + +The SL-8 class ranks second: its FoM is smaller than SL-16's under both weightings (mass-dominated, as above), but it closes today with a cheap, practical sail kit (7..115 m^2 for the 25-year guideline, tens of square meters -- no EDT branch and no open T7 caveat needed), and it represents a much larger per-class population than the SL-16 top-20 concern list: wp13-literature.md Topic 5b: SL-8 (Kosmos-3M) second-stage clusters span three altitude boxes -- Box 3 ~700-800 km (50 R/Bs), Box 2 ~900-1000 km (143 R/Bs), Box 1 ~1500-1600 km (44 R/Bs), inclinations ~74/83 deg -- a combined population an order of magnitude larger than the SL-16 top-20 concern list; the class appears prominently in SL-8-specific ADR mission studies (e.g. ADReS-A, Springer 10.1007/978-3-319-15982-9_3). Cost per removal at its own baseline N is essentially the same as SL-16's in CU terms (44.66 vs 44.80 CU/removal p50), so the batch-amortization economics are not a differentiator between the two classes -- kit simplicity and closure certainty are. Legal accessibility is a gate, not a factor in this ranking (D12, above). + +## 3. Envisat-class massive SSO payload (separate mission class, not kit-ranked) + +No WP5/WP6 campaign rows exist for this catalog (it is not part of the kit-installer Monte Carlo sweep), so FoM and cost_per_removal are undefined for it here -- stated rather than silently omitted. + +### Why it is not a kit choice + +- sail area, 25-yr guideline (solar max..min): 61..983 m^2 (context only -- not the recommendation). +- EDT deorbit-time band: 5.5..37.5 yr (context only -- not the recommendation). +- kit_option: `controlled-reentry-mission-class` +- verbatim rationale, quoted from `generated/wp13_kit_trade.csv` (never hand-transcribed): "massive (8211 kg) high-inclination payload -- sail/EDT kit-only deorbit is not the recommended mission class; see generated/wp13_classC.md for the controlled-reentry comparison" + +### Prioritization defense (one paragraph) + +The Envisat-class massive SSO payload is evaluated as a separate controlled-reentry mission class (spec WP13 Class C), not ranked against SL-16 or SL-8: wp13-literature.md Topic 6 item 1: Envisat (ESA), 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 deg, defunct since April 2012; the archetypal 'too massive / high-casualty-risk-on-uncontrolled-reentry' object cited across the debris-mitigation literature (eoPortal; ESA Earth Online). Controlled-deorbit delta-v, casualty-risk framing and cost detail for this candidate live in `generated/wp13_classC.{csv,md}`, out of scope for this cost/FoM joined view; what belongs here is the negative result itself, stated plainly -- a sail or EDT kit-only deorbit is not the recommended path for an 8,211 kg, ~98.4 deg payload, so it is not assigned a kit-ranked priority number alongside the SL-16/SL-8 classes above. Legal accessibility is a gate, not a factor here either (D12, above). + +## Honesty note (T5, criticality weighting citation basis) + +The criticality weighting's citation basis is McKnight-anchored at the ~840-850 km peak (McKnight et al. 2021: the top ~20 most-concerning LEO objects are all SL-16/Zenit-2 second stages, ~9 t class, ~850 km -- exactly catalog A's band), which is a real, cited anchor point for catalog A's criticality weight w(h=840)=1.00. But the REST of the criticality weighting table (`CostConfig::weight_table`, the non-peak altitude/weight pairs away from 840-850 km) remains PLACEHOLDER: the McKnight ranking gives a strong, cited reason to weight the ~840-850 km peak high, but it does not by itself supply a citable functional form for how the weight falls off away from that peak, so the shape between anchor points is still an uncited interpolation (T5 open). The dressed-up-mass-sort critique from the WP14 review is acknowledged here, not dismissed: because both weighting tables today have limited dynamic range and removed mass dominates the FoM ratio, a skeptical reader is right to ask whether the 'criticality' weighting is doing real discriminating work beyond re-deriving a mass ranking; strengthening the non-peak citation basis (or accepting the narrower dynamic range as the honest state of the evidence) is the open item, not resolved by this document. + +--- +*Generated by `tools/prioritization/make_prioritization.py` from committed artifacts only. This is not legal advice.* diff --git a/generated/wp6_cost_schema.md b/generated/wp6_cost_schema.md index 140aea4..35a10e5 100644 --- a/generated/wp6_cost_schema.md +++ b/generated/wp6_cost_schema.md @@ -1,29 +1,46 @@ -# WP6 cost/FoM CSV schema (version 1.0) +# WP6/WP14 cost/FoM CSV schema (version 1.1) -`generated/wp6_cost_summary.csv` is a long-format table consumed by WP7. It -consumes the WP5 campaign (schema 1.0): the amortization sweep re-runs the WP5 -engine across the kit-inventory sweep at the fixed master seed, and the +`generated/wp6_cost_summary.csv` is a long-format table consumed downstream. +It consumes the WP5 campaign (schema 1.0): the amortization sweep re-runs the +WP5 engine across the kit-inventory sweep at the fixed master seed, and the baseline row (n_kits = CampaignConfig::kits_initial) reproduces `generated/wp5_campaign_runs.csv` exactly. Primary units are RELATIVE cost -units (CU); **no point-value currency is emitted** (R6/D10). +units (CU); **no point-value currency is emitted** (R6/D10) -- absolute costs +appear ONLY as cited low/mid/high ranges (WP14). + +Schema 1.1 (WP14) is an ADDITIVE bump over 1.0 (WP6): one new trailing +column (`cost_scenario`), four new `record_type` values (cost_component_musd +/ currency_anchor_derived / campaign_cost_musd / cost_per_removal_musd), and +two new `metric` values under the existing `fom` record_type +(cost_per_risk_equiv_mass / cost_per_risk_equiv_mass_musd). Every row that +existed under schema 1.0 is byte-identical in VALUE under 1.1; the only +change to those rows is the appended blank `cost_scenario` field. ## Columns | column | meaning | |---|---| -| schema_version | WP6 schema id (`1.0`) | +| schema_version | schema id (`1.1`) | | catalog | class-level preset name (blank for global rows) | -| record_type | amortization / fom / tornado / cost_component / currency_anchor | -| n_kits | kits carried (amortization/cost_component rows); blank otherwise | -| param | cost parameter (tornado / cost_component rows); blank otherwise | +| record_type | amortization / fom / tornado / cost_component / currency_anchor / cost_component_musd / currency_anchor_derived / campaign_cost_musd / cost_per_removal_musd | +| n_kits | kits carried (amortization/cost_component rows) or the baseline N + the WP14 derived rows were computed at; blank otherwise | +| param | cost parameter (tornado / cost_component rows) or the WP14 item id + (cost_component_musd rows); blank otherwise | | weighting | spatial_density / criticality (fom rows); blank otherwise | | metric | the quantity named in the row | | estimate | point value (p50 for distributions) | | p05 / p50 / p95 | percentiles (distribution rows; 0 when not applicable) | -| units | CU_per_removal / CU / count / ratio / kg / kg_per_CU / normalized / musd_per_cu | -| notes | provenance / PLACEHOLDER caveats | +| units | CU_per_removal / CU / count / ratio / kg / kg_per_CU / CU_per_kg / + normalized / musd_per_cu / MUSD / MUSD_per_removal / MUSD_per_kg / MUSD_per_yr + / MUSD_per_unit / percent-valued units carried in the row's own `units` + string (e.g. "pct of cost") | +| notes | provenance / PLACEHOLDER caveats / WP14 arithmetic strings | +| cost_scenario | low / mid / high (WP14 rows only); blank for pre-WP14 rows + and for rows (e.g. fom / cost_per_risk_equiv_mass in CU) that are not + currency-scenario-dependent | -## record_type rows +## record_type rows (schema 1.0, unchanged) - **amortization** (per catalog, per swept N): `cost_per_removal` [CU/removal, p05/p50/p95], `cost_per_removal_ratio_to_n1` [ratio], `removals` [count p50], @@ -34,18 +51,71 @@ units (CU); **no point-value currency is emitted** (R6/D10). - **fom** (per catalog): `removed_mass_per_mission` [kg p50], and per weighting `fom` [kg/CU, p05/p50/p95] and `band_weight` [normalized w(h)]. FoM = sum m_i*w(h_i)/C_campaign (spec §4). The two weightings are PLACEHOLDER and - disagree on band priority (T5). + disagree on band priority (T5). WP14 adds, per weighting: `cost_per_ risk_equiv_mass` [CU_per_kg, p05/p50/p95] = C_campaign/sum(m_i*w(h_i)), + the exact inverse orientation: each percentile is the reciprocal of the + FoM percentile BY CONSTRUCTION (NOT the percentile of per-run + reciprocals -- order statistics interpolate, so the two are not the same + computation), with p05/p95 swapped since 1/x is decreasing; and + `cost_per_risk_equiv_mass_musd` [MUSD_per_kg, cost_scenario=mid only, to + bound row count] = the same quantity converted via the mid-scenario anchor. - **cost_component** (per catalog, baseline N): C_dev / C_bus / C_kit_total / C_launch / C_ops [CU] at the median mission time. - **tornado** (representative catalog, baseline N): per cost parameter, `cost_per_removal_swing` [CU] and `fom_swing` [kg/CU] under a +/- one-at-a- time perturbation; rows are sorted by cost/removal swing. -- **currency_anchor** (global): `cu_to_musd_range` -- a PLACEHOLDER cited - range (estimate/p05 = low, p95 = high). WP6 never emits a point-value dollar - figure; a filled cited range is a WP7 deliverable. +- **currency_anchor** (global): `cu_to_musd_range` -- unfilled pre-WP14 + sentinel (CostConfig::cu_to_musd_low/high, both 0.0; SUPERSEDED, see + currency_anchor_derived below). Retained byte-identical for schema + continuity; it never carries a point-value dollar figure. + +## record_type rows (schema 1.1, new -- WP14) + +- **cost_component_musd** (global, one row per WP14 itemized cost row x + cost_scenario): `metric=cost`, `estimate`/`p50` = the row's value at that + scenario, `units` = the item's own unit (MUSD / MUSD/yr / MUSD/unit / a + percentage unit), `param` = the item id, `notes` = the full cited source + note (D10: every row is sourced-or-PLACEHOLDER; a PLACEHOLDER row's note + ends `[PLACEHOLDER]` and its value is 0, never fabricated). +- **currency_anchor_derived** (global, one row per cost_scenario): + `metric=anchor_musd_per_cu` [musd_per_cu] = core_5_component / + C_campaign_CU_p50 at the baseline N (catalog A). The core is EXACTLY the + 5 CU-mapped components (Sec.10 Method): development + + manufacturing_bus_unit + kit_unit_scaled_9t x baseline-N-kits + C_launch + (low=rideshare card low, mid=rideshare upper, high=dedicated Electron -- + width preserved, not narrowed) + operations_3yr. ground_segment_3yr, + licensing_fcc_application, licensing_fcc_annual, and the PLACEHOLDER + ssa_tracking row are ADDITIVE and excluded from this core (see + campaign_cost_musd below); so are the insurance/contingency percentage + rows. The `notes` field prints the full arithmetic string, numerator AND + denominator, so the figure is independently auditable from the row alone. +- **campaign_cost_musd** (global, per cost_scenario), four separately + visible rows so the anchor's basis is auditable: `metric=core_5_component` + = the identical numerator as currency_anchor_derived, in MUSD; + `metric=additives` = ground_segment_3yr + licensing_fcc_application + + licensing_fcc_annual x3 yr + ssa_tracking (PLACEHOLDER, 0), outside the CU + model and outside the anchor core; `metric=base_total` = core_5_component + + additives; `metric=with_reserves` = base_total x (1 + + contingency_reserve_pct/100) + insurance_line, where insurance_line = + (insurance_launch_plus_1yr_pct/100) x insured_value and insured_value = + manufacturing_bus_unit + kit_unit_scaled_9t x baseline-N-kits + C_launch + (asset+launch scale only -- insurance is an ADDITIVE line, not a + multiplier over the multi-year opex folded into base_total); `notes` + carries the formula for each row. +- **cost_per_removal_musd** (catalog A, baseline N, per cost_scenario): + `metric=cost_per_removal` [MUSD_per_removal, p05/p50/p95] = the existing + cost_per_removal CU percentiles at the baseline N times that scenario's + anchor_musd_per_cu. + +## Honesty framing (D10, carried into every WP14 currency row) + +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. -## Future visualization / evidence (WP7) — not implemented here +## Future visualization / evidence -- not implemented here The schema supports, without change: amortization curves, FoM-under-weightings -bars with percentile whiskers, tornado bars, and the cost-component breakdown. -WP6 emits none of these charts and predicts no absolute cost. +bars with percentile whiskers, tornado bars, the cost-component breakdown, and +the WP14 itemized/anchor/campaign-cost tables. This module emits none of +these charts. diff --git a/generated/wp6_cost_summary.csv b/generated/wp6_cost_summary.csv index 713bc4c..f405572 100644 --- a/generated/wp6_cost_summary.csv +++ b/generated/wp6_cost_summary.csv @@ -1,96 +1,158 @@ -schema_version,catalog,record_type,n_kits,param,weighting,metric,estimate,p05,p50,p95,units,notes -1.0,SL-16 / Zenit-2 second stage,amortization,1,,,cost_per_removal,157.068554,157.068476,157.068554,159.068573,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,1,,,cost_per_removal_ratio_to_n1,1.000000,0.000000,1.000000,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,1,,,removals,1.000000,0.000000,1.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,1,,,campaign_cost,157.068554,0.000000,157.068554,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,amortization,2,,,cost_per_removal,82.222051,82.221995,82.222051,83.222100,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,2,,,cost_per_removal_ratio_to_n1,0.523479,0.000000,0.523479,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,2,,,removals,2.000000,0.000000,2.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,2,,,campaign_cost,164.444102,0.000000,164.444102,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,amortization,3,,,cost_per_removal,57.273224,57.273171,57.273224,57.939899,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,3,,,cost_per_removal_ratio_to_n1,0.364638,0.000000,0.364638,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,3,,,removals,3.000000,0.000000,3.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,3,,,campaign_cost,171.819673,0.000000,171.819673,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,amortization,4,,,cost_per_removal,44.798792,44.798756,44.798792,59.724414,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,4,,,cost_per_removal_ratio_to_n1,0.285218,0.000000,0.285218,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,4,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,4,,,campaign_cost,179.195140,0.000000,179.195140,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,amortization,5,,,cost_per_removal,46.137160,36.914113,46.137160,61.508947,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,5,,,cost_per_removal_ratio_to_n1,0.293739,0.000000,0.293739,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,5,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,5,,,campaign_cost,184.548865,0.000000,184.548865,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,amortization,6,,,cost_per_removal,47.475560,37.984833,47.475560,63.293481,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,6,,,cost_per_removal_ratio_to_n1,0.302260,0.000000,0.302260,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,6,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,6,,,campaign_cost,189.902465,0.000000,189.902465,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,amortization,7,,,cost_per_removal,48.813960,39.055553,48.813960,65.078014,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,7,,,cost_per_removal_ratio_to_n1,0.310781,0.000000,0.310781,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,7,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,7,,,campaign_cost,195.256065,0.000000,195.256065,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,amortization,8,,,cost_per_removal,50.152360,40.126273,50.152360,66.862547,CU_per_removal,"" -1.0,SL-16 / Zenit-2 second stage,amortization,8,,,cost_per_removal_ratio_to_n1,0.319302,0.000000,0.319302,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-16 / Zenit-2 second stage,amortization,8,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-16 / Zenit-2 second stage,amortization,8,,,campaign_cost,200.609665,0.000000,200.609665,0.000000,CU,"" -1.0,SL-16 / Zenit-2 second stage,fom,,,,removed_mass_per_mission,36000.000000,0.000000,36000.000000,0.000000,kg,"per mission at baseline N" -1.0,SL-16 / Zenit-2 second stage,fom,,,spatial_density,fom,156.700654,117.539872,156.700654,156.700781,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)" -1.0,SL-16 / Zenit-2 second stage,fom,,,spatial_density,band_weight,0.780000,0.780000,0.780000,0.780000,normalized,"w(h) PLACEHOLDER; cite on fill" -1.0,SL-16 / Zenit-2 second stage,fom,,,criticality,fom,200.898275,150.692144,200.898275,200.898438,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)" -1.0,SL-16 / Zenit-2 second stage,fom,,,criticality,band_weight,1.000000,1.000000,1.000000,1.000000,normalized,"w(h) PLACEHOLDER; cite on fill" -1.0,SL-16 / Zenit-2 second stage,cost_component,4,C_dev,,component_cost,100.000000,0.000000,100.000000,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-16 / Zenit-2 second stage,cost_component,4,C_bus,,component_cost,30.291410,0.000000,30.291410,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-16 / Zenit-2 second stage,cost_component,4,C_kit_total,,component_cost,16.000000,0.000000,16.000000,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-16 / Zenit-2 second stage,cost_component,4,C_launch,,component_cost,24.816000,0.000000,24.816000,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-16 / Zenit-2 second stage,cost_component,4,C_ops,,component_cost,8.087730,0.000000,8.087730,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-8 / Kosmos-3M second stage,amortization,1,,,cost_per_removal,156.590154,156.590067,156.590154,158.590163,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,1,,,cost_per_removal_ratio_to_n1,1.000000,0.000000,1.000000,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,1,,,removals,1.000000,0.000000,1.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,1,,,campaign_cost,156.590154,0.000000,156.590154,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,2,,,cost_per_removal,81.967258,81.967198,81.967258,82.967274,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,2,,,cost_per_removal_ratio_to_n1,0.523451,0.000000,0.523451,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,2,,,removals,2.000000,0.000000,2.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,2,,,campaign_cost,163.934516,0.000000,163.934516,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,3,,,cost_per_removal,57.092962,57.092904,57.092962,57.759636,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,3,,,cost_per_removal_ratio_to_n1,0.364601,0.000000,0.364601,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,3,,,removals,3.000000,0.000000,3.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,3,,,campaign_cost,171.278887,0.000000,171.278887,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,4,,,cost_per_removal,44.655793,44.655758,44.655793,59.533747,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,4,,,cost_per_removal_ratio_to_n1,0.285176,0.000000,0.285176,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,4,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,4,,,campaign_cost,178.623146,0.000000,178.623146,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,5,,,cost_per_removal,45.986360,36.793479,45.986360,61.307881,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,5,,,cost_per_removal_ratio_to_n1,0.293673,0.000000,0.293673,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,5,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,5,,,campaign_cost,183.945668,0.000000,183.945668,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,6,,,cost_per_removal,47.316960,37.857959,47.316960,63.082014,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,6,,,cost_per_removal_ratio_to_n1,0.302171,0.000000,0.302171,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,6,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,6,,,campaign_cost,189.268068,0.000000,189.268068,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,7,,,cost_per_removal,48.647560,38.922439,48.647560,64.856147,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,7,,,cost_per_removal_ratio_to_n1,0.310668,0.000000,0.310668,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,7,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,7,,,campaign_cost,194.590468,0.000000,194.590468,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,8,,,cost_per_removal,49.978160,39.986919,49.978160,66.630281,CU_per_removal,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,8,,,cost_per_removal_ratio_to_n1,0.319165,0.000000,0.319165,0.000000,ratio,"p50 relative to sweep min N" -1.0,SL-8 / Kosmos-3M second stage,amortization,8,,,removals,4.000000,0.000000,4.000000,0.000000,count,"" -1.0,SL-8 / Kosmos-3M second stage,amortization,8,,,campaign_cost,199.912868,0.000000,199.912868,0.000000,CU,"" -1.0,SL-8 / Kosmos-3M second stage,fom,,,,removed_mass_per_mission,5600.000000,0.000000,5600.000000,0.000000,kg,"per mission at baseline N" -1.0,SL-8 / Kosmos-3M second stage,fom,,,spatial_density,fom,31.350916,23.516074,31.350916,31.350940,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)" -1.0,SL-8 / Kosmos-3M second stage,fom,,,spatial_density,band_weight,1.000000,1.000000,1.000000,1.000000,normalized,"w(h) PLACEHOLDER; cite on fill" -1.0,SL-8 / Kosmos-3M second stage,fom,,,criticality,fom,21.318623,15.990930,21.318623,21.318639,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)" -1.0,SL-8 / Kosmos-3M second stage,fom,,,criticality,band_weight,0.680000,0.680000,0.680000,0.680000,normalized,"w(h) PLACEHOLDER; cite on fill" -1.0,SL-8 / Kosmos-3M second stage,cost_component,4,C_dev,,component_cost,100.000000,0.000000,100.000000,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-8 / Kosmos-3M second stage,cost_component,4,C_bus,,component_cost,30.291410,0.000000,30.291410,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-8 / Kosmos-3M second stage,cost_component,4,C_kit_total,,component_cost,16.000000,0.000000,16.000000,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-8 / Kosmos-3M second stage,cost_component,4,C_launch,,component_cost,24.244000,0.000000,24.244000,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-8 / Kosmos-3M second stage,cost_component,4,C_ops,,component_cost,8.087736,0.000000,8.087736,0.000000,CU,"baseline breakdown at median mission time" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_dev_cu,,cost_per_removal_swing,15.000000,0.000000,15.000000,0.000000,CU_per_removal,"low=37.2988;high=52.2988 (+/-30% one-at-a-time)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_dev_cu,,fom_swing,53.981127,0.000000,53.981127,0.000000,kg_per_CU,"low=188.20985;high=134.22872 (spatial FoM)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_bus_cu,,cost_per_removal_swing,4.543711,0.000000,4.543711,0.000000,CU_per_removal,"low=42.5269;high=47.0706 (+/-30% one-at-a-time)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_bus_cu,,fom_swing,15.934322,0.000000,15.934322,0.000000,kg_per_CU,"low=165.07185;high=149.13753 (spatial FoM)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_launch_cu_per_kg,,cost_per_removal_swing,3.722400,0.000000,3.722400,0.000000,CU_per_removal,"low=42.9376;high=46.6600 (+/-30% one-at-a-time)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_launch_cu_per_kg,,fom_swing,13.043009,0.000000,13.043009,0.000000,kg_per_CU,"low=163.49310;high=150.45009 (spatial FoM)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_kit_cu,,cost_per_removal_swing,2.400000,0.000000,2.400000,0.000000,CU_per_removal,"low=43.5988;high=45.9988 (+/-30% one-at-a-time)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_kit_cu,,fom_swing,8.400932,0.000000,8.400932,0.000000,kg_per_CU,"low=161.01364;high=152.61270 (spatial FoM)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_ops_cu_per_day,,cost_per_removal_swing,1.213164,0.000000,1.213164,0.000000,CU_per_removal,"low=44.1922;high=45.4054 (+/-30% one-at-a-time)" -1.0,SL-16 / Zenit-2 second stage,tornado,,c_ops_cu_per_day,,fom_swing,4.244275,0.000000,4.244275,0.000000,kg_per_CU,"low=158.85153;high=154.60725 (spatial FoM)" -1.0,,currency_anchor,,,,cu_to_musd_range,0.000000,0.000000,0.000000,0.000000,musd_per_cu,"PLACEHOLDER: fill a CITED range in WP7; no point-value dollar figure is claimed" +schema_version,catalog,record_type,n_kits,param,weighting,metric,estimate,p05,p50,p95,units,notes,cost_scenario +1.1,SL-16 / Zenit-2 second stage,amortization,1,,,cost_per_removal,157.068554,157.068476,157.068554,159.068573,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,1,,,cost_per_removal_ratio_to_n1,1.000000,0.000000,1.000000,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,1,,,removals,1.000000,0.000000,1.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,1,,,campaign_cost,157.068554,0.000000,157.068554,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,amortization,2,,,cost_per_removal,82.222051,82.221995,82.222051,83.222100,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,2,,,cost_per_removal_ratio_to_n1,0.523479,0.000000,0.523479,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,2,,,removals,2.000000,0.000000,2.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,2,,,campaign_cost,164.444102,0.000000,164.444102,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,amortization,3,,,cost_per_removal,57.273224,57.273171,57.273224,57.939899,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,3,,,cost_per_removal_ratio_to_n1,0.364638,0.000000,0.364638,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,3,,,removals,3.000000,0.000000,3.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,3,,,campaign_cost,171.819673,0.000000,171.819673,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,amortization,4,,,cost_per_removal,44.798792,44.798756,44.798792,59.724414,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,4,,,cost_per_removal_ratio_to_n1,0.285218,0.000000,0.285218,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,4,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,4,,,campaign_cost,179.195140,0.000000,179.195140,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,amortization,5,,,cost_per_removal,46.137160,36.914113,46.137160,61.508947,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,5,,,cost_per_removal_ratio_to_n1,0.293739,0.000000,0.293739,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,5,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,5,,,campaign_cost,184.548865,0.000000,184.548865,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,amortization,6,,,cost_per_removal,47.475560,37.984833,47.475560,63.293481,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,6,,,cost_per_removal_ratio_to_n1,0.302260,0.000000,0.302260,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,6,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,6,,,campaign_cost,189.902465,0.000000,189.902465,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,amortization,7,,,cost_per_removal,48.813960,39.055553,48.813960,65.078014,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,7,,,cost_per_removal_ratio_to_n1,0.310781,0.000000,0.310781,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,7,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,7,,,campaign_cost,195.256065,0.000000,195.256065,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,amortization,8,,,cost_per_removal,50.152360,40.126273,50.152360,66.862547,CU_per_removal,"", +1.1,SL-16 / Zenit-2 second stage,amortization,8,,,cost_per_removal_ratio_to_n1,0.319302,0.000000,0.319302,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-16 / Zenit-2 second stage,amortization,8,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-16 / Zenit-2 second stage,amortization,8,,,campaign_cost,200.609665,0.000000,200.609665,0.000000,CU,"", +1.1,SL-16 / Zenit-2 second stage,fom,,,,removed_mass_per_mission,36000.000000,0.000000,36000.000000,0.000000,kg,"per mission at baseline N", +1.1,SL-16 / Zenit-2 second stage,fom,,,spatial_density,fom,156.700654,117.539872,156.700654,156.700781,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)", +1.1,SL-16 / Zenit-2 second stage,fom,,,spatial_density,band_weight,0.780000,0.780000,0.780000,0.780000,normalized,"w(h) PLACEHOLDER; cite on fill", +1.1,SL-16 / Zenit-2 second stage,fom,,,spatial_density,cost_per_risk_equiv_mass,0.006382,0.006382,0.006382,0.008508,CU_per_kg,"= C_campaign/sum(m_i*w(h_i)) = 1/fom per the same per-run FoM machinery (weights not recomputed); p50 is the reciprocal of the fom_p50 percentile BY CONSTRUCTION, NOT the percentile of per-run reciprocals (order statistics interpolate); p05/p95 are reciprocals of fom_p95/fom_p05 (order reverses for a decreasing transform)", +1.1,SL-16 / Zenit-2 second stage,fom,,,spatial_density,cost_per_risk_equiv_mass_musd,0.000986,0.000986,0.000986,0.001315,MUSD_per_kg,"= cost_per_risk_equiv_mass[CU_per_kg] x anchor_musd_per_cu[cost_scenario=mid]; mid-scenario anchor only to bound row count",mid +1.1,SL-16 / Zenit-2 second stage,fom,,,criticality,fom,200.898275,150.692144,200.898275,200.898438,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)", +1.1,SL-16 / Zenit-2 second stage,fom,,,criticality,band_weight,1.000000,1.000000,1.000000,1.000000,normalized,"w(h) PLACEHOLDER; cite on fill", +1.1,SL-16 / Zenit-2 second stage,fom,,,criticality,cost_per_risk_equiv_mass,0.004978,0.004978,0.004978,0.006636,CU_per_kg,"= C_campaign/sum(m_i*w(h_i)) = 1/fom per the same per-run FoM machinery (weights not recomputed); p50 is the reciprocal of the fom_p50 percentile BY CONSTRUCTION, NOT the percentile of per-run reciprocals (order statistics interpolate); p05/p95 are reciprocals of fom_p95/fom_p05 (order reverses for a decreasing transform)", +1.1,SL-16 / Zenit-2 second stage,fom,,,criticality,cost_per_risk_equiv_mass_musd,0.000769,0.000769,0.000769,0.001026,MUSD_per_kg,"= cost_per_risk_equiv_mass[CU_per_kg] x anchor_musd_per_cu[cost_scenario=mid]; mid-scenario anchor only to bound row count",mid +1.1,SL-16 / Zenit-2 second stage,cost_component,4,C_dev,,component_cost,100.000000,0.000000,100.000000,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-16 / Zenit-2 second stage,cost_component,4,C_bus,,component_cost,30.291410,0.000000,30.291410,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-16 / Zenit-2 second stage,cost_component,4,C_kit_total,,component_cost,16.000000,0.000000,16.000000,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-16 / Zenit-2 second stage,cost_component,4,C_launch,,component_cost,24.816000,0.000000,24.816000,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-16 / Zenit-2 second stage,cost_component,4,C_ops,,component_cost,8.087730,0.000000,8.087730,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-8 / Kosmos-3M second stage,amortization,1,,,cost_per_removal,156.590154,156.590067,156.590154,158.590163,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,1,,,cost_per_removal_ratio_to_n1,1.000000,0.000000,1.000000,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,1,,,removals,1.000000,0.000000,1.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,1,,,campaign_cost,156.590154,0.000000,156.590154,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,2,,,cost_per_removal,81.967258,81.967198,81.967258,82.967274,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,2,,,cost_per_removal_ratio_to_n1,0.523451,0.000000,0.523451,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,2,,,removals,2.000000,0.000000,2.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,2,,,campaign_cost,163.934516,0.000000,163.934516,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,3,,,cost_per_removal,57.092962,57.092904,57.092962,57.759636,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,3,,,cost_per_removal_ratio_to_n1,0.364601,0.000000,0.364601,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,3,,,removals,3.000000,0.000000,3.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,3,,,campaign_cost,171.278887,0.000000,171.278887,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,4,,,cost_per_removal,44.655793,44.655758,44.655793,59.533747,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,4,,,cost_per_removal_ratio_to_n1,0.285176,0.000000,0.285176,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,4,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,4,,,campaign_cost,178.623146,0.000000,178.623146,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,5,,,cost_per_removal,45.986360,36.793479,45.986360,61.307881,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,5,,,cost_per_removal_ratio_to_n1,0.293673,0.000000,0.293673,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,5,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,5,,,campaign_cost,183.945668,0.000000,183.945668,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,6,,,cost_per_removal,47.316960,37.857959,47.316960,63.082014,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,6,,,cost_per_removal_ratio_to_n1,0.302171,0.000000,0.302171,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,6,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,6,,,campaign_cost,189.268068,0.000000,189.268068,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,7,,,cost_per_removal,48.647560,38.922439,48.647560,64.856147,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,7,,,cost_per_removal_ratio_to_n1,0.310668,0.000000,0.310668,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,7,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,7,,,campaign_cost,194.590468,0.000000,194.590468,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,8,,,cost_per_removal,49.978160,39.986919,49.978160,66.630281,CU_per_removal,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,8,,,cost_per_removal_ratio_to_n1,0.319165,0.000000,0.319165,0.000000,ratio,"p50 relative to sweep min N", +1.1,SL-8 / Kosmos-3M second stage,amortization,8,,,removals,4.000000,0.000000,4.000000,0.000000,count,"", +1.1,SL-8 / Kosmos-3M second stage,amortization,8,,,campaign_cost,199.912868,0.000000,199.912868,0.000000,CU,"", +1.1,SL-8 / Kosmos-3M second stage,fom,,,,removed_mass_per_mission,5600.000000,0.000000,5600.000000,0.000000,kg,"per mission at baseline N", +1.1,SL-8 / Kosmos-3M second stage,fom,,,spatial_density,fom,31.350916,23.516074,31.350916,31.350940,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)", +1.1,SL-8 / Kosmos-3M second stage,fom,,,spatial_density,band_weight,1.000000,1.000000,1.000000,1.000000,normalized,"w(h) PLACEHOLDER; cite on fill", +1.1,SL-8 / Kosmos-3M second stage,fom,,,spatial_density,cost_per_risk_equiv_mass,0.031897,0.031897,0.031897,0.042524,CU_per_kg,"= C_campaign/sum(m_i*w(h_i)) = 1/fom per the same per-run FoM machinery (weights not recomputed); p50 is the reciprocal of the fom_p50 percentile BY CONSTRUCTION, NOT the percentile of per-run reciprocals (order statistics interpolate); p05/p95 are reciprocals of fom_p95/fom_p05 (order reverses for a decreasing transform)", +1.1,SL-8 / Kosmos-3M second stage,fom,,,spatial_density,cost_per_risk_equiv_mass_musd,0.004931,0.004931,0.004931,0.006573,MUSD_per_kg,"= cost_per_risk_equiv_mass[CU_per_kg] x anchor_musd_per_cu[cost_scenario=mid]; mid-scenario anchor only to bound row count",mid +1.1,SL-8 / Kosmos-3M second stage,fom,,,criticality,fom,21.318623,15.990930,21.318623,21.318639,kg_per_CU,"debris-risk-reduction per cost; weighting is PLACEHOLDER (T5)", +1.1,SL-8 / Kosmos-3M second stage,fom,,,criticality,band_weight,0.680000,0.680000,0.680000,0.680000,normalized,"w(h) PLACEHOLDER; cite on fill", +1.1,SL-8 / Kosmos-3M second stage,fom,,,criticality,cost_per_risk_equiv_mass,0.046907,0.046907,0.046907,0.062535,CU_per_kg,"= C_campaign/sum(m_i*w(h_i)) = 1/fom per the same per-run FoM machinery (weights not recomputed); p50 is the reciprocal of the fom_p50 percentile BY CONSTRUCTION, NOT the percentile of per-run reciprocals (order statistics interpolate); p05/p95 are reciprocals of fom_p95/fom_p05 (order reverses for a decreasing transform)", +1.1,SL-8 / Kosmos-3M second stage,fom,,,criticality,cost_per_risk_equiv_mass_musd,0.007251,0.007251,0.007251,0.009667,MUSD_per_kg,"= cost_per_risk_equiv_mass[CU_per_kg] x anchor_musd_per_cu[cost_scenario=mid]; mid-scenario anchor only to bound row count",mid +1.1,SL-8 / Kosmos-3M second stage,cost_component,4,C_dev,,component_cost,100.000000,0.000000,100.000000,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-8 / Kosmos-3M second stage,cost_component,4,C_bus,,component_cost,30.291410,0.000000,30.291410,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-8 / Kosmos-3M second stage,cost_component,4,C_kit_total,,component_cost,16.000000,0.000000,16.000000,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-8 / Kosmos-3M second stage,cost_component,4,C_launch,,component_cost,24.244000,0.000000,24.244000,0.000000,CU,"baseline breakdown at median mission time", +1.1,SL-8 / Kosmos-3M second stage,cost_component,4,C_ops,,component_cost,8.087736,0.000000,8.087736,0.000000,CU,"baseline breakdown at median mission time", +1.1,,cost_component_musd,,development,,cost,8.000000,0.000000,8.000000,0.000000,MUSD,"Smallsat bus + RPO/GNC dev NRE allocated to the campaign. Anchors: govt CubeSat/smallsat missions ~3-30 MUSD (New Space Economy survey); Aerospace Corp SSCM19 parametric CER methodology basis (aerospace.org/sscm); RPO-servicer ceiling analog Astroscale ELSA-d ~100 MUSD total two-spacecraft mission (184 kg servicer + 16 kg client). Low = lean COTS bus + heritage GNC; mid = smallsat band with RPO uplift; high = RPO-heavy half of the ELSA-d ceiling. [web-verified (analog) 2026-07-11]",low +1.1,,cost_component_musd,,development,,cost,20.000000,0.000000,20.000000,0.000000,MUSD,"Smallsat bus + RPO/GNC dev NRE allocated to the campaign. Anchors: govt CubeSat/smallsat missions ~3-30 MUSD (New Space Economy survey); Aerospace Corp SSCM19 parametric CER methodology basis (aerospace.org/sscm); RPO-servicer ceiling analog Astroscale ELSA-d ~100 MUSD total two-spacecraft mission (184 kg servicer + 16 kg client). Low = lean COTS bus + heritage GNC; mid = smallsat band with RPO uplift; high = RPO-heavy half of the ELSA-d ceiling. [web-verified (analog) 2026-07-11]",mid +1.1,,cost_component_musd,,development,,cost,50.000000,0.000000,50.000000,0.000000,MUSD,"Smallsat bus + RPO/GNC dev NRE allocated to the campaign. Anchors: govt CubeSat/smallsat missions ~3-30 MUSD (New Space Economy survey); Aerospace Corp SSCM19 parametric CER methodology basis (aerospace.org/sscm); RPO-servicer ceiling analog Astroscale ELSA-d ~100 MUSD total two-spacecraft mission (184 kg servicer + 16 kg client). Low = lean COTS bus + heritage GNC; mid = smallsat band with RPO uplift; high = RPO-heavy half of the ELSA-d ceiling. [web-verified (analog) 2026-07-11]",high +1.1,,cost_component_musd,,manufacturing_bus_unit,,cost,1.500000,0.000000,1.500000,0.000000,MUSD,"One ~25 kg-dry RPO-capable recurring flight bus (NRE lives in the development row). Anchors: high-performance smallsat bus > 1 MUSD (New Space Economy survey); recurring-unit learning rates 95pct (units 1-10) / 90pct (11-50) / 85pct (>50) (ICEAA Learning Rate Sensitivity Model). [web-verified (analog) 2026-07-11]",low +1.1,,cost_component_musd,,manufacturing_bus_unit,,cost,3.000000,0.000000,3.000000,0.000000,MUSD,"One ~25 kg-dry RPO-capable recurring flight bus (NRE lives in the development row). Anchors: high-performance smallsat bus > 1 MUSD (New Space Economy survey); recurring-unit learning rates 95pct (units 1-10) / 90pct (11-50) / 85pct (>50) (ICEAA Learning Rate Sensitivity Model). [web-verified (analog) 2026-07-11]",mid +1.1,,cost_component_musd,,manufacturing_bus_unit,,cost,6.000000,0.000000,6.000000,0.000000,MUSD,"One ~25 kg-dry RPO-capable recurring flight bus (NRE lives in the development row). Anchors: high-performance smallsat bus > 1 MUSD (New Space Economy survey); recurring-unit learning rates 95pct (units 1-10) / 90pct (11-50) / 85pct (>50) (ICEAA Learning Rate Sensitivity Model). [web-verified (analog) 2026-07-11]",high +1.1,,cost_component_musd,,launch_rideshare_sso,,cost,0.350000,0.000000,0.350000,0.000000,MUSD,"SpaceX rideshare SSO price card Feb 2026: base 350000 USD for up to 50 kg to SSO plus 7000 USD/kg above 50 kg; a ~44 kg servicer fits the 50 kg block. Mid/high add mid-inclination or dedicated-deployer/scheduling premiums (SpaceX directs non-SSO orbits to inquiry). [web-verified 2026-07-11]",low +1.1,,cost_component_musd,,launch_rideshare_sso,,cost,0.500000,0.000000,0.500000,0.000000,MUSD,"SpaceX rideshare SSO price card Feb 2026: base 350000 USD for up to 50 kg to SSO plus 7000 USD/kg above 50 kg; a ~44 kg servicer fits the 50 kg block. Mid/high add mid-inclination or dedicated-deployer/scheduling premiums (SpaceX directs non-SSO orbits to inquiry). [web-verified 2026-07-11]",mid +1.1,,cost_component_musd,,launch_rideshare_sso,,cost,0.700000,0.000000,0.700000,0.000000,MUSD,"SpaceX rideshare SSO price card Feb 2026: base 350000 USD for up to 50 kg to SSO plus 7000 USD/kg above 50 kg; a ~44 kg servicer fits the 50 kg block. Mid/high add mid-inclination or dedicated-deployer/scheduling premiums (SpaceX directs non-SSO orbits to inquiry). [web-verified 2026-07-11]",high +1.1,,cost_component_musd,,launch_dedicated_anchor,,cost,7.100000,0.000000,7.100000,0.000000,MUSD,"Context row NOT additive with launch_rideshare_sso -- alternative anchor if a dedicated orbit/schedule is required instead of rideshare; feeds the launch-HIGH bound (7.5 MUSD) of the anchor core's C_launch term (Sec.10 Method / adjudicated B3 -- width preserved, not narrowed). Rocket Lab Electron: ~300 kg to LEO dedicated mission; Q1-2025 realized avg revenue/launch 7.1 MUSD (SEC 10-Q); ~7.5 MUSD nominal dedicated-mission price (SpaceNexus explainer). [web-verified 2026-07-11]",low +1.1,,cost_component_musd,,launch_dedicated_anchor,,cost,7.500000,0.000000,7.500000,0.000000,MUSD,"Context row NOT additive with launch_rideshare_sso -- alternative anchor if a dedicated orbit/schedule is required instead of rideshare; feeds the launch-HIGH bound (7.5 MUSD) of the anchor core's C_launch term (Sec.10 Method / adjudicated B3 -- width preserved, not narrowed). Rocket Lab Electron: ~300 kg to LEO dedicated mission; Q1-2025 realized avg revenue/launch 7.1 MUSD (SEC 10-Q); ~7.5 MUSD nominal dedicated-mission price (SpaceNexus explainer). [web-verified 2026-07-11]",mid +1.1,,cost_component_musd,,launch_dedicated_anchor,,cost,7.500000,0.000000,7.500000,0.000000,MUSD,"Context row NOT additive with launch_rideshare_sso -- alternative anchor if a dedicated orbit/schedule is required instead of rideshare; feeds the launch-HIGH bound (7.5 MUSD) of the anchor core's C_launch term (Sec.10 Method / adjudicated B3 -- width preserved, not narrowed). Rocket Lab Electron: ~300 kg to LEO dedicated mission; Q1-2025 realized avg revenue/launch 7.1 MUSD (SEC 10-Q); ~7.5 MUSD nominal dedicated-mission price (SpaceNexus explainer). [web-verified 2026-07-11]",high +1.1,,cost_component_musd,,ground_segment_3yr,,cost,0.060000,0.000000,0.060000,0.000000,MUSD,"Derived: AWS Ground Station published per-minute rates 3 USD/min (narrowband reserved) to 22 USD/min (wideband on-demand) at 4-6 passes/day imply ~20000-60000 USD/yr all-in ground segment (AWS billing docs + ElasticScale / New Space Economy secondary confirmation); x3 yr campaign span gives 0.06-0.18 MUSD. [web-verified 2026-07-11]",low +1.1,,cost_component_musd,,ground_segment_3yr,,cost,0.120000,0.000000,0.120000,0.000000,MUSD,"Derived: AWS Ground Station published per-minute rates 3 USD/min (narrowband reserved) to 22 USD/min (wideband on-demand) at 4-6 passes/day imply ~20000-60000 USD/yr all-in ground segment (AWS billing docs + ElasticScale / New Space Economy secondary confirmation); x3 yr campaign span gives 0.06-0.18 MUSD. [web-verified 2026-07-11]",mid +1.1,,cost_component_musd,,ground_segment_3yr,,cost,0.180000,0.000000,0.180000,0.000000,MUSD,"Derived: AWS Ground Station published per-minute rates 3 USD/min (narrowband reserved) to 22 USD/min (wideband on-demand) at 4-6 passes/day imply ~20000-60000 USD/yr all-in ground segment (AWS billing docs + ElasticScale / New Space Economy secondary confirmation); x3 yr campaign span gives 0.06-0.18 MUSD. [web-verified 2026-07-11]",high +1.1,,cost_component_musd,,operations_3yr,,cost,1.000000,0.000000,1.000000,0.000000,MUSD,"3-yr campaign routine ops: 1-2 FTE at 150000-200000 USD/yr fully-burdened plus mission-ops software 50000-200000 USD/yr (SpaceNexus mission-cost guide); ops roughly 8pct of total mission cost / 20pct of bus cost. C_ops (CU) tracks ~4 sim-days not the real multi-year ops span -- this row is the real-dollar multi-year figure. [web-verified (analog) 2026-07-11]",low +1.1,,cost_component_musd,,operations_3yr,,cost,2.000000,0.000000,2.000000,0.000000,MUSD,"3-yr campaign routine ops: 1-2 FTE at 150000-200000 USD/yr fully-burdened plus mission-ops software 50000-200000 USD/yr (SpaceNexus mission-cost guide); ops roughly 8pct of total mission cost / 20pct of bus cost. C_ops (CU) tracks ~4 sim-days not the real multi-year ops span -- this row is the real-dollar multi-year figure. [web-verified (analog) 2026-07-11]",mid +1.1,,cost_component_musd,,operations_3yr,,cost,4.000000,0.000000,4.000000,0.000000,MUSD,"3-yr campaign routine ops: 1-2 FTE at 150000-200000 USD/yr fully-burdened plus mission-ops software 50000-200000 USD/yr (SpaceNexus mission-cost guide); ops roughly 8pct of total mission cost / 20pct of bus cost. C_ops (CU) tracks ~4 sim-days not the real multi-year ops span -- this row is the real-dollar multi-year figure. [web-verified (analog) 2026-07-11]",high +1.1,,cost_component_musd,,ssa_tracking,,cost,0.000000,0.000000,0.000000,0.000000,MUSD/yr,"LeoLabs declines to publicly quote; subscription is customized by fleet size and monitoring cadence (SpaceNews; CNBC). Zero-cost floor pointer: USSF/18th SDS free public conjunction data messages (CDMs). 0/0/0 is an UNFILLED placeholder not a claim the service is free -- do not invent a subscription figure. [PLACEHOLDER]",low +1.1,,cost_component_musd,,ssa_tracking,,cost,0.000000,0.000000,0.000000,0.000000,MUSD/yr,"LeoLabs declines to publicly quote; subscription is customized by fleet size and monitoring cadence (SpaceNews; CNBC). Zero-cost floor pointer: USSF/18th SDS free public conjunction data messages (CDMs). 0/0/0 is an UNFILLED placeholder not a claim the service is free -- do not invent a subscription figure. [PLACEHOLDER]",mid +1.1,,cost_component_musd,,ssa_tracking,,cost,0.000000,0.000000,0.000000,0.000000,MUSD/yr,"LeoLabs declines to publicly quote; subscription is customized by fleet size and monitoring cadence (SpaceNews; CNBC). Zero-cost floor pointer: USSF/18th SDS free public conjunction data messages (CDMs). 0/0/0 is an UNFILLED placeholder not a claim the service is free -- do not invent a subscription figure. [PLACEHOLDER]",high +1.1,,cost_component_musd,,insurance_launch_plus_1yr_pct,,cost,5.000000,0.000000,5.000000,0.000000,% insured value,"Launch-plus-first-year-in-orbit premium as pct of insured value; range rises after major-loss years (2023-24 hardened the market) or for unproven vehicles (Orbital Radar; Taylor Wessing; SpaceNews). Many smallsat missions self-insure -- applied only in the campaign_cost_musd_with_reserves row never in the base cost. [web-verified 2026-07-11]",low +1.1,,cost_component_musd,,insurance_launch_plus_1yr_pct,,cost,8.000000,0.000000,8.000000,0.000000,% insured value,"Launch-plus-first-year-in-orbit premium as pct of insured value; range rises after major-loss years (2023-24 hardened the market) or for unproven vehicles (Orbital Radar; Taylor Wessing; SpaceNews). Many smallsat missions self-insure -- applied only in the campaign_cost_musd_with_reserves row never in the base cost. [web-verified 2026-07-11]",mid +1.1,,cost_component_musd,,insurance_launch_plus_1yr_pct,,cost,12.000000,0.000000,12.000000,0.000000,% insured value,"Launch-plus-first-year-in-orbit premium as pct of insured value; range rises after major-loss years (2023-24 hardened the market) or for unproven vehicles (Orbital Radar; Taylor Wessing; SpaceNews). Many smallsat missions self-insure -- applied only in the campaign_cost_musd_with_reserves row never in the base cost. [web-verified 2026-07-11]",high +1.1,,cost_component_musd,,licensing_fcc_application,,cost,0.030000,0.000000,0.030000,0.000000,MUSD,"FCC Part 25 application fee one-time (not annual): streamlined smallsat 30000 USD (6-yr license term) is low; standard 45000 USD (15-yr term) is high; mid is the arithmetic midpoint (no published intermediate tier). [web-verified 2026-07-11]",low +1.1,,cost_component_musd,,licensing_fcc_application,,cost,0.037500,0.000000,0.037500,0.000000,MUSD,"FCC Part 25 application fee one-time (not annual): streamlined smallsat 30000 USD (6-yr license term) is low; standard 45000 USD (15-yr term) is high; mid is the arithmetic midpoint (no published intermediate tier). [web-verified 2026-07-11]",mid +1.1,,cost_component_musd,,licensing_fcc_application,,cost,0.045000,0.000000,0.045000,0.000000,MUSD,"FCC Part 25 application fee one-time (not annual): streamlined smallsat 30000 USD (6-yr license term) is low; standard 45000 USD (15-yr term) is high; mid is the arithmetic midpoint (no published intermediate tier). [web-verified 2026-07-11]",high +1.1,,cost_component_musd,,licensing_fcc_annual,,cost,0.012215,0.000000,0.012215,0.000000,MUSD/yr,"FCC annual regulatory fee NGSO Small Satellite category FY2024: 12215 USD per license/call sign (Federal Register FY2024 fee assessment; FCC 25-31); category explicitly includes On-Orbit Servicing (OOS) and Rendezvous and Proximity Operations (RPO) -- the ADSC servicer class. Single published figure flat across low/mid/high. [web-verified 2026-07-11]",low +1.1,,cost_component_musd,,licensing_fcc_annual,,cost,0.012215,0.000000,0.012215,0.000000,MUSD/yr,"FCC annual regulatory fee NGSO Small Satellite category FY2024: 12215 USD per license/call sign (Federal Register FY2024 fee assessment; FCC 25-31); category explicitly includes On-Orbit Servicing (OOS) and Rendezvous and Proximity Operations (RPO) -- the ADSC servicer class. Single published figure flat across low/mid/high. [web-verified 2026-07-11]",mid +1.1,,cost_component_musd,,licensing_fcc_annual,,cost,0.012215,0.000000,0.012215,0.000000,MUSD/yr,"FCC annual regulatory fee NGSO Small Satellite category FY2024: 12215 USD per license/call sign (Federal Register FY2024 fee assessment; FCC 25-31); category explicitly includes On-Orbit Servicing (OOS) and Rendezvous and Proximity Operations (RPO) -- the ADSC servicer class. Single published figure flat across low/mid/high. [web-verified 2026-07-11]",high +1.1,,cost_component_musd,,kit_unit_scaled_9t,,cost,0.100000,0.000000,0.100000,0.000000,MUSD/unit,"9-t-class installer deorbit kit; no citable product exists at this scale. Extrapolated from Terminator Tape (50000-75000 USD/unit CubeSat-scale lower-bound analog flight-proven) and dragNET (2.8 kg module mass/design cited no public unit price) up to a custom EDT/sail plus attach interface for a multi-tonne derelict; upper bound is engineering judgment. [training-data extrapolation]",low +1.1,,cost_component_musd,,kit_unit_scaled_9t,,cost,0.500000,0.000000,0.500000,0.000000,MUSD/unit,"9-t-class installer deorbit kit; no citable product exists at this scale. Extrapolated from Terminator Tape (50000-75000 USD/unit CubeSat-scale lower-bound analog flight-proven) and dragNET (2.8 kg module mass/design cited no public unit price) up to a custom EDT/sail plus attach interface for a multi-tonne derelict; upper bound is engineering judgment. [training-data extrapolation]",mid +1.1,,cost_component_musd,,kit_unit_scaled_9t,,cost,2.000000,0.000000,2.000000,0.000000,MUSD/unit,"9-t-class installer deorbit kit; no citable product exists at this scale. Extrapolated from Terminator Tape (50000-75000 USD/unit CubeSat-scale lower-bound analog flight-proven) and dragNET (2.8 kg module mass/design cited no public unit price) up to a custom EDT/sail plus attach interface for a multi-tonne derelict; upper bound is engineering judgment. [training-data extrapolation]",high +1.1,,cost_component_musd,,contingency_reserve_pct,,cost,20.000000,0.000000,20.000000,0.000000,% of cost,"Standard NASA development-phase cost-reserve guideline for Class C/D smallsat missions (NASA Cost Estimating Handbook v4.0; NASA reserves lesson 1780; NASA TMC Class C/D expectations); applied to the pre-reserve campaign cost in the campaign_cost_musd_with_reserves row. [web-verified 2026-07-11]",low +1.1,,cost_component_musd,,contingency_reserve_pct,,cost,25.000000,0.000000,25.000000,0.000000,% of cost,"Standard NASA development-phase cost-reserve guideline for Class C/D smallsat missions (NASA Cost Estimating Handbook v4.0; NASA reserves lesson 1780; NASA TMC Class C/D expectations); applied to the pre-reserve campaign cost in the campaign_cost_musd_with_reserves row. [web-verified 2026-07-11]",mid +1.1,,cost_component_musd,,contingency_reserve_pct,,cost,30.000000,0.000000,30.000000,0.000000,% of cost,"Standard NASA development-phase cost-reserve guideline for Class C/D smallsat missions (NASA Cost Estimating Handbook v4.0; NASA reserves lesson 1780; NASA TMC Class C/D expectations); applied to the pre-reserve campaign cost in the campaign_cost_musd_with_reserves row. [web-verified 2026-07-11]",high +1.1,,currency_anchor_derived,4,,,anchor_musd_per_cu,0.062781,0.000000,0.062781,0.000000,musd_per_cu,"anchor_musd_per_cu[low] = core_5_component / C_campaign_CU_p50; core = development(8.0000) + manufacturing_bus_unit(1.5000) + kit_unit_scaled_9t*4kits(0.4000) + C_launch_core(0.3500) + operations_3yr(1.0000) = 11.250000 MUSD; C_campaign_CU_p50(catalog=SL-16 / Zenit-2 second stage;baseline N=4)=179.195140 CU; anchor=0.062781 MUSD/CU. C_launch_core[low]=0.3500 MUSD (low=rideshare card low, mid=rideshare upper/heavier buy, high=dedicated Electron -- launch_dedicated_anchor feeds this high bound, width preserved not narrowed, B3). ONLY these 5 CU-mapped components are in the anchor core (Sec.10 Method, B2): ground_segment_3yr, licensing_fcc_application, licensing_fcc_annual*3yr and ssa_tracking (PLACEHOLDER) are ADDITIVE, outside the core (see campaign_cost_musd additives/base_total rows); insurance_launch_plus_1yr_pct and contingency_reserve_pct apply only in campaign_cost_musd[with_reserves]",low +1.1,,currency_anchor_derived,4,,,anchor_musd_per_cu,0.154580,0.000000,0.154580,0.000000,musd_per_cu,"anchor_musd_per_cu[mid] = core_5_component / C_campaign_CU_p50; core = development(20.0000) + manufacturing_bus_unit(3.0000) + kit_unit_scaled_9t*4kits(2.0000) + C_launch_core(0.7000) + operations_3yr(2.0000) = 27.700000 MUSD; C_campaign_CU_p50(catalog=SL-16 / Zenit-2 second stage;baseline N=4)=179.195140 CU; anchor=0.154580 MUSD/CU. C_launch_core[mid]=0.7000 MUSD (low=rideshare card low, mid=rideshare upper/heavier buy, high=dedicated Electron -- launch_dedicated_anchor feeds this high bound, width preserved not narrowed, B3). ONLY these 5 CU-mapped components are in the anchor core (Sec.10 Method, B2): ground_segment_3yr, licensing_fcc_application, licensing_fcc_annual*3yr and ssa_tracking (PLACEHOLDER) are ADDITIVE, outside the core (see campaign_cost_musd additives/base_total rows); insurance_launch_plus_1yr_pct and contingency_reserve_pct apply only in campaign_cost_musd[with_reserves]",mid +1.1,,currency_anchor_derived,4,,,anchor_musd_per_cu,0.421328,0.000000,0.421328,0.000000,musd_per_cu,"anchor_musd_per_cu[high] = core_5_component / C_campaign_CU_p50; core = development(50.0000) + manufacturing_bus_unit(6.0000) + kit_unit_scaled_9t*4kits(8.0000) + C_launch_core(7.5000) + operations_3yr(4.0000) = 75.500000 MUSD; C_campaign_CU_p50(catalog=SL-16 / Zenit-2 second stage;baseline N=4)=179.195140 CU; anchor=0.421328 MUSD/CU. C_launch_core[high]=7.5000 MUSD (low=rideshare card low, mid=rideshare upper/heavier buy, high=dedicated Electron -- launch_dedicated_anchor feeds this high bound, width preserved not narrowed, B3). ONLY these 5 CU-mapped components are in the anchor core (Sec.10 Method, B2): ground_segment_3yr, licensing_fcc_application, licensing_fcc_annual*3yr and ssa_tracking (PLACEHOLDER) are ADDITIVE, outside the core (see campaign_cost_musd additives/base_total rows); insurance_launch_plus_1yr_pct and contingency_reserve_pct apply only in campaign_cost_musd[with_reserves]",high +1.1,,campaign_cost_musd,4,,,core_5_component,11.250000,0.000000,11.250000,0.000000,MUSD,"campaign_cost_musd[core_5_component;low] = the 5 CU-mapped components (development+manufacturing_bus_unit+kit_unit_scaled_9t*4kits+C_launch_core+operations_3yr); identical numerator to currency_anchor_derived = 11.250000 MUSD",low +1.1,,campaign_cost_musd,4,,,additives,0.126645,0.000000,0.126645,0.000000,MUSD,"campaign_cost_musd[additives;low] = ground_segment_3yr + licensing_fcc_application + licensing_fcc_annual*3yr + ssa_tracking (PLACEHOLDER, 0); outside the CU model and outside the anchor core (Sec.10 Method) = 0.126645 MUSD",low +1.1,,campaign_cost_musd,4,,,base_total,11.376645,0.000000,11.376645,0.000000,MUSD,"campaign_cost_musd[base_total;low] = core_5_component(11.250000) + additives(0.126645) = 11.376645 MUSD",low +1.1,,campaign_cost_musd,4,,,with_reserves,13.764474,0.000000,13.764474,0.000000,MUSD,"campaign_cost_musd[with_reserves;low] = base_total(11.376645 MUSD) * (1+contingency_reserve_pct[low]/100=1.2000) + insurance_line(0.112500 MUSD); insurance_line = insurance_launch_plus_1yr_pct[low]/100=0.0500 * insured_value(manufacturing_bus_unit+kit_unit_scaled_9t*4kits+C_launch_core=2.250000 MUSD) = 0.112500 MUSD -- insurance scales with asset+launch value (N5), NOT multiplied over the multi-year opex in base_total",low +1.1,,campaign_cost_musd,4,,,core_5_component,27.700000,0.000000,27.700000,0.000000,MUSD,"campaign_cost_musd[core_5_component;mid] = the 5 CU-mapped components (development+manufacturing_bus_unit+kit_unit_scaled_9t*4kits+C_launch_core+operations_3yr); identical numerator to currency_anchor_derived = 27.700000 MUSD",mid +1.1,,campaign_cost_musd,4,,,additives,0.194145,0.000000,0.194145,0.000000,MUSD,"campaign_cost_musd[additives;mid] = ground_segment_3yr + licensing_fcc_application + licensing_fcc_annual*3yr + ssa_tracking (PLACEHOLDER, 0); outside the CU model and outside the anchor core (Sec.10 Method) = 0.194145 MUSD",mid +1.1,,campaign_cost_musd,4,,,base_total,27.894145,0.000000,27.894145,0.000000,MUSD,"campaign_cost_musd[base_total;mid] = core_5_component(27.700000) + additives(0.194145) = 27.894145 MUSD",mid +1.1,,campaign_cost_musd,4,,,with_reserves,35.323681,0.000000,35.323681,0.000000,MUSD,"campaign_cost_musd[with_reserves;mid] = base_total(27.894145 MUSD) * (1+contingency_reserve_pct[mid]/100=1.2500) + insurance_line(0.456000 MUSD); insurance_line = insurance_launch_plus_1yr_pct[mid]/100=0.0800 * insured_value(manufacturing_bus_unit+kit_unit_scaled_9t*4kits+C_launch_core=5.700000 MUSD) = 0.456000 MUSD -- insurance scales with asset+launch value (N5), NOT multiplied over the multi-year opex in base_total",mid +1.1,,campaign_cost_musd,4,,,core_5_component,75.500000,0.000000,75.500000,0.000000,MUSD,"campaign_cost_musd[core_5_component;high] = the 5 CU-mapped components (development+manufacturing_bus_unit+kit_unit_scaled_9t*4kits+C_launch_core+operations_3yr); identical numerator to currency_anchor_derived = 75.500000 MUSD",high +1.1,,campaign_cost_musd,4,,,additives,0.261645,0.000000,0.261645,0.000000,MUSD,"campaign_cost_musd[additives;high] = ground_segment_3yr + licensing_fcc_application + licensing_fcc_annual*3yr + ssa_tracking (PLACEHOLDER, 0); outside the CU model and outside the anchor core (Sec.10 Method) = 0.261645 MUSD",high +1.1,,campaign_cost_musd,4,,,base_total,75.761645,0.000000,75.761645,0.000000,MUSD,"campaign_cost_musd[base_total;high] = core_5_component(75.500000) + additives(0.261645) = 75.761645 MUSD",high +1.1,,campaign_cost_musd,4,,,with_reserves,101.070138,0.000000,101.070138,0.000000,MUSD,"campaign_cost_musd[with_reserves;high] = base_total(75.761645 MUSD) * (1+contingency_reserve_pct[high]/100=1.3000) + insurance_line(2.580000 MUSD); insurance_line = insurance_launch_plus_1yr_pct[high]/100=0.1200 * insured_value(manufacturing_bus_unit+kit_unit_scaled_9t*4kits+C_launch_core=21.500000 MUSD) = 2.580000 MUSD -- insurance scales with asset+launch value (N5), NOT multiplied over the multi-year opex in base_total",high +1.1,SL-16 / Zenit-2 second stage,cost_per_removal_musd,4,,,cost_per_removal,2.812500,2.812498,2.812500,3.749542,MUSD_per_removal,"cost_per_removal_musd[low] = cost_per_removal_CU(catalog=SL-16 / Zenit-2 second stage;baseline N=4; p05=44.7988 p50=44.7988 p95=59.7244) x anchor_musd_per_cu[low]=0.062781 MUSD/CU",low +1.1,SL-16 / Zenit-2 second stage,cost_per_removal_musd,4,,,cost_per_removal,6.925001,6.924995,6.925001,9.232205,MUSD_per_removal,"cost_per_removal_musd[mid] = cost_per_removal_CU(catalog=SL-16 / Zenit-2 second stage;baseline N=4; p05=44.7988 p50=44.7988 p95=59.7244) x anchor_musd_per_cu[mid]=0.154580 MUSD/CU",mid +1.1,SL-16 / Zenit-2 second stage,cost_per_removal_musd,4,,,cost_per_removal,18.875003,18.874988,18.875003,25.163591,MUSD_per_removal,"cost_per_removal_musd[high] = cost_per_removal_CU(catalog=SL-16 / Zenit-2 second stage;baseline N=4; p05=44.7988 p50=44.7988 p95=59.7244) x anchor_musd_per_cu[high]=0.421328 MUSD/CU",high +1.1,SL-16 / Zenit-2 second stage,tornado,,c_dev_cu,,cost_per_removal_swing,15.000000,0.000000,15.000000,0.000000,CU_per_removal,"low=37.2988;high=52.2988 (+/-30% one-at-a-time)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_dev_cu,,fom_swing,53.981127,0.000000,53.981127,0.000000,kg_per_CU,"low=188.20985;high=134.22872 (spatial FoM)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_bus_cu,,cost_per_removal_swing,4.543711,0.000000,4.543711,0.000000,CU_per_removal,"low=42.5269;high=47.0706 (+/-30% one-at-a-time)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_bus_cu,,fom_swing,15.934322,0.000000,15.934322,0.000000,kg_per_CU,"low=165.07185;high=149.13753 (spatial FoM)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_launch_cu_per_kg,,cost_per_removal_swing,3.722400,0.000000,3.722400,0.000000,CU_per_removal,"low=42.9376;high=46.6600 (+/-30% one-at-a-time)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_launch_cu_per_kg,,fom_swing,13.043009,0.000000,13.043009,0.000000,kg_per_CU,"low=163.49310;high=150.45009 (spatial FoM)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_kit_cu,,cost_per_removal_swing,2.400000,0.000000,2.400000,0.000000,CU_per_removal,"low=43.5988;high=45.9988 (+/-30% one-at-a-time)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_kit_cu,,fom_swing,8.400932,0.000000,8.400932,0.000000,kg_per_CU,"low=161.01364;high=152.61270 (spatial FoM)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_ops_cu_per_day,,cost_per_removal_swing,1.213164,0.000000,1.213164,0.000000,CU_per_removal,"low=44.1922;high=45.4054 (+/-30% one-at-a-time)", +1.1,SL-16 / Zenit-2 second stage,tornado,,c_ops_cu_per_day,,fom_swing,4.244275,0.000000,4.244275,0.000000,kg_per_CU,"low=158.85153;high=154.60725 (spatial FoM)", +1.1,,currency_anchor,,,,cu_to_musd_range,0.000000,0.000000,0.000000,0.000000,musd_per_cu,"SUPERSEDED sentinel (CostConfig::cu_to_musd_low/high stay 0.0 for schema continuity); the anchor is now DERIVED AT RUNTIME from the WP14 itemized cost table (see currency_anchor_derived rows) -- no point-value dollar figure is claimed", diff --git a/generated/wp6_cost_summary.md b/generated/wp6_cost_summary.md index 89af5d4..449d5c4 100644 --- a/generated/wp6_cost_summary.md +++ b/generated/wp6_cost_summary.md @@ -1,6 +1,6 @@ # WP6 Parametric cost model + FoM — summary -Primary output is in **relative cost units (CU)**; **no point-value dollar figure is claimed** (R6/D10) -- the CU->currency anchor is a PLACEHOLDER cited range, filled in WP7. All cost/FoM parameters are PLACEHOLDER (see `CostConfig`). Cost, cost/removal and FoM are propagated **per run** from the WP5 campaign (schema 1.0) and reported as p05/p50/p95. Regenerate with `adsc_cost`. +Primary output is in **relative cost units (CU)**; **no point-value dollar figure is claimed** (R6/D10) -- the CU->currency anchor is now DERIVED AT RUNTIME from the WP14 cited itemized cost table (see the `currency_anchor_derived` rows in the CSV), per cost_scenario (low/mid/high); `CostConfig::cu_to_musd_low/high` are superseded sentinels left at 0.0. All cost/FoM parameters are PLACEHOLDER (see `CostConfig`). Cost, cost/removal and FoM are propagated **per run** from the WP5 campaign (schema 1.0) and reported as p05/p50/p95. Regenerate with `adsc_cost`. ## SL-16 / Zenit-2 second stage diff --git a/include/adsc/cost.hpp b/include/adsc/cost.hpp index 3c8a59d..0182d04 100644 --- a/include/adsc/cost.hpp +++ b/include/adsc/cost.hpp @@ -22,23 +22,28 @@ namespace adsc { // C_campaign = C_dev + C_bus(m_dry) + N*C_kit + C_launch(m_wet, band) // + C_ops(T) // The PRIMARY output is in RELATIVE cost units (CU). Absolute currency is NEVER -// emitted as a point value (R6/D10): only a cited RANGE via the PLACEHOLDER -// CU->currency anchor, left unfilled here (filled with citations in WP7). +// emitted as a point value (R6/D10): only cited low/mid/high RANGES. WP14 +// adds a cited itemized USD table (`wp14_cost_items()`) and derives the +// CU->currency anchor from it AT RUNTIME (see write_cost_summary_csv in +// cost.cpp) -- `CostConfig::cu_to_musd_low/high` stay unused sentinels. // // Figure of merit (spec §4): FoM = sum_i m_i*w(h_i) / C_campaign, a // debris-risk-reduction-per-cost (cascade-fuel) metric. Reported under >=2 // normalized congestion weightings; the ranking disagreement is open trade T5. +// WP14 also reports the inverse orientation, cost_per_risk_equiv_mass = +// C_campaign / sum_i m_i*w(h_i), as CU/kg and (mid cost_scenario) MUSD/kg. // // Distribution propagation: cost, cost/removal and FoM are computed PER RUN // from the WP5 per-run removals/mission-time and reported as p05/p50/p95 -- // never mean-only. // -// All parameters live in CostConfig and are marked PLACEHOLDER (R10). WP6 -// implements the cost model / amortization / tornado / FoM only -- no WP7 -// evidence pack, no charts. +// The relative CU coefficients and both FoM weight tables in CostConfig are +// PLACEHOLDER (R10) and untouched by WP14. WP6/WP14 implement the cost model +// / amortization / tornado / FoM / itemized-USD-anchor only -- no evidence +// pack, no charts (those are separate tools that consume this CSV). // ============================================================================ -inline const char* wp6_schema_version() { return "1.0"; } +inline const char* wp6_schema_version() { return "1.1"; } // A congestion-weight anchor: normalized (peak ~1) weight of a target band at // `altitude_km` under each weighting. PLACEHOLDER values -- fill with citations @@ -56,6 +61,29 @@ struct WeightAnchor { enum class Weighting { SpatialDensity, Criticality }; const char* weighting_label(Weighting w); +// WP14 itemized absolute-cost row (low/mid/high, cited or PLACEHOLDER). Source: +// `_tasks_local/wp14-cost-sources.md` (retrieval date 2026-07-11); every row's +// `note` ends with one of "[web-verified 2026-07-11]" / "[web-verified +// (analog) 2026-07-11]" / "[training-data extrapolation]" / "[PLACEHOLDER]" +// (D10: sourced-or-PLACEHOLDER, never fabricated). This table is separate +// from, and does not alter, the relative CU `CostConfig` coefficients above +// -- it is consumed only to derive the currency-anchor rows written into +// wp6_cost_summary.csv (schema 1.1, WP14). +struct CostItemUsd { + const char* item; // stable machine-readable row id (snake_case) + double low_musd; // low estimate, in `unit` + double mid_musd; // mid estimate, in `unit` + double high_musd; // high estimate, in `unit` + const char* unit; // e.g. "MUSD", "MUSD/yr", "MUSD/unit", "% of cost" + const char* note; // ASCII provenance note, ends with a bracketed flag +}; + +// The static WP14 itemized cost table (development, manufacturing, launch, +// ground, ops, SSA, insurance/licensing, kit unit, contingency -- see +// cost.cpp). `*count` is set to the row count; the returned pointer is valid +// for the process lifetime (static storage). +const CostItemUsd* wp14_cost_items(int* count); + // All PLACEHOLDER; grouped so nothing is a bare literal in the cost logic (R10). struct CostConfig { // --- relative cost-unit (CU) coefficients --- @@ -79,11 +107,17 @@ struct CostConfig { // Tornado sensitivity half-width (fractional one-at-a-time perturbation). double tornado_delta_frac = 0.30; // PLACEHOLDER +/-30% - // CU -> currency anchor RANGE. Left 0 => NOT filled; no dollar value is - // emitted. Fill with a CITED range (public launch $/kg + smallsat bus cost - // ranges) in WP7; a point-value dollar claim is forbidden (R6/D10). - double cu_to_musd_low = 0.0; // PLACEHOLDER (cited range, WP7) - double cu_to_musd_high = 0.0; // PLACEHOLDER (cited range, WP7) + // CU -> currency anchor RANGE. SUPERSEDED by WP14: the anchor is now + // DERIVED AT RUNTIME (see `wp14_cost_items()` + write_cost_summary_csv in + // cost.cpp) from the cited itemized USD table, per cost_scenario + // (low/mid/high), and written into the CSV as `currency_anchor_derived` + // rows -- it is no longer read from these two fields. These fields stay + // intentionally unused sentinels at 0.0 (not repurposed, not removed) so + // the pre-WP14 "unfilled anchor" config shape is preserved for any caller + // still reading `CostConfig` directly; a point-value dollar claim remains + // forbidden everywhere (R6/D10) -- only cited low/mid/high ranges appear. + double cu_to_musd_low = 0.0; // unused sentinel; anchor is WP14 runtime-derived + double cu_to_musd_high = 0.0; // unused sentinel; anchor is WP14 runtime-derived // Normalized congestion-weight table (PLACEHOLDER; cite on fill). Peaks // differ by column so band ranking flips between weightings (T5). diff --git a/src/cost.cpp b/src/cost.cpp index 459d201..119b892 100644 --- a/src/cost.cpp +++ b/src/cost.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,6 +12,115 @@ namespace adsc { +// ============================================================================ +// WP14 itemized absolute-cost table -- transcribed EXACTLY from +// `_tasks_local/wp14-cost-sources.md` (retrieval date 2026-07-11). Every note +// ends with its verified-status flag; D10 (sourced-or-PLACEHOLDER, never +// fabricated). Units vary by row: MUSD, MUSD/yr, MUSD/unit, or a percentage +// applied elsewhere (never as a bare point-dollar figure -- R6/D10). +// ============================================================================ +namespace { + +const CostItemUsd kWp14CostItems[] = { + {"development", 8.0, 20.0, 50.0, "MUSD", + "Smallsat bus + RPO/GNC dev NRE allocated to the campaign. Anchors: govt " + "CubeSat/smallsat missions ~3-30 MUSD (New Space Economy survey); " + "Aerospace Corp SSCM19 parametric CER methodology basis " + "(aerospace.org/sscm); RPO-servicer ceiling analog Astroscale ELSA-d " + "~100 MUSD total two-spacecraft mission (184 kg servicer + 16 kg " + "client). Low = lean COTS bus + heritage GNC; mid = smallsat band with " + "RPO uplift; high = RPO-heavy half of the ELSA-d ceiling. " + "[web-verified (analog) 2026-07-11]"}, + {"manufacturing_bus_unit", 1.5, 3.0, 6.0, "MUSD", + "One ~25 kg-dry RPO-capable recurring flight bus (NRE lives in the " + "development row). Anchors: high-performance smallsat bus > 1 MUSD (New " + "Space Economy survey); recurring-unit learning rates 95pct (units " + "1-10) / 90pct (11-50) / 85pct (>50) (ICEAA Learning Rate Sensitivity " + "Model). [web-verified (analog) 2026-07-11]"}, + {"launch_rideshare_sso", 0.35, 0.5, 0.7, "MUSD", + "SpaceX rideshare SSO price card Feb 2026: base 350000 USD for up to " + "50 kg to SSO plus 7000 USD/kg above 50 kg; a ~44 kg servicer fits the " + "50 kg block. Mid/high add mid-inclination or dedicated-deployer/" + "scheduling premiums (SpaceX directs non-SSO orbits to inquiry). " + "[web-verified 2026-07-11]"}, + {"launch_dedicated_anchor", 7.1, 7.5, 7.5, "MUSD", + "Context row NOT additive with launch_rideshare_sso -- alternative " + "anchor if a dedicated orbit/schedule is required instead of " + "rideshare; feeds the launch-HIGH bound (7.5 MUSD) of the anchor " + "core's C_launch term (Sec.10 Method / adjudicated B3 -- width " + "preserved, not narrowed). Rocket Lab Electron: ~300 kg to LEO " + "dedicated mission; Q1-2025 realized avg revenue/launch 7.1 MUSD (SEC " + "10-Q); ~7.5 MUSD nominal dedicated-mission price (SpaceNexus " + "explainer). [web-verified 2026-07-11]"}, + {"ground_segment_3yr", 0.06, 0.12, 0.18, "MUSD", + "Derived: AWS Ground Station published per-minute rates 3 USD/min " + "(narrowband reserved) to 22 USD/min (wideband on-demand) at 4-6 " + "passes/day imply ~20000-60000 USD/yr all-in ground segment (AWS " + "billing docs + ElasticScale / New Space Economy secondary " + "confirmation); x3 yr campaign span gives 0.06-0.18 MUSD. " + "[web-verified 2026-07-11]"}, + {"operations_3yr", 1.0, 2.0, 4.0, "MUSD", + "3-yr campaign routine ops: 1-2 FTE at 150000-200000 USD/yr " + "fully-burdened plus mission-ops software 50000-200000 USD/yr " + "(SpaceNexus mission-cost guide); ops roughly 8pct of total mission " + "cost / 20pct of bus cost. C_ops (CU) tracks ~4 sim-days not the real " + "multi-year ops span -- this row is the real-dollar multi-year " + "figure. [web-verified (analog) 2026-07-11]"}, + {"ssa_tracking", 0.0, 0.0, 0.0, "MUSD/yr", + "LeoLabs declines to publicly quote; subscription is customized by " + "fleet size and monitoring cadence (SpaceNews; CNBC). Zero-cost floor " + "pointer: USSF/18th SDS free public conjunction data messages (CDMs). " + "0/0/0 is an UNFILLED placeholder not a claim the service is free -- " + "do not invent a subscription figure. [PLACEHOLDER]"}, + {"insurance_launch_plus_1yr_pct", 5.0, 8.0, 12.0, "% insured value", + "Launch-plus-first-year-in-orbit premium as pct of insured value; " + "range rises after major-loss years (2023-24 hardened the market) or " + "for unproven vehicles (Orbital Radar; Taylor Wessing; SpaceNews). " + "Many smallsat missions self-insure -- applied only in the " + "campaign_cost_musd_with_reserves row never in the base cost. " + "[web-verified 2026-07-11]"}, + {"licensing_fcc_application", 0.030, 0.0375, 0.045, "MUSD", + "FCC Part 25 application fee one-time (not annual): streamlined " + "smallsat 30000 USD (6-yr license term) is low; standard 45000 USD " + "(15-yr term) is high; mid is the arithmetic midpoint (no published " + "intermediate tier). [web-verified 2026-07-11]"}, + {"licensing_fcc_annual", 0.012215, 0.012215, 0.012215, "MUSD/yr", + "FCC annual regulatory fee NGSO Small Satellite category FY2024: " + "12215 USD per license/call sign (Federal Register FY2024 fee " + "assessment; FCC 25-31); category explicitly includes On-Orbit " + "Servicing (OOS) and Rendezvous and Proximity Operations (RPO) -- the " + "ADSC servicer class. Single published figure flat across low/mid/" + "high. [web-verified 2026-07-11]"}, + {"kit_unit_scaled_9t", 0.1, 0.5, 2.0, "MUSD/unit", + "9-t-class installer deorbit kit; no citable product exists at this " + "scale. Extrapolated from Terminator Tape (50000-75000 USD/unit " + "CubeSat-scale lower-bound analog flight-proven) and dragNET (2.8 kg " + "module mass/design cited no public unit price) up to a custom " + "EDT/sail plus attach interface for a multi-tonne derelict; upper " + "bound is engineering judgment. [training-data extrapolation]"}, + {"contingency_reserve_pct", 20.0, 25.0, 30.0, "% of cost", + "Standard NASA development-phase cost-reserve guideline for Class " + "C/D smallsat missions (NASA Cost Estimating Handbook v4.0; NASA " + "reserves lesson 1780; NASA TMC Class C/D expectations); applied to " + "the pre-reserve campaign cost in the campaign_cost_musd_with_" + "reserves row. [web-verified 2026-07-11]"}, +}; +constexpr int kNumWp14CostItems = + static_cast(sizeof(kWp14CostItems) / sizeof(kWp14CostItems[0])); + +const CostItemUsd* find_wp14_item(const char* name) { + for (int i = 0; i < kNumWp14CostItems; ++i) + if (std::strcmp(kWp14CostItems[i].item, name) == 0) return &kWp14CostItems[i]; + return nullptr; +} + +} // namespace + +const CostItemUsd* wp14_cost_items(int* count) { + if (count) *count = kNumWp14CostItems; + return kWp14CostItems; +} + const char* weighting_label(Weighting w) { switch (w) { case Weighting::SpatialDensity: return "spatial_density"; @@ -287,16 +397,109 @@ void write_cost_summary_csv(const std::string& path, const CostConfig& cc, if (!f) return; std::fprintf(f, "schema_version,catalog,record_type,n_kits,param,weighting,metric," - "estimate,p05,p50,p95,units,notes\n"); + "estimate,p05,p50,p95,units,notes,cost_scenario\n"); + // `cost_scenario` (WP14, schema 1.1) is an ADDITIVE trailing column: every + // pre-WP14 call site below omits it and gets the default "" (blank), so + // every existing row's VALUES are unchanged -- only a new blank field is + // appended. Only the new WP14 rows further down pass a real scenario. const auto emit = [&](const char* cat, const char* rt, const std::string& nk, const char* param, const char* weighting, const char* metric, double est, double p05, double p50, - double p95, const char* units, const char* notes) { - std::fprintf(f, "%s,%s,%s,%s,%s,%s,%s,%.6f,%.6f,%.6f,%.6f,%s,\"%s\"\n", + double p95, const char* units, const char* notes, + const char* cost_scenario = "") { + std::fprintf(f, "%s,%s,%s,%s,%s,%s,%s,%.6f,%.6f,%.6f,%.6f,%s,\"%s\",%s\n", wp6_schema_version(), cat, rt, nk.c_str(), param, weighting, - metric, est, p05, p50, p95, units, notes); + metric, est, p05, p50, p95, units, notes, cost_scenario); }; + // ------------------------------------------------------------- WP14 setup + // Itemized USD table + the campaign-level anchor derived from it. The + // anchor's denominator (C_campaign_CU_p50 at the baseline N) is read from + // the SAME sweep already computed above for catalog A -- nothing here + // re-runs the campaign or touches a CU coefficient. + int wp14_n_items = 0; + const CostItemUsd* wp14_items = wp14_cost_items(&wp14_n_items); + const char* kScenario[3] = {"low", "mid", "high"}; + + const DebrisCatalog catA = catalog_A(); + const CatalogCost* cqA = nullptr; + for (const CatalogCost& cq : report.catalogs) + if (std::strcmp(cq.catalog.name, catA.name) == 0) { cqA = &cq; break; } + const AmortPoint* baseAp = nullptr; + if (cqA) + for (const AmortPoint& a : cqA->amortization) + if (a.n_kits == cqA->baseline_n_kits) { baseAp = &a; break; } + const double denom_cu = baseAp ? baseAp->campaign_cost_p50 : 0.0; + const int baseline_n = cqA ? cqA->baseline_n_kits : 0; + const std::string baseline_nk = std::to_string(baseline_n); + + const CostItemUsd* it_dev = find_wp14_item("development"); + const CostItemUsd* it_bus = find_wp14_item("manufacturing_bus_unit"); + const CostItemUsd* it_launch = find_wp14_item("launch_rideshare_sso"); + const CostItemUsd* it_launch_ded = find_wp14_item("launch_dedicated_anchor"); + const CostItemUsd* it_ground = find_wp14_item("ground_segment_3yr"); + const CostItemUsd* it_ops = find_wp14_item("operations_3yr"); + const CostItemUsd* it_fcc_app = find_wp14_item("licensing_fcc_application"); + const CostItemUsd* it_fcc_annual = find_wp14_item("licensing_fcc_annual"); + const CostItemUsd* it_kit = find_wp14_item("kit_unit_scaled_9t"); + const CostItemUsd* it_ssa = find_wp14_item("ssa_tracking"); + const CostItemUsd* it_insurance = find_wp14_item("insurance_launch_plus_1yr_pct"); + const CostItemUsd* it_contingency = find_wp14_item("contingency_reserve_pct"); + const auto scenario_val = [](const CostItemUsd* it, int s) { + if (!it) return 0.0; + return s == 0 ? it->low_musd : (s == 1 ? it->mid_musd : it->high_musd); + }; + + // C_launch term for the anchor CORE (Sec.10 Method, adjudicated B3): + // width preserved on purpose, NOT narrowed artificially -- low = the + // rideshare card low (launch_rideshare_sso.low), mid = the rideshare + // upper / heavier-rideshare-buy figure (launch_rideshare_sso.high), high + // = the dedicated Electron anchor (launch_dedicated_anchor.high, which + // equals .mid, both 7.5 MUSD). + const double c_launch_core[3] = { + it_launch ? it_launch->low_musd : 0.0, + it_launch ? it_launch->high_musd : 0.0, + it_launch_ded ? it_launch_ded->high_musd : 0.0, + }; + + // Anchor CORE = exactly the 5 CU-mapped components ONLY (Sec.10 Method, + // adjudicated B2): development + manufacturing_bus_unit + + // kit_unit_scaled_9t x baseline_n kits + C_launch [c_launch_core above] + // + operations_3yr. ADDITIVES sit on top of the core and are OUTSIDE the + // anchor numerator: ground_segment_3yr, licensing_fcc_application, + // licensing_fcc_annual x3 yr, and the PLACEHOLDER ssa_tracking row + // (value 0 either way). base_total = core + additives (B5-structure). + double core_musd[3]; + double additives_musd[3]; + double base_total_musd[3]; + double anchor_musd_per_cu[3]; + double insured_value_musd[3]; + double insurance_line_musd[3]; + double with_reserves_musd[3]; + for (int s = 0; s < 3; ++s) { + core_musd[s] = scenario_val(it_dev, s) + scenario_val(it_bus, s) + + scenario_val(it_kit, s) * static_cast(baseline_n) + + c_launch_core[s] + scenario_val(it_ops, s); + additives_musd[s] = scenario_val(it_ground, s) + scenario_val(it_fcc_app, s) + + scenario_val(it_fcc_annual, s) * 3.0 + scenario_val(it_ssa, s); + base_total_musd[s] = core_musd[s] + additives_musd[s]; + anchor_musd_per_cu[s] = (denom_cu > 0.0) ? core_musd[s] / denom_cu : 0.0; + + // N5-insurance: insurance is an ADDITIVE line scaled to the + // asset+launch insured value (matching the source's illustration), + // NOT a multiplicative factor over base_total -- base_total also + // carries the multi-year ops/ground/licensing opex, which is not + // part of what gets insured. + insured_value_musd[s] = scenario_val(it_bus, s) + + scenario_val(it_kit, s) * static_cast(baseline_n) + + c_launch_core[s]; + const double insurance_pct = scenario_val(it_insurance, s); + insurance_line_musd[s] = (insurance_pct / 100.0) * insured_value_musd[s]; + const double contingency_pct = scenario_val(it_contingency, s); + with_reserves_musd[s] = + base_total_musd[s] * (1.0 + contingency_pct / 100.0) + insurance_line_musd[s]; + } + for (const CatalogCost& cq : report.catalogs) { for (const AmortPoint& a : cq.amortization) { const std::string nk = std::to_string(a.n_kits); @@ -322,6 +525,36 @@ void write_cost_summary_csv(const std::string& path, const CostConfig& cc, emit(cq.catalog.name, "fom", "", "", weighting_label(fr.weighting), "band_weight", fr.band_weight, fr.band_weight, fr.band_weight, fr.band_weight, "normalized", "w(h) PLACEHOLDER; cite on fill"); + + // WP14 (e): inverse-FoM orientation, reusing the fom_p05/p50/p95 + // already computed above (same congestion_weight() call, same + // per-run mass/cost -- no weight is recomputed here). 1/x is + // strictly decreasing for x>0, so the percentile order reverses: + // each cost_per_risk_equiv_mass percentile is the reciprocal of + // the FoM percentile BY CONSTRUCTION -- it is NOT the percentile + // of an independent per-run-reciprocal distribution (order + // statistics interpolate, so those two are not generally equal); + // p05/p95 swap to the reciprocal of fom's p95/p05. + const double risk_p50 = (fr.fom_p50 > 0.0) ? 1.0 / fr.fom_p50 : 0.0; + const double risk_p05 = (fr.fom_p95 > 0.0) ? 1.0 / fr.fom_p95 : 0.0; + const double risk_p95 = (fr.fom_p05 > 0.0) ? 1.0 / fr.fom_p05 : 0.0; + emit(cq.catalog.name, "fom", "", "", weighting_label(fr.weighting), + "cost_per_risk_equiv_mass", risk_p50, risk_p05, risk_p50, risk_p95, + "CU_per_kg", + "= C_campaign/sum(m_i*w(h_i)) = 1/fom per the same per-run FoM " + "machinery (weights not recomputed); p50 is the reciprocal of " + "the fom_p50 percentile BY CONSTRUCTION, NOT the percentile of " + "per-run reciprocals (order statistics interpolate); p05/p95 " + "are reciprocals of fom_p95/fom_p05 (order reverses for a " + "decreasing transform)"); + emit(cq.catalog.name, "fom", "", "", weighting_label(fr.weighting), + "cost_per_risk_equiv_mass_musd", + risk_p50 * anchor_musd_per_cu[1], risk_p05 * anchor_musd_per_cu[1], + risk_p50 * anchor_musd_per_cu[1], risk_p95 * anchor_musd_per_cu[1], + "MUSD_per_kg", + "= cost_per_risk_equiv_mass[CU_per_kg] x anchor_musd_per_cu" + "[cost_scenario=mid]; mid-scenario anchor only to bound row count", + "mid"); } const std::string bnk = std::to_string(cq.baseline_n_kits); for (const CostComponent& comp : cq.baseline_components) @@ -330,6 +563,122 @@ void write_cost_summary_csv(const std::string& path, const CostConfig& cc, "baseline breakdown at median mission time"); } + // ------------------------------------------------------- WP14 (a): items + for (int i = 0; i < wp14_n_items; ++i) { + const CostItemUsd& it = wp14_items[i]; + for (int s = 0; s < 3; ++s) { + const double v = scenario_val(&it, s); + emit("", "cost_component_musd", "", it.item, "", "cost", v, 0.0, v, 0.0, + it.unit, it.note, kScenario[s]); + } + } + + // ------------------------------------------- WP14 (b): derived CU anchor + for (int s = 0; s < 3; ++s) { + char note[1024]; + std::snprintf(note, sizeof(note), + "anchor_musd_per_cu[%s] = core_5_component / C_campaign_CU_p50; " + "core = development(%.4f) + manufacturing_bus_unit(%.4f) + " + "kit_unit_scaled_9t*%dkits(%.4f) + C_launch_core(%.4f) + " + "operations_3yr(%.4f) = %.6f MUSD; C_campaign_CU_p50(catalog=%s;" + "baseline N=%d)=%.6f CU; anchor=%.6f MUSD/CU. C_launch_core[%s]=" + "%.4f MUSD (low=rideshare card low, mid=rideshare upper/heavier " + "buy, high=dedicated Electron -- launch_dedicated_anchor feeds " + "this high bound, width preserved not narrowed, B3). ONLY these " + "5 CU-mapped components are in the anchor core (Sec.10 Method, " + "B2): ground_segment_3yr, licensing_fcc_application, " + "licensing_fcc_annual*3yr and ssa_tracking (PLACEHOLDER) are " + "ADDITIVE, outside the core (see campaign_cost_musd additives/" + "base_total rows); insurance_launch_plus_1yr_pct and " + "contingency_reserve_pct apply only in campaign_cost_musd" + "[with_reserves]", + kScenario[s], scenario_val(it_dev, s), scenario_val(it_bus, s), + baseline_n, scenario_val(it_kit, s) * static_cast(baseline_n), + c_launch_core[s], scenario_val(it_ops, s), core_musd[s], catA.name, + baseline_n, denom_cu, anchor_musd_per_cu[s], kScenario[s], + c_launch_core[s]); + emit("", "currency_anchor_derived", baseline_nk, "", "", "anchor_musd_per_cu", + anchor_musd_per_cu[s], 0.0, anchor_musd_per_cu[s], 0.0, "musd_per_cu", + note, kScenario[s]); + } + + // --------------------------------------------- WP14 (c): campaign cost + // Four rows per scenario, each separately visible (B5-structure) so the + // anchor's basis is auditable straight from the CSV: core_5_component + // (== currency_anchor_derived numerator), additives (outside the CU + // model, outside the anchor), base_total = core + additives, and + // with_reserves (N5-insurance: contingency multiplies base_total, + // insurance is an ADDITIVE line on insured_value, not a multiplier over + // the whole base_total -- see the with_reserves note below). + for (int s = 0; s < 3; ++s) { + char note_core[320]; + std::snprintf(note_core, sizeof(note_core), + "campaign_cost_musd[core_5_component;%s] = the 5 CU-mapped " + "components (development+manufacturing_bus_unit+" + "kit_unit_scaled_9t*%dkits+C_launch_core+operations_3yr); " + "identical numerator to currency_anchor_derived = %.6f MUSD", + kScenario[s], baseline_n, core_musd[s]); + emit("", "campaign_cost_musd", baseline_nk, "", "", "core_5_component", + core_musd[s], 0.0, core_musd[s], 0.0, "MUSD", note_core, kScenario[s]); + + char note_add[320]; + std::snprintf(note_add, sizeof(note_add), + "campaign_cost_musd[additives;%s] = ground_segment_3yr + " + "licensing_fcc_application + licensing_fcc_annual*3yr + " + "ssa_tracking (PLACEHOLDER, 0); outside the CU model and outside " + "the anchor core (Sec.10 Method) = %.6f MUSD", + kScenario[s], additives_musd[s]); + emit("", "campaign_cost_musd", baseline_nk, "", "", "additives", + additives_musd[s], 0.0, additives_musd[s], 0.0, "MUSD", note_add, + kScenario[s]); + + char note_base[320]; + std::snprintf(note_base, sizeof(note_base), + "campaign_cost_musd[base_total;%s] = core_5_component(%.6f) + " + "additives(%.6f) = %.6f MUSD", + kScenario[s], core_musd[s], additives_musd[s], base_total_musd[s]); + emit("", "campaign_cost_musd", baseline_nk, "", "", "base_total", + base_total_musd[s], 0.0, base_total_musd[s], 0.0, "MUSD", note_base, + kScenario[s]); + + char note2[640]; + std::snprintf(note2, sizeof(note2), + "campaign_cost_musd[with_reserves;%s] = base_total(%.6f MUSD) * " + "(1+contingency_reserve_pct[%s]/100=%.4f) + insurance_line" + "(%.6f MUSD); insurance_line = insurance_launch_plus_1yr_pct[%s]" + "/100=%.4f * insured_value(manufacturing_bus_unit+" + "kit_unit_scaled_9t*%dkits+C_launch_core=%.6f MUSD) = %.6f MUSD " + "-- insurance scales with asset+launch value (N5), NOT " + "multiplied over the multi-year opex in base_total", + kScenario[s], base_total_musd[s], kScenario[s], + 1.0 + scenario_val(it_contingency, s) / 100.0, insurance_line_musd[s], + kScenario[s], scenario_val(it_insurance, s) / 100.0, baseline_n, + insured_value_musd[s], insurance_line_musd[s]); + emit("", "campaign_cost_musd", baseline_nk, "", "", "with_reserves", + with_reserves_musd[s], 0.0, with_reserves_musd[s], 0.0, "MUSD", note2, + kScenario[s]); + } + + // --------------------------------------- WP14 (d): cost/removal in MUSD + for (int s = 0; s < 3; ++s) { + const double p05 = baseAp ? baseAp->cost_per_removal_p05 * anchor_musd_per_cu[s] : 0.0; + const double p50 = baseAp ? baseAp->cost_per_removal_p50 * anchor_musd_per_cu[s] : 0.0; + const double p95 = baseAp ? baseAp->cost_per_removal_p95 * anchor_musd_per_cu[s] : 0.0; + char note[384]; + std::snprintf(note, sizeof(note), + "cost_per_removal_musd[%s] = cost_per_removal_CU(catalog=%s;" + "baseline N=%d; p05=%.4f p50=%.4f p95=%.4f) x " + "anchor_musd_per_cu[%s]=%.6f MUSD/CU", + kScenario[s], catA.name, baseline_n, + baseAp ? baseAp->cost_per_removal_p05 : 0.0, + baseAp ? baseAp->cost_per_removal_p50 : 0.0, + baseAp ? baseAp->cost_per_removal_p95 : 0.0, kScenario[s], + anchor_musd_per_cu[s]); + emit(catA.name, "cost_per_removal_musd", baseline_nk, "", "", + "cost_per_removal", p50, p05, p50, p95, "MUSD_per_removal", note, + kScenario[s]); + } + for (const TornadoRow& tr : report.tornado) { char note[128]; std::snprintf(note, sizeof(note), "low=%.4f;high=%.4f (+/-%.0f%% one-at-a-time)", @@ -347,7 +696,10 @@ void write_cost_summary_csv(const std::string& path, const CostConfig& cc, emit("", "currency_anchor", "", "", "", "cu_to_musd_range", cc.cu_to_musd_low, cc.cu_to_musd_low, 0.0, cc.cu_to_musd_high, "musd_per_cu", - "PLACEHOLDER: fill a CITED range in WP7; no point-value dollar figure is claimed"); + "SUPERSEDED sentinel (CostConfig::cu_to_musd_low/high stay 0.0 for " + "schema continuity); the anchor is now DERIVED AT RUNTIME from the " + "WP14 itemized cost table (see currency_anchor_derived rows) -- no " + "point-value dollar figure is claimed"); std::fclose(f); } @@ -361,11 +713,14 @@ void write_cost_summary_md(const std::string& path, const CostConfig& cc, // fail the "CI regeneration == committed" reproducibility check (R6). std::fprintf(f, "Primary output is in **relative cost units (CU)**; **no point-value " - "dollar figure is claimed** (R6/D10) -- the CU->currency anchor is a " - "PLACEHOLDER cited range, filled in WP7. All cost/FoM parameters are " - "PLACEHOLDER (see `CostConfig`). Cost, cost/removal and FoM are " - "propagated **per run** from the WP5 campaign (schema 1.0) and reported " - "as p05/p50/p95. Regenerate with `adsc_cost`.\n\n"); + "dollar figure is claimed** (R6/D10) -- the CU->currency anchor is now " + "DERIVED AT RUNTIME from the WP14 cited itemized cost table (see the " + "`currency_anchor_derived` rows in the CSV), per cost_scenario " + "(low/mid/high); `CostConfig::cu_to_musd_low/high` are superseded " + "sentinels left at 0.0. All cost/FoM parameters are PLACEHOLDER (see " + "`CostConfig`). Cost, cost/removal and FoM are propagated **per run** " + "from the WP5 campaign (schema 1.0) and reported as p05/p50/p95. " + "Regenerate with `adsc_cost`.\n\n"); for (const CatalogCost& cq : report.catalogs) { std::fprintf(f, "## %s\n\n", cq.catalog.name); @@ -422,32 +777,51 @@ void write_cost_schema_md(const std::string& path) { std::FILE* f = std::fopen(path.c_str(), "w"); if (!f) return; std::fprintf(f, -"# WP6 cost/FoM CSV schema (version 1.0)\n" +"# WP6/WP14 cost/FoM CSV schema (version 1.1)\n" "\n" -"`generated/wp6_cost_summary.csv` is a long-format table consumed by WP7. It\n" -"consumes the WP5 campaign (schema 1.0): the amortization sweep re-runs the WP5\n" -"engine across the kit-inventory sweep at the fixed master seed, and the\n" +"`generated/wp6_cost_summary.csv` is a long-format table consumed downstream.\n" +"It consumes the WP5 campaign (schema 1.0): the amortization sweep re-runs the\n" +"WP5 engine across the kit-inventory sweep at the fixed master seed, and the\n" "baseline row (n_kits = CampaignConfig::kits_initial) reproduces\n" "`generated/wp5_campaign_runs.csv` exactly. Primary units are RELATIVE cost\n" -"units (CU); **no point-value currency is emitted** (R6/D10).\n" +"units (CU); **no point-value currency is emitted** (R6/D10) -- absolute costs\n" +"appear ONLY as cited low/mid/high ranges (WP14).\n" +"\n" +"Schema 1.1 (WP14) is an ADDITIVE bump over 1.0 (WP6): one new trailing\n" +"column (`cost_scenario`), four new `record_type` values (cost_component_musd\n" +"/ currency_anchor_derived / campaign_cost_musd / cost_per_removal_musd), and\n" +"two new `metric` values under the existing `fom` record_type\n" +"(cost_per_risk_equiv_mass / cost_per_risk_equiv_mass_musd). Every row that\n" +"existed under schema 1.0 is byte-identical in VALUE under 1.1; the only\n" +"change to those rows is the appended blank `cost_scenario` field.\n" "\n" "## Columns\n" "\n" "| column | meaning |\n" "|---|---|\n" -"| schema_version | WP6 schema id (`1.0`) |\n" +"| schema_version | schema id (`1.1`) |\n" "| catalog | class-level preset name (blank for global rows) |\n" -"| record_type | amortization / fom / tornado / cost_component / currency_anchor |\n" -"| n_kits | kits carried (amortization/cost_component rows); blank otherwise |\n" -"| param | cost parameter (tornado / cost_component rows); blank otherwise |\n" +"| record_type | amortization / fom / tornado / cost_component / " +"currency_anchor / cost_component_musd / currency_anchor_derived / " +"campaign_cost_musd / cost_per_removal_musd |\n" +"| n_kits | kits carried (amortization/cost_component rows) or the baseline N\n" +" the WP14 derived rows were computed at; blank otherwise |\n" +"| param | cost parameter (tornado / cost_component rows) or the WP14 item id\n" +" (cost_component_musd rows); blank otherwise |\n" "| weighting | spatial_density / criticality (fom rows); blank otherwise |\n" "| metric | the quantity named in the row |\n" "| estimate | point value (p50 for distributions) |\n" "| p05 / p50 / p95 | percentiles (distribution rows; 0 when not applicable) |\n" -"| units | CU_per_removal / CU / count / ratio / kg / kg_per_CU / normalized / musd_per_cu |\n" -"| notes | provenance / PLACEHOLDER caveats |\n" +"| units | CU_per_removal / CU / count / ratio / kg / kg_per_CU / CU_per_kg /\n" +" normalized / musd_per_cu / MUSD / MUSD_per_removal / MUSD_per_kg / MUSD_per_yr\n" +" / MUSD_per_unit / percent-valued units carried in the row's own `units`\n" +" string (e.g. \"pct of cost\") |\n" +"| notes | provenance / PLACEHOLDER caveats / WP14 arithmetic strings |\n" +"| cost_scenario | low / mid / high (WP14 rows only); blank for pre-WP14 rows\n" +" and for rows (e.g. fom / cost_per_risk_equiv_mass in CU) that are not\n" +" currency-scenario-dependent |\n" "\n" -"## record_type rows\n" +"## record_type rows (schema 1.0, unchanged)\n" "\n" "- **amortization** (per catalog, per swept N): `cost_per_removal` [CU/removal,\n" " p05/p50/p95], `cost_per_removal_ratio_to_n1` [ratio], `removals` [count p50],\n" @@ -458,21 +832,75 @@ void write_cost_schema_md(const std::string& path) { "- **fom** (per catalog): `removed_mass_per_mission` [kg p50], and per weighting\n" " `fom` [kg/CU, p05/p50/p95] and `band_weight` [normalized w(h)]. FoM =\n" " sum m_i*w(h_i)/C_campaign (spec §4). The two weightings are PLACEHOLDER and\n" -" disagree on band priority (T5).\n" +" disagree on band priority (T5). WP14 adds, per weighting: `cost_per_" +" risk_equiv_mass` [CU_per_kg, p05/p50/p95] = C_campaign/sum(m_i*w(h_i)),\n" +" the exact inverse orientation: each percentile is the reciprocal of the\n" +" FoM percentile BY CONSTRUCTION (NOT the percentile of per-run\n" +" reciprocals -- order statistics interpolate, so the two are not the same\n" +" computation), with p05/p95 swapped since 1/x is decreasing; and\n" +" `cost_per_risk_equiv_mass_musd` [MUSD_per_kg, cost_scenario=mid only, to\n" +" bound row count] = the same quantity converted via the mid-scenario anchor.\n" "- **cost_component** (per catalog, baseline N): C_dev / C_bus / C_kit_total /\n" " C_launch / C_ops [CU] at the median mission time.\n" "- **tornado** (representative catalog, baseline N): per cost parameter,\n" " `cost_per_removal_swing` [CU] and `fom_swing` [kg/CU] under a +/- one-at-a-\n" " time perturbation; rows are sorted by cost/removal swing.\n" -"- **currency_anchor** (global): `cu_to_musd_range` -- a PLACEHOLDER cited\n" -" range (estimate/p05 = low, p95 = high). WP6 never emits a point-value dollar\n" -" figure; a filled cited range is a WP7 deliverable.\n" +"- **currency_anchor** (global): `cu_to_musd_range` -- unfilled pre-WP14\n" +" sentinel (CostConfig::cu_to_musd_low/high, both 0.0; SUPERSEDED, see\n" +" currency_anchor_derived below). Retained byte-identical for schema\n" +" continuity; it never carries a point-value dollar figure.\n" +"\n" +"## record_type rows (schema 1.1, new -- WP14)\n" +"\n" +"- **cost_component_musd** (global, one row per WP14 itemized cost row x\n" +" cost_scenario): `metric=cost`, `estimate`/`p50` = the row's value at that\n" +" scenario, `units` = the item's own unit (MUSD / MUSD/yr / MUSD/unit / a\n" +" percentage unit), `param` = the item id, `notes` = the full cited source\n" +" note (D10: every row is sourced-or-PLACEHOLDER; a PLACEHOLDER row's note\n" +" ends `[PLACEHOLDER]` and its value is 0, never fabricated).\n" +"- **currency_anchor_derived** (global, one row per cost_scenario):\n" +" `metric=anchor_musd_per_cu` [musd_per_cu] = core_5_component / \n" +" C_campaign_CU_p50 at the baseline N (catalog A). The core is EXACTLY the\n" +" 5 CU-mapped components (Sec.10 Method): development +\n" +" manufacturing_bus_unit + kit_unit_scaled_9t x baseline-N-kits + C_launch\n" +" (low=rideshare card low, mid=rideshare upper, high=dedicated Electron --\n" +" width preserved, not narrowed) + operations_3yr. ground_segment_3yr,\n" +" licensing_fcc_application, licensing_fcc_annual, and the PLACEHOLDER\n" +" ssa_tracking row are ADDITIVE and excluded from this core (see\n" +" campaign_cost_musd below); so are the insurance/contingency percentage\n" +" rows. The `notes` field prints the full arithmetic string, numerator AND\n" +" denominator, so the figure is independently auditable from the row alone.\n" +"- **campaign_cost_musd** (global, per cost_scenario), four separately\n" +" visible rows so the anchor's basis is auditable: `metric=core_5_component`\n" +" = the identical numerator as currency_anchor_derived, in MUSD;\n" +" `metric=additives` = ground_segment_3yr + licensing_fcc_application +\n" +" licensing_fcc_annual x3 yr + ssa_tracking (PLACEHOLDER, 0), outside the CU\n" +" model and outside the anchor core; `metric=base_total` = core_5_component\n" +" + additives; `metric=with_reserves` = base_total x (1 +\n" +" contingency_reserve_pct/100) + insurance_line, where insurance_line =\n" +" (insurance_launch_plus_1yr_pct/100) x insured_value and insured_value =\n" +" manufacturing_bus_unit + kit_unit_scaled_9t x baseline-N-kits + C_launch\n" +" (asset+launch scale only -- insurance is an ADDITIVE line, not a\n" +" multiplier over the multi-year opex folded into base_total); `notes`\n" +" carries the formula for each row.\n" +"- **cost_per_removal_musd** (catalog A, baseline N, per cost_scenario):\n" +" `metric=cost_per_removal` [MUSD_per_removal, p05/p50/p95] = the existing\n" +" cost_per_removal CU percentiles at the baseline N times that scenario's\n" +" anchor_musd_per_cu.\n" +"\n" +"## Honesty framing (D10, carried into every WP14 currency row)\n" +"\n" +"External absolute-cost estimation by an unaffiliated author is inherently\n" +"low-standing; agencies re-run with their own parametric models; the relative\n" +"CU results remain primary. No point-value currency figure is emitted:\n" +"absolute costs appear ONLY as low/mid/high cited ranges.\n" "\n" -"## Future visualization / evidence (WP7) — not implemented here\n" +"## Future visualization / evidence -- not implemented here\n" "\n" "The schema supports, without change: amortization curves, FoM-under-weightings\n" -"bars with percentile whiskers, tornado bars, and the cost-component breakdown.\n" -"WP6 emits none of these charts and predicts no absolute cost.\n"); +"bars with percentile whiskers, tornado bars, the cost-component breakdown, and\n" +"the WP14 itemized/anchor/campaign-cost tables. This module emits none of\n" +"these charts.\n"); std::fclose(f); } diff --git a/src/main_kit_trade.cpp b/src/main_kit_trade.cpp index 4f929be..e826df4 100644 --- a/src/main_kit_trade.cpp +++ b/src/main_kit_trade.cpp @@ -424,8 +424,8 @@ void write_classc_md(const std::string& out_dir, const DebrisCatalog& zenit, std::fprintf(f, "## Cost\n\n"); std::fprintf(f, "| candidate | cost | source |\n"); std::fprintf(f, "|---|---|---|\n"); - std::fprintf(f, "| Zenit-2 / SL-16 (catalog_A) | PLACEHOLDER until WP14 | WP14 cost-range itemization |\n"); - std::fprintf(f, "| Envisat-class (catalog_C) | PLACEHOLDER until WP14 | WP14 cost-range itemization |\n\n"); + std::fprintf(f, "| Zenit-2 / SL-16 (catalog_A) | itemized low/mid/high ranges | wp6_cost_summary.csv cost_component_musd + campaign_cost_musd rows (WP14) |\n"); + std::fprintf(f, "| Envisat-class (catalog_C) | not separately itemized | WP14 ranges are servicer-campaign-scale; a controlled-reentry mission needs its own estimate (open) |\n\n"); std::fprintf(f, "## `wp13_classC.csv` columns\n\n"); std::fprintf(f, "| column | meaning |\n"); diff --git a/tests/test_cost.cpp b/tests/test_cost.cpp index e5e6229..a439fd9 100644 --- a/tests/test_cost.cpp +++ b/tests/test_cost.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,78 @@ static const AmortPoint* at_n(const CatalogCost& cq, int n) { return nullptr; } +// Manual QUOTE-AWARE comma split (NOT std::getline-on-stringstream: that +// silently drops a trailing empty field, e.g. the blank cost_scenario column +// on every pre-WP14 row, misaligning every column-index lookup below). +// Quote-awareness is REQUIRED: several WP14 quoted notes fields contain +// literal commas (e.g. the currency_anchor_derived arithmetic note), so a +// plain comma split would shift the cost_scenario column out of position. +// RFC-4180-lite: '"' toggles the quoted state; a doubled '""' inside quotes +// is an escaped quote; commas inside quotes are field content. Quotes are +// stripped from the returned fields. +static std::vector split_csv_line(const std::string& line) { + std::vector f; + std::string cur; + bool quoted = false; + for (std::size_t i = 0; i < line.size(); ++i) { + const char c = line[i]; + if (quoted) { + if (c == '"') { + if (i + 1 < line.size() && line[i + 1] == '"') { cur += '"'; ++i; } + else quoted = false; + } else { + cur += c; + } + } else if (c == '"') { + quoted = true; + } else if (c == ',') { + f.push_back(cur); + cur.clear(); + } else { + cur += c; + } + } + f.push_back(cur); + return f; +} + +// schema 1.1 column order: 0 schema_version 1 catalog 2 record_type 3 n_kits +// 4 param 5 weighting 6 metric 7 estimate 8 p05 9 p50 10 p95 11 units 12 notes +// 13 cost_scenario. +static std::vector> parse_csv_rows(const std::string& csv_text) { + std::vector> rows; + std::stringstream ss(csv_text); + std::string line; + std::getline(ss, line); // header + while (std::getline(ss, line)) { + if (!line.empty()) rows.push_back(split_csv_line(line)); + } + return rows; +} + +// First row matching every non-null filter; returns the value at `col` +// (std::atof) or a sentinel if no row matches. Pass nullptr to skip a filter, +// or "" to require the column be literally blank. +static double find_csv_val(const std::vector>& rows, + const char* record_type, const char* metric, + const char* weighting, const char* cost_scenario, + int col) { + for (const std::vector& f : rows) { + if (f.size() <= static_cast(col)) continue; + if (record_type && f[2] != record_type) continue; + if (metric && f[6] != metric) continue; + if (weighting && f[5] != weighting) continue; + if (cost_scenario && f[13] != cost_scenario) continue; + return std::atof(f[col].c_str()); + } + return -999999.0; // sentinel: no matching row found +} + +static bool ends_with(const std::string& s, const std::string& suffix) { + return s.size() >= suffix.size() && + s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; +} + int main() { const Config base; const CostConfig cc; @@ -179,14 +252,27 @@ int main() { const std::string header = txt.substr(0, txt.find('\n')); const char* cols[] = {"schema_version", "catalog", "record_type", "n_kits", "param", "weighting", "metric", "estimate", "p05", - "p50", "p95", "units", "notes"}; + "p50", "p95", "units", "notes", "cost_scenario"}; for (const char* c : cols) CHECK(header.find(c) != std::string::npos); const char* rts[] = {"amortization", "fom", "tornado", "cost_component", - "currency_anchor"}; + "currency_anchor", "cost_component_musd", + "currency_anchor_derived", "campaign_cost_musd", + "cost_per_removal_musd", "cost_per_risk_equiv_mass"}; for (const char* rt : rts) CHECK(txt.find(rt) != std::string::npos); - // No point-value currency: '$' must never appear in any WP6 artifact, - // and the CU->currency anchor is left unfilled (low = high = 0). + // No point-value currency: '$' must never appear in any WP6/WP14 + // artifact (this scan runs over `txt`/md/sch AFTER the WP14 rows are + // written, so it covers the new cost_component_musd / + // currency_anchor_derived / campaign_cost_musd / cost_per_removal_musd + // / cost_per_risk_equiv_mass(_musd) rows too -- no separate scan + // needed). `CostConfig::cu_to_musd_low/high` are UNUSED SENTINELS, + // intentionally left at 0.0 post-WP14: the real WP14 currency anchor + // is derived at runtime from the cited itemized table (see + // `currency_anchor_derived` checks below) and written straight into + // the CSV -- it never round-trips through these two config fields, so + // their ==0.0 assert below is still exactly what should hold, just + // for a different reason than pre-WP14 ("not yet filled" -> + // "deliberately bypassed"). CHECK(txt.find('$') == std::string::npos); CHECK(slurp(md).find('$') == std::string::npos); CHECK(slurp(sch).find('$') == std::string::npos); @@ -197,6 +283,79 @@ int main() { CHECK(txt.find(wd) == std::string::npos); CHECK(slurp(md).find(wd) == std::string::npos); } + + // 7b. WP14: itemized USD table + derived anchor/campaign-cost/ + // cost-per-risk rows written into the same CSV. + { + // Table shape + D10 (sourced-or-PLACEHOLDER, never fabricated): + // every row's note is non-empty and ends with one of the four + // verified-status flags. + int n_items = 0; + const CostItemUsd* items = wp14_cost_items(&n_items); + CHECK(n_items >= 11); + for (int i = 0; i < n_items; ++i) { + CHECK(items[i].item != nullptr && items[i].item[0] != '\0'); + CHECK(items[i].unit != nullptr && items[i].unit[0] != '\0'); + const std::string note = items[i].note ? items[i].note : ""; + CHECK(!note.empty()); + CHECK(ends_with(note, "[web-verified 2026-07-11]") || + ends_with(note, "[web-verified (analog) 2026-07-11]") || + ends_with(note, "[training-data extrapolation]") || + ends_with(note, "[PLACEHOLDER]")); + } + + const std::vector> rows = parse_csv_rows(txt); + + // currency_anchor_derived: a real, cited low 0.0); + CHECK(anchor_high > anchor_low); + + // campaign_cost_musd: with_reserves (contingency applied over + // base_total, plus the additive insurance_line) strictly exceeds + // the pre-reserve base_total at every scenario that has a + // nonzero contingency pct; check the high scenario (contingency + // 30%, insurance 12% -- both nonzero, so the contingency + // multiplier is > 1 and insurance_line is a positive add-on, so + // the inequality is strict either way). + const double campaign_base_total_high = + find_csv_val(rows, "campaign_cost_musd", "base_total", nullptr, + "high", 7); + const double campaign_with_reserves_high = + find_csv_val(rows, "campaign_cost_musd", "with_reserves", nullptr, + "high", 7); + CHECK(campaign_base_total_high > 0.0); + CHECK(campaign_with_reserves_high > campaign_base_total_high); + + // Inverse-FoM identity: cost_per_risk_equiv_mass (CU/kg) is + // DEFINED as the reciprocal of fom (kg/CU) at each percentile, + // with p05/p95 swapped (1/x is monotone-decreasing for x>0). + // Inverse-FoM identity BY CONSTRUCTION: each + // cost_per_risk_equiv_mass percentile is the reciprocal of the + // already-computed FoM percentile (NOT the percentile of a + // per-run-reciprocal distribution -- order statistics interpolate, + // so those would differ). The CSV stores the value through the + // fixed "%.6f" format, so the honest exact comparison is against + // the reciprocal ROUND-TRIPPED through the same format: identical + // strings parse to identical doubles. + const double csv_risk_p50_criticality = find_csv_val( + rows, "fom", "cost_per_risk_equiv_mass", "criticality", "", 9); + const double fom_p50_criticality = + report.catalogs.front().fom[1].fom_p50; // index 1 == Criticality + CHECK(report.catalogs.front().fom[1].weighting == Weighting::Criticality); + CHECK(fom_p50_criticality > 0.0); + CHECK(csv_risk_p50_criticality > 0.0); + char expect_buf[64]; + std::snprintf(expect_buf, sizeof expect_buf, "%.6f", + 1.0 / fom_p50_criticality); + CHECK(std::abs(csv_risk_p50_criticality - std::atof(expect_buf)) < 1e-15); + } } // 8. WP5-summary consumer (schema 1.0): parse a synthetic summary row. diff --git a/tools/evidence/make_evidence.py b/tools/evidence/make_evidence.py index 51159a1..402039f 100644 --- a/tools/evidence/make_evidence.py +++ b/tools/evidence/make_evidence.py @@ -18,6 +18,7 @@ import csv import json import os +import re import sys OUT_REL = os.path.join("evidence", "adsc_evidence_pack.md") @@ -56,6 +57,8 @@ def __init__(self, root): self.wp12 = load_csv(os.path.join(g, "wp12_ladder.csv")) self.wp13 = load_csv(os.path.join(g, "wp13_kit_trade.csv")) self.wp13c = load_csv(os.path.join(g, "wp13_classC.csv")) + with open(os.path.join(g, "wp6_cost_schema.md"), encoding="utf-8") as f: + self.wp6_schema_md = f.read() with open(os.path.join(g, "t6_flux_sweep.md"), encoding="utf-8") as f: self.t6 = {} for line in f: @@ -92,14 +95,50 @@ def wp12v(self, section, level, catalog, metric, item=""): return float(r["value"]) raise KeyError((section, level, catalog, metric, item)) - def wp6q(self, catalog, record_type, metric, n_kits=None, weighting=None): + def wp6q(self, catalog, record_type, metric, n_kits=None, weighting=None, + cost_scenario=None): for r in self.wp6: 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)): + and (weighting is None or r["weighting"] == weighting) + and (cost_scenario is None + or r["cost_scenario"] == cost_scenario)): return r - raise KeyError((catalog, record_type, metric, n_kits, weighting)) + raise KeyError((catalog, record_type, metric, n_kits, weighting, + cost_scenario)) + + def wp6_item(self, param, cost_scenario): + """One WP14 itemized cost_component_musd row (global; catalog blank), + keyed by its item id (`param` column) and cost_scenario.""" + for r in self.wp6: + if (r["record_type"] == "cost_component_musd" + and r["param"] == param + and r["cost_scenario"] == cost_scenario): + return r + raise KeyError(("wp6_item", param, cost_scenario)) + + def md_section(self, md_text, heading): + """The body text of one '## heading' section of a committed markdown + doc, as a single collapsed paragraph (blank lines dropped, internal + newlines joined with spaces) -- used to quote a doc section verbatim + without hand-retyping it, so the generator and the source cannot + drift apart.""" + lines = md_text.splitlines() + start = None + for i, ln in enumerate(lines): + if ln.strip() == heading: + start = i + 1 + break + if start is None: + raise KeyError(("md_section: heading not found", heading)) + body = [] + for ln in lines[start:]: + if ln.startswith("## "): + break + if ln.strip(): + body.append(ln.strip()) + return " ".join(body) def wp3_area25(self, catalog): for r in self.wp3: @@ -137,6 +176,29 @@ def f4(v): return "%.4f" % float(v) +def f6(v): + return "%.6f" % float(v) + + +def note_lead(notes): + """The first sentence of a CSV `notes` field (up to the first '. '), + extracted mechanically rather than hand-retyping the whole (sometimes + long, third-party-citation-bearing) note.""" + return notes.split(". ", 1)[0].rstrip(".") + "." + + +def note_tag(notes): + """The trailing bracketed provenance tag of a CSV `notes` field (e.g. + '[PLACEHOLDER]' / '[training-data extrapolation]'), extracted + mechanically via the same bracket convention D10 already requires every + itemized cost row's note to end with.""" + m = re.search(r"\[[^\[\]]*\]\s*$", notes.strip()) + if not m: + raise ValueError("note has no trailing bracketed provenance tag: %r" + % notes) + return m.group(0) + + def ko_claim(row, n_runs, ds_id): """R14-tagged keep-out claim from a WP5 keep_out_violation_rate row: '**k of n** [L0: linear CW, dispersion set ds-v1, Wilson 95% upper bound @@ -197,6 +259,66 @@ def collect_placeholders(root): return hits +# ---- PLACEHOLDER importance classification (spec:260-264) ------------------ +# A small, explicit path+keyword rule table, evaluated top-to-bottom (first +# match wins). Intent, not line-level precision (a handful of borderline +# comment lines land one bucket over from a stricter reading -- acceptable +# for a "reviewer triage" classification, not a formal audit): +# decision-critical: the mark sits on, or documents, a value that DIRECTLY +# re-bases a headline CU/MUSD/FoM number or a campaign safety/outcome +# rate if filled -- cost.hpp's CU coefficients and FoM weight tables +# (the entire file: every mark there is part of that one system), the +# cu-anchor itemized-cost-table rows in cost.cpp, decay.hpp's EdtConfig +# physical parameters, campaign.hpp's Delta-v/time dispersions and the +# capture-contact closing-speed threshold, mission.hpp's catalog-like +# target physical parameters, and decay.cpp's catalog mass/altitude/ +# inclination constructors. +# cosmetic: the mark appears only inside a print/log statement or a +# doc-generator string being written into a schema/report markdown file +# -- never on the value itself (detected by shape: a printf/fprintf call, +# a raw string-literal continuation line, or a literal PLACEHOLDER string +# used only as a test-assertion target). +# moderate: everything else (structural comments, lower-priority / +# secondary-fidelity-ladder parameters not named above) -- the default. +IMPORTANCE_WHOLE_FILE_CRITICAL = ( + "include/adsc/cost.hpp", # CU coefficients + FoM weight tables + cu-anchor doc + "src/decay.cpp", # catalog mass/altitude/inclination constructors +) +IMPORTANCE_KEYWORD_CRITICAL = { + "include/adsc/decay.hpp": ("kit_mass_kg", "avg_current_a", "eta_libration", + "deploy_failure_prob"), + "include/adsc/campaign.hpp": ("dv_", "t_attach_s", "t_depart_s", + "t_phasing_s", "disp_", "nominal_closing_m_s", + "nominal_solar_factor"), + "include/adsc/mission.hpp": ("target_altitude_km", "target_inertia_diag", + "sync_target_rate_deg_s", + "servicer_drag_area_m2", + "solar_min_density_factor", + "solar_max_density_factor"), +} +# cost.cpp's itemized-cost-table rows are C++ aggregate-initializer entries +# ending "...]"...}," (e.g. the ssa_tracking row) -- distinct from that same +# file's surrounding prose/print-string lines. +COST_CPP_ITEM_ROW_RE = re.compile(r'\]"\s*\}') +# A print/log statement or a raw string-literal continuation line: printf( +# call sites, lines that are themselves just a quoted string fragment, or +# fragments ending in an escaped newline-then-quote (a printed line of text). +PRINT_STRING_RE = re.compile(r'printf\(|^\s*"|\\n"\s*[\),;]*\s*$') + + +def classify_importance(rel, txt): + if rel in IMPORTANCE_WHOLE_FILE_CRITICAL: + return "decision-critical" + if rel == "src/cost.cpp" and COST_CPP_ITEM_ROW_RE.search(txt): + return "decision-critical" + kws = IMPORTANCE_KEYWORD_CRITICAL.get(rel) + if kws and any(k in txt for k in kws): + return "decision-critical" + if PRINT_STRING_RE.search(txt) or 'check("PLACEHOLDER"' in txt: + return "cosmetic" + return "moderate" + + def build(d): A, B = d.catalogs[0], d.catalogs[1] Ashort, Bshort = A.split("/")[0].strip(), B.split("/")[0].strip() @@ -678,9 +800,10 @@ def build(d): w("The amortization curve (section 1) bottoms at N=%d because the **Delta-v" % nmin) w("budget, not the kit count, caps removals** - the honest capacity story a") - w("mission designer needs. Cost is RELATIVE (CU) throughout: **no absolute") - w("cost is predicted anywhere in this package**; the CU-to-currency anchor is") - w("a deliberately unfilled cited-range PLACEHOLDER. Parameter sensitivity is") + w("mission designer needs. Cost is RELATIVE (CU) throughout and stays the") + w("PRIMARY metric; WP14 additionally derives a cited absolute-cost RANGE") + w("(never a point value) from an itemized cost table, detailed in the") + w("'Absolute cost ranges (WP14)' subsection below. Parameter sensitivity is") w("ranked by a one-at-a-time tornado (development cost dominates):") w("") w("![WP6 tornado](../generated/viz/wp6_tornado.svg)") @@ -695,6 +818,84 @@ def build(d): w("") w("![WP6 FoM weightings](../generated/viz/wp6_fom_weightings.svg)") w("") + w("### Absolute cost ranges (WP14)") + w("") + w("Spec D10 rule, quoted verbatim: \"Every row carries a source or stays") + w("PLACEHOLDER - this is D10 applied, not relaxed. Relative CU results") + w("remain primary.\" (adsc-specification-v5.md section 6, WP14). Every") + w("number in this subsection is read from the new WP14 rows of the") + w("committed `generated/wp6_cost_summary.csv` (schema %s) - nothing" + % d.wp6[0]["schema_version"]) + w("hand-written.") + w("") + anchor = {s: d.wp6q("", "currency_anchor_derived", "anchor_musd_per_cu", + cost_scenario=s) for s in ("low", "mid", "high")} + base_total = {s: d.wp6q("", "campaign_cost_musd", "base_total", + cost_scenario=s) for s in ("low", "mid", "high")} + with_res = {s: d.wp6q("", "campaign_cost_musd", "with_reserves", + cost_scenario=s) for s in ("low", "mid", "high")} + cpr_musd = {s: d.wp6q(A, "cost_per_removal_musd", "cost_per_removal", + cost_scenario=s) for s in ("low", "mid", "high")} + w("**CU->currency anchor**, derived at runtime from a 5-component core of") + w("the itemized cost table (development + manufacturing_bus_unit +") + w("kit_unit_scaled_9t x baseline-N-kits + launch + operations_3yr): low") + w("**%s** MUSD/CU .. high **%s** MUSD/CU (mid %s MUSD/CU); low+high anchor" + % (f6(anchor["low"]["estimate"]), f6(anchor["high"]["estimate"]), + f6(anchor["mid"]["estimate"]))) + w("values themselves come only from cited itemized rows (D10) - never a") + w("point-value dollar figure.") + w("") + w("| cost_scenario | anchor [MUSD/CU] | campaign base_total [MUSD] |" + " campaign with_reserves [MUSD] |") + w("|---|---:|---:|---:|") + for s in ("low", "mid", "high"): + w("| %s | %s | %s | %s |" % (s, f6(anchor[s]["estimate"]), + f2(base_total[s]["estimate"]), + f2(with_res[s]["estimate"]))) + w("") + w("`base_total` is the 5-component anchor core plus the additive") + w("ground-segment/licensing/SSA-tracking rows (all outside the CU model);") + w("`with_reserves` additionally applies the contingency-reserve percentage") + w("and an asset+launch-scaled insurance line (arithmetic provenance for") + w("both is printed in full in each row's own `notes` field, see the CSV).") + w("") + w("**Cost per removal (%s class, baseline N, MUSD, p50 per" + % Ashort) + w("cost_scenario):** low **%s** / mid **%s** / high **%s** MUSD/removal." + % (f3(cpr_musd["low"]["p50"]), f3(cpr_musd["mid"]["p50"]), + f3(cpr_musd["high"]["p50"]))) + cost_per_risk_cu = d.wp6q(A, "fom", "cost_per_risk_equiv_mass", + weighting="criticality") + cost_per_risk_musd = d.wp6q(A, "fom", "cost_per_risk_equiv_mass_musd", + weighting="criticality", cost_scenario="mid") + w("**Cost per risk-equivalent mass (criticality weighting), p50:** %s" + % f6(cost_per_risk_cu["p50"])) + w("CU/kg (%s MUSD/kg, mid cost_scenario) - the exact inverse orientation" + % f6(cost_per_risk_musd["p50"])) + w("of the FoM above (1/FoM by construction, not an independent") + w("computation - see the CSV row's own notes for the reciprocal-order-") + w("statistics caveat).") + w("") + ssa_row = d.wp6_item("ssa_tracking", "mid") + kit_row = d.wp6_item("kit_unit_scaled_9t", "mid") + w("**PLACEHOLDER/extrapolation flags, extracted (never hand-retyped)") + w("from the committed CSV's own notes -- the leading clause plus the") + w("trailing bracketed provenance tag, D10:**") + w("") + w("- `ssa_tracking`: \"%s\" ... %s" + % (note_lead(ssa_row["notes"]), note_tag(ssa_row["notes"]))) + w("- `kit_unit_scaled_9t`: \"%s\" ... %s" + % (note_lead(kit_row["notes"]), note_tag(kit_row["notes"]))) + w("") + honesty = d.md_section( + d.wp6_schema_md, + "## Honesty framing (D10, carried into every WP14 currency row)") + w("Honesty framing, quoted verbatim from `generated/wp6_cost_schema.md`") + w("(never hand-retyped, so the generator and the schema doc cannot") + w("drift apart):") + w("") + w("> %s" % honesty) + w("") w("## 6. Limitations - stated plainly, none hidden") w("") w("- **Linear CW scope (F2):** every passive-safety statement is exact only in") @@ -806,9 +1007,10 @@ def build(d): w("```") w("") w("`tools/regenerate_all.sh` is the single source of truth for regeneration") - w("order (campaign -> cost -> decay -> flux -> reference metrics -> figures ->") - w("compliance -> this document); CI runs exactly this script and enforces the") - w("byte-identity gate on every push. Measured wall time is printed to the CI") + w("order (campaign -> cost -> decay -> kit trade -> prioritization -> flux ->") + w("reference metrics -> figures -> compliance -> this document); CI runs") + w("exactly this script and enforces the byte-identity gate on every push.") + w("Measured wall time is printed to the CI") w("log (see the latest `build-and-test` run); it is intentionally never") w("embedded in artifacts, and it is well under the one-hour reproduction") w("budget (D10). Requirements: C++17 compiler, Eigen 3.3+, CMake, Python 3") @@ -821,15 +1023,33 @@ def build(d): w("(R10), collected automatically by the generator of this document") w("(`tools/evidence/` itself is excluded from the scan - it names the marker") w("only in order to collect and audit it). If a value is listed here, treat") - w("it as unvalidated until a cited source replaces it.") + w("it as unvalidated until a cited source replaces it. Each mark also") + w("carries a machine-assigned **importance** (spec:260-264): whether filling") + w("it in with a citation would directly re-base a headline CU/MUSD/FoM") + w("number or campaign safety/outcome rate (`decision-critical`), whether it") + w("is a lower-priority or secondary-fidelity parameter (`moderate`), or") + w("whether the mark appears only in a print/log statement or doc-generator") + w("string, never on the value itself (`cosmetic`). Classification rule") + w("table: `tools/evidence/make_evidence.py::classify_importance` (a small") + w("explicit path+keyword table, not a formal per-line audit -- a handful of") + w("comment lines may land one bucket over from a stricter reading).") w("") hits = collect_placeholders(d.root) - w("Total marks: **%d**" % len(hits)) - w("") - w("| location | line |") - w("|---|---|") + counts = {"decision-critical": 0, "moderate": 0, "cosmetic": 0} + classified = [] for rel, i, txt in hits: - w("| `%s:%d` | %s |" % (rel, i, txt)) + imp = classify_importance(rel, txt) + counts[imp] += 1 + classified.append((rel, i, txt, imp)) + w("Total marks: **%d** (decision-critical: **%d**, moderate: **%d**, " + "cosmetic: **%d**)" + % (len(hits), counts["decision-critical"], counts["moderate"], + counts["cosmetic"])) + w("") + w("| location | importance | line |") + w("|---|---|---|") + for rel, i, txt, imp in classified: + w("| `%s:%d` | %s | %s |" % (rel, i, imp, txt)) w("") w("## 11. Changelog - R15 pin supersessions") w("") diff --git a/tools/evidence/test_evidence.py b/tools/evidence/test_evidence.py index 3aa3be2..97c98bc 100644 --- a/tools/evidence/test_evidence.py +++ b/tools/evidence/test_evidence.py @@ -203,14 +203,36 @@ def must_contain(s, why): must_contain("PASS=%d, INFO=%d, WARN=%d, BLOCK=%d" % (cs["PASS"], cs["INFO"], cs["WARN"], cs["BLOCK"]), "compliance summary counts") - # PLACEHOLDER inventory is non-empty and counted - m = re.search(r"Total marks: \*\*(\d+)\*\*", pack) - check(m is not None, "PLACEHOLDER inventory lacks a total count") + # PLACEHOLDER inventory is non-empty, counted, and importance-classified + # (spec:260-264): "Total marks: **N** (decision-critical: **X**, + # moderate: **Y**, cosmetic: **Z**)". + m = re.search( + r"Total marks: \*\*(\d+)\*\* \(decision-critical: \*\*(\d+)\*\*, " + r"moderate: \*\*(\d+)\*\*, cosmetic: \*\*(\d+)\*\*\)", pack) + check(m is not None, + "PLACEHOLDER inventory lacks the importance-classified total-count line") if m: + n_total, n_crit, n_mod, n_cosmetic = (int(g) for g in m.groups()) n_rows = len(re.findall(r"\n\| `[^`]+:\d+` \|", pack)) - check(int(m.group(1)) == n_rows, - "inventory count %s != table rows %d" % (m.group(1), n_rows)) + check(n_total == n_rows, + "inventory count %d != table rows %d" % (n_total, n_rows)) check(n_rows > 20, "inventory suspiciously small (%d)" % n_rows) + check(n_crit + n_mod + n_cosmetic == n_total, + "decision-critical(%d) + moderate(%d) + cosmetic(%d) != total(%d)" + % (n_crit, n_mod, n_cosmetic, n_total)) + # Sanity floor, not a pin: as of this writing the actual count is 56; + # 20 is chosen comfortably below that so incidental PLACEHOLDER-line + # rewording elsewhere doesn't flip this test, while still catching a + # real regression (e.g. the classifier silently stops matching). + check(n_crit >= 20, + "decision-critical count (%d) is below the 20-item sanity floor" + % n_crit) + # The importance column itself is present and every value recognized. + check("| location | importance | line |" in pack, + "inventory table lacks the importance column header") + for label in ("decision-critical", "moderate", "cosmetic"): + check(("| %s |" % label) in pack, + "inventory table has no %s-classified row" % label) # 4. Determinism + committed == regenerated. The committed pack content was # captured into `pack` above; the generator then intentionally rewrites the diff --git a/tools/prioritization/make_prioritization.py b/tools/prioritization/make_prioritization.py new file mode 100644 index 0000000..ede4327 --- /dev/null +++ b/tools/prioritization/make_prioritization.py @@ -0,0 +1,643 @@ +#!/usr/bin/env python3 +"""WP14 Prioritization table -> generated/wp14_prioritization.{csv,md} + +A pure derived/joined view over three already-committed, already-regenerated +CSVs -- NO new simulation, NO new statistics. Every number below is either +read directly from a committed row, or is a single elementwise arithmetic +combination of two already-committed numbers (always noted where it occurs, +e.g. cost_per_removal_musd for a catalog that has no dedicated WP6 anchor row +is computed as CU_percentile x anchor_musd_per_cu -- exactly the same +multiplication WP6/WP14 already performs for the anchor catalog itself). +Zero hand-written numbers. Python 3 standard library only (spec v5 R9). + +Inputs (schema versions read from the files themselves, never hardcoded): + generated/wp6_cost_summary.csv (schema 1.1: cost/FoM/currency-anchor rows) + generated/wp13_kit_trade.csv (schema 1.0: per-class kit/EDT/sail trade) + generated/wp5_campaign_summary.csv (schema 1.1: campaign Monte Carlo rates) + +Discipline (spec v5 R6/D10/R11): + * No wall-clock timestamp or run time is embedded in any output. + * All numeric fields use fixed-decimal formatting so the byte output is + stable across platforms and runs (the CI "regeneration == committed" + gate). + * No point-value currency figure is ever printed -- absolute-cost numbers + appear ONLY as low/mid/high cited ranges (the same D10 discipline as + wp6_cost_summary.csv). + * D12: legal accessibility enters as a gate + metadata flags via the WP8 + compliance engine, never as a multiplier in the FoM or in any rank score + computed here. + * ASCII only, LF-only line endings. + +Usage: make_prioritization.py [out_dir] + out_dir defaults to /generated +""" +import csv +import os +import sys + +CAT_A = "SL-16 / Zenit-2 second stage" +CAT_B = "SL-8 / Kosmos-3M second stage" +CAT_C = "Envisat-class massive SSO payload" + +WEIGHTINGS = ("spatial_density", "criticality") +SCENARIOS = ("low", "mid", "high") + +CSV_HEADER = [ + "schema_version", "catalog", "record_type", "metric", "weighting", + "cost_scenario", "kit_option", "estimate", "p05", "p50", "p95", + "value_lo", "value_hi", "units", "notes", +] +SCHEMA_VERSION = "1.0" + + +# ---- fixed formatting (platform-stable) ------------------------------------ +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 band_str(lo, hi, fmt): + """'lo..hi' honoring the wp13_kit_trade.csv convention that a non-numeric + band field (e.g. the exact-polar 'n/a (...)' text) is the honest physical + null result, not missing data -- printed verbatim rather than reformatted.""" + try: + return "%s..%s" % (fmt(float(lo)), fmt(float(hi))) + except ValueError: + return "%s .. %s" % (lo, hi) + + +def load_rows(path): + with open(path, newline="", encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +def write_text(path, s): + # newline="\n" forces LF on every platform so committed bytes are stable. + with open(path, "w", encoding="utf-8", newline="\n") as f: + f.write(s) + + +# ---- literature citations (D2/D12; hand-written prose, not data -- the same +# ---- convention tools/evidence/make_evidence.py already uses for its own +# ---- Kessler/Liou/McKnight citations) -------------------------------------- +MCKNIGHT_CITATION = ( + "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 " + "(wp13-literature.md Topic 6): 11 expert teams' consensus top-50 most-" + "concerning LEO objects is dominated by rocket bodies, and 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)." +) +SL8_POPULATION_CITATION = ( + "wp13-literature.md Topic 5b: SL-8 (Kosmos-3M) second-stage clusters " + "span three altitude boxes -- Box 3 ~700-800 km (50 R/Bs), Box 2 " + "~900-1000 km (143 R/Bs), Box 1 ~1500-1600 km (44 R/Bs), inclinations " + "~74/83 deg -- a combined population an order of magnitude larger than " + "the SL-16 top-20 concern list; the class appears prominently in SL-8-" + "specific ADR mission studies (e.g. ADReS-A, Springer " + "10.1007/978-3-319-15982-9_3)." +) +ENVISAT_CITATION = ( + "wp13-literature.md Topic 6 item 1: Envisat (ESA), 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 deg, defunct since April 2012; the archetypal " + "'too massive / high-casualty-risk-on-uncontrolled-reentry' object " + "cited across the debris-mitigation literature (eoPortal; ESA Earth " + "Online)." +) +D12_SENTENCE = ( + "Legal accessibility enters as a gate and metadata flags, never a " + "multiplier (D12, adsc-specification-v5.md section 3): the WP8 " + "compliance engine (`tools/compliance/check_compliance.py`, policy " + "`ADSC-POL-01`) BLOCKs unconsented active debris removal, and this " + "prioritization table consumes its PASS/BLOCK output and metadata " + "flags rather than folding a subjective legal weight into the FoM or " + "into any rank below. No catalog in this document is ranked, up or " + "down, on legal/consent grounds -- consent is a gate, not a scoring " + "input." +) +HONESTY_NOTE = ( + "The criticality weighting's citation basis is McKnight-anchored at " + "the ~840-850 km peak (McKnight et al. 2021: the top ~20 most-" + "concerning LEO objects are all SL-16/Zenit-2 second stages, ~9 t " + "class, ~850 km -- exactly catalog A's band), which is a real, cited " + "anchor point for catalog A's criticality weight w(h=840)=1.00. But " + "the REST of the criticality weighting table " + "(`CostConfig::weight_table`, the non-peak altitude/weight pairs away " + "from 840-850 km) remains PLACEHOLDER: the McKnight ranking gives a " + "strong, cited reason to weight the ~840-850 km peak high, but it " + "does not by itself supply a citable functional form for how the " + "weight falls off away from that peak, so the shape between anchor " + "points is still an uncited interpolation (T5 open). The dressed-up-" + "mass-sort critique from the WP14 review is acknowledged here, not " + "dismissed: because both weighting tables today have limited dynamic " + "range and removed mass dominates the FoM ratio, a skeptical reader " + "is right to ask whether the 'criticality' weighting is doing real " + "discriminating work beyond re-deriving a mass ranking; strengthening " + "the non-peak citation basis (or accepting the narrower dynamic range " + "as the honest state of the evidence) is the open item, not resolved " + "by this document." +) + + +class Data: + """Reads the three committed CSVs; exposes only values that appear in + them (no hand-written numbers).""" + + def __init__(self, gen): + self.wp6 = load_rows(os.path.join(gen, "wp6_cost_summary.csv")) + self.wp13 = load_rows(os.path.join(gen, "wp13_kit_trade.csv")) + self.wp5 = load_rows(os.path.join(gen, "wp5_campaign_summary.csv")) + self.wp6_schema = self.wp6[0]["schema_version"] + self.wp13_schema = self.wp13[0]["schema_version"] + self.wp5_schema = self.wp5[0]["schema_version"] + self.catalogs13 = [] + for r in self.wp13: + if r["catalog"] not in self.catalogs13: + self.catalogs13.append(r["catalog"]) + self.campaign_catalogs = [] + for r in self.wp5: + if r["catalog"] not in self.campaign_catalogs: + self.campaign_catalogs.append(r["catalog"]) + + def wp6q(self, catalog, record_type, metric, n_kits=None, weighting=None, + cost_scenario=None): + out = [] + for r in self.wp6: + 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) + and (cost_scenario is None + or r["cost_scenario"] == cost_scenario)): + out.append(r) + if len(out) != 1: + raise KeyError((catalog, record_type, metric, n_kits, weighting, + cost_scenario, "rows_found=%d" % len(out))) + return out[0] + + def wp6_anchor(self, cost_scenario): + """currency_anchor_derived row (global; catalog is blank in the CSV).""" + return self.wp6q("", "currency_anchor_derived", "anchor_musd_per_cu", + cost_scenario=cost_scenario) + + def wp6_baseline_n(self, catalog): + """Baseline kit count N, read from the cost_component rows' own + n_kits column (never hardcoded).""" + ns = {r["n_kits"] for r in self.wp6 + if r["catalog"] == catalog and r["record_type"] == "cost_component"} + if len(ns) != 1: + raise KeyError(("wp6_baseline_n", catalog, ns)) + return int(next(iter(ns))) + + def wp13v(self, catalog, record_type, kit_option=None): + out = [] + for r in self.wp13: + if (r["catalog"] == catalog and r["record_type"] == record_type + and (kit_option is None or r["kit_option"] == kit_option)): + out.append(r) + if len(out) != 1: + raise KeyError((catalog, record_type, kit_option, + "rows_found=%d" % len(out))) + return out[0] + + +def short(catalog): + return catalog.split("/")[0].strip() + + +# ---- row builder ------------------------------------------------------------ +def row(catalog="", record_type="", metric="", weighting="", cost_scenario="", + kit_option="", estimate="", p05="", p50="", p95="", value_lo="", + value_hi="", units="", notes=""): + return { + "schema_version": SCHEMA_VERSION, "catalog": catalog, + "record_type": record_type, "metric": metric, "weighting": weighting, + "cost_scenario": cost_scenario, "kit_option": kit_option, + "estimate": estimate, "p05": p05, "p50": p50, "p95": p95, + "value_lo": value_lo, "value_hi": value_hi, "units": units, + "notes": notes, + } + + +def cu_to_musd(cu_row, anchor_row): + """(p05, p50, p95) in MUSD/removal, elementwise CU_percentile x + anchor_musd_per_cu -- the identical multiplication WP6/WP14 already + performs for the anchor catalog's own cost_per_removal_musd rows.""" + a = float(anchor_row["estimate"]) + return (float(cu_row["p05"]) * a, float(cu_row["p50"]) * a, + float(cu_row["p95"]) * a) + + +def build_rows(d): + rows = [] + anchors = {s: d.wp6_anchor(s) for s in SCENARIOS} + baseline_n = {} + cu_baseline = {} + + for cat in (CAT_A, CAT_B): + baseline_n[cat] = d.wp6_baseline_n(cat) + n = baseline_n[cat] + + # FoM under both weightings, p05/p50/p95 [kg/CU] -- machine-read. + for w in WEIGHTINGS: + fr = d.wp6q(cat, "fom", "fom", weighting=w) + bw = d.wp6q(cat, "fom", "band_weight", weighting=w) + rows.append(row(catalog=cat, record_type="fom", metric="fom", + weighting=w, estimate=fr["estimate"], p05=fr["p05"], + p50=fr["p50"], p95=fr["p95"], units=fr["units"], + notes=fr["notes"])) + rows.append(row(catalog=cat, record_type="fom", metric="band_weight", + weighting=w, estimate=bw["estimate"], + units=bw["units"], notes=bw["notes"])) + + rmass = d.wp6q(cat, "fom", "removed_mass_per_mission") + rows.append(row(catalog=cat, record_type="fom", + metric="removed_mass_per_mission", + estimate=rmass["estimate"], units=rmass["units"], + notes=rmass["notes"])) + + # Cost per removal, CU, at the baseline (Delta-v-limited) N. + cu = d.wp6q(cat, "amortization", "cost_per_removal", n_kits=n) + cu_baseline[cat] = cu + rows.append(row(catalog=cat, record_type="cost_per_removal_cu", + metric="cost_per_removal", p05=cu["p05"], p50=cu["p50"], + p95=cu["p95"], units=cu["units"], + notes="baseline N=%d (the Delta-v-limited amortization " + "minimum, generated/wp6_cost_summary.csv " + "record_type=amortization)" % n)) + + # Cost per removal, MUSD, per cost_scenario -- via the anchor rows. + for s in SCENARIOS: + p05, p50, p95 = cu_to_musd(cu, anchors[s]) + if cat == CAT_A: + # CAT_A already has a dedicated WP6 cost_per_removal_musd row + # (the anchor was itself derived from CAT_A's own baseline + # cost). Cross-check rather than trust: this catches any + # future WP6 change silently drifting from this tool's own + # arithmetic. + wp6row = d.wp6q(cat, "cost_per_removal_musd", "cost_per_removal", + n_kits=n, cost_scenario=s) + for label, mine, theirs in (("p05", p05, wp6row["p05"]), + ("p50", p50, wp6row["p50"]), + ("p95", p95, wp6row["p95"])): + # Both operands are read back from 6-decimal CSV text, so + # each carries up to 5e-7 of independent rounding error; + # at the ~45 CU / ~0.06 MUSD/CU magnitudes here that + # compounds to ~3e-5 in the product -- a real drift bug + # would be orders of magnitude larger than this floor. + if abs(mine - float(theirs)) > 5e-5: + raise AssertionError( + "cost_per_removal_musd cross-check failed for " + "%s/%s/%s: computed %.6f != committed WP6 %.6f" + % (cat, s, label, mine, float(theirs))) + note = ("= cost_per_removal_CU(catalog=%s;baseline N=%d) x " + "anchor_musd_per_cu[%s]=%s MUSD/CU " + "(generated/wp6_cost_summary.csv " + "record_type=currency_anchor_derived); cross-checked " + "against that same file's own " + "record_type=cost_per_removal_musd row for this " + "catalog/scenario -- consistent to display precision " + "(this tool multiplies by the 6dp-rounded anchor while " + "the wp6 row uses the full-precision anchor, so the " + "last printed digit can differ by rounding path)" + % (cat, n, s, f6(anchors[s]["estimate"]))) + else: + note = ("= cost_per_removal_CU(catalog=%s;baseline N=%d) x " + "anchor_musd_per_cu[%s]=%s MUSD/CU (generated/" + "wp6_cost_summary.csv record_type=" + "currency_anchor_derived); NOTE: the anchor itself " + "was derived from catalog A's (%s) baseline campaign " + "cost (Sec.10 Method), not this catalog's own -- " + "applying it here assumes the CU->MUSD conversion " + "factor is campaign-structure-invariant across " + "catalogs rather than catalog-specific; a stated " + "simplifying assumption, not a hidden one" + % (cat, n, s, f6(anchors[s]["estimate"]), short(CAT_A))) + rows.append(row(catalog=cat, record_type="cost_per_removal_musd", + metric="cost_per_removal", cost_scenario=s, + estimate=f6(p50), p05=f6(p05), p50=f6(p50), + p95=f6(p95), units="MUSD_per_removal", notes=note)) + + # Envisat-class: no WP5/WP6 campaign rows -- say so explicitly. + rows.append(row(catalog=CAT_C, record_type="mission_class", + metric="not_kit_ranked", + notes="Envisat-class massive SSO payload has no WP5/WP6 " + "campaign rows (it is not part of the kit-installer " + "Monte Carlo sweep); FoM and cost_per_removal are " + "undefined for this class in this joined view. It " + "is evaluated instead as a separate controlled-" + "reentry mission class (spec WP13 Class C; see " + "generated/wp13_classC.md), not kit-ranked against " + "SL-16/SL-8.")) + + # Recommended kit + sail/EDT band, all three catalogs (wp13 only). + for cat in d.catalogs13: + rk = d.wp13v(cat, "recommended_kit") + rows.append(row(catalog=cat, record_type="recommended_kit", + kit_option=rk["kit_option"], notes=rk["notes"])) + sail = d.wp13v(cat, "sail_area_25yr") + rows.append(row(catalog=cat, record_type="sail_area_25yr", + value_lo=sail["value_lo"], value_hi=sail["value_hi"], + units=sail["units"], notes=sail["notes"])) + edt = d.wp13v(cat, "edt_years") + rows.append(row(catalog=cat, record_type="edt_years", + value_lo=edt["value_lo"], value_hi=edt["value_hi"], + units=edt["units"], notes=edt["notes"])) + + # Priority rank -- the deliverable conclusion of this document, not a + # measured quantity; the supporting FoM/cost/citation rows above and the + # one-paragraph defenses in the .md are the evidence for it. + rows.append(row(catalog=CAT_A, record_type="priority_rank", metric="rank", + estimate="1", + notes="mass-dominant FoM under BOTH weightings + " + "McKnight top-20-all-SL-16 citation; see the .md " + "defense paragraph")) + rows.append(row(catalog=CAT_B, record_type="priority_rank", metric="rank", + estimate="2", + notes="cheap closing sail kit, high per-class population; " + "see the .md defense paragraph")) + rows.append(row(catalog=CAT_C, record_type="priority_rank", metric="rank", + kit_option="not-kit-ranked", + notes="separate controlled-reentry mission class (spec " + "WP13 Class C), not ranked against the two " + "kit-installer classes above; see the .md defense " + "paragraph")) + + return rows, anchors, baseline_n, cu_baseline + + +def write_csv(path, rows): + lines = [",".join(CSV_HEADER)] + for r in rows: + cells = [] + for col in CSV_HEADER: + v = str(r[col]) + if any(c in v for c in (",", "\"", "\n")): + v = "\"%s\"" % v.replace("\"", "\"\"") + cells.append(v) + lines.append(",".join(cells)) + write_text(path, "\n".join(lines) + "\n") + + +def build_md(d, anchors, baseline_n, cu_baseline): + L = [] + w = L.append + + fomA_sp = d.wp6q(CAT_A, "fom", "fom", weighting="spatial_density") + fomA_cr = d.wp6q(CAT_A, "fom", "fom", weighting="criticality") + fomB_sp = d.wp6q(CAT_B, "fom", "fom", weighting="spatial_density") + fomB_cr = d.wp6q(CAT_B, "fom", "fom", weighting="criticality") + bwA_sp = d.wp6q(CAT_A, "fom", "band_weight", weighting="spatial_density") + bwA_cr = d.wp6q(CAT_A, "fom", "band_weight", weighting="criticality") + bwB_sp = d.wp6q(CAT_B, "fom", "band_weight", weighting="spatial_density") + bwB_cr = d.wp6q(CAT_B, "fom", "band_weight", weighting="criticality") + + w("# WP14 Prioritization: target-class ranking (cost + FoM + kit trade)") + w("") + w("A pure derived/joined view over three already-committed, already-") + w("regenerated CSVs -- `generated/wp6_cost_summary.csv` (schema %s), " + "`generated/wp13_kit_trade.csv` (schema %s), and " + "`generated/wp5_campaign_summary.csv` (schema %s). This tool runs no " + "new simulation and adds no new statistics; every number below is read " + "directly from those files, or is a single elementwise arithmetic " + "combination of two already-committed numbers (noted inline wherever " + "that occurs). Regenerated by " + "`tools/prioritization/make_prioritization.py` (Python 3 stdlib only, " + "R9)." % (d.wp6_schema, d.wp13_schema, d.wp5_schema)) + w("") + w("## Ranking (spec:265-266)") + w("") + w("1. **%s** -- mass-dominant FoM under both weightings; the McKnight " + "top-of-ranking class." % CAT_A) + w("2. **%s** -- cheap closing sail kit, high per-class population." + % CAT_B) + w("3. **%s** -- a separate controlled-reentry mission class, " + "NOT kit-ranked against the two classes above." % CAT_C) + w("") + w("## Legal/consent gate (D12)") + w("") + w(D12_SENTENCE) + w("") + + # ---- Catalog A ----------------------------------------------------- + w("## 1. %s (priority 1)" % CAT_A) + w("") + w("### Figure of merit (debris-risk reduction per cost), p50 [kg/CU]") + w("") + w("| weighting | FoM p50 | FoM p05..p95 | band weight w(h) |") + w("|---|---:|---|---:|") + w("| spatial_density | %s | %s..%s | %s |" % ( + f1(fomA_sp["p50"]), f1(fomA_sp["p05"]), f1(fomA_sp["p95"]), + f2(bwA_sp["estimate"]))) + w("| criticality | %s | %s..%s | %s |" % ( + f1(fomA_cr["p50"]), f1(fomA_cr["p05"]), f1(fomA_cr["p95"]), + f2(bwA_cr["estimate"]))) + w("") + nA = baseline_n[CAT_A] + cuA = cu_baseline[CAT_A] + w("### Cost per removal (baseline N=%d, the Delta-v-limited amortization " + "minimum)" % nA) + w("") + w("- CU: p05 %s / p50 %s / p95 %s CU/removal (relative cost units, " + "primary metric)." % (f2(cuA["p05"]), f2(cuA["p50"]), f2(cuA["p95"]))) + w("- MUSD (external, low-standing -- see the honesty framing quoted in " + "the WP14 evidence-pack cost section), p50 per cost_scenario:") + for s in SCENARIOS: + p05, p50, p95 = cu_to_musd(cuA, anchors[s]) + w(" - %s: p05 %s / p50 %s / p95 %s MUSD/removal" % ( + s, f3(p05), f3(p50), f3(p95))) + w("") + w("### Recommended kit") + w("") + rkA = d.wp13v(CAT_A, "recommended_kit") + sailA = d.wp13v(CAT_A, "sail_area_25yr") + edtA = d.wp13v(CAT_A, "edt_years") + w("- kit_option: `%s`" % rkA["kit_option"]) + w("- sail area, 25-yr guideline (solar max..min): %s m^2 -- " + "impractical at the upper end." % band_str(sailA["value_lo"], + sailA["value_hi"], f0)) + w("- EDT deorbit-time band: %s yr %s" % ( + band_str(edtA["value_lo"], edtA["value_hi"], f1), edtA["notes"])) + w("- verbatim rationale, quoted from `generated/wp13_kit_trade.csv` (never " + "hand-transcribed): \"%s\"" % rkA["notes"]) + w("") + w("### Prioritization defense (one paragraph)") + w("") + w("The %s class ranks first because its FoM dominates the %s class " + "under BOTH weightings (spatial-density %s vs %s kg/CU p50; " + "criticality %s vs %s kg/CU p50 -- computed from " + "generated/wp6_cost_summary.csv), which is the mass-dominance result " + "the WP6/WP14 cost model already establishes; that dominance is " + "corroborated, not merely asserted, by the published debris-ranking " + "literature: %s The class is the ADSC catalog-A anchor for exactly " + "this reason. Its recommended kit is an honest EDT candidate, not a " + "closed recommendation: sail-only does not close the 25-year " + "guideline for this class (%s m^2, impractical), so the " + "electrodynamic-tether branch is carried instead, and that branch " + "remains open on the libration/dynamic-tether-stability question " + "(T7, Pelaez et al. 2000, cited in wp13-literature.md) -- the EDT " + "years band above folds libration in only as a flat PLACEHOLDER " + "duty-cycle knob, never claimed solved. Legal accessibility is a " + "gate, not a factor in this ranking (D12, above)." % ( + short(CAT_A), short(CAT_B), f1(fomA_sp["p50"]), f1(fomB_sp["p50"]), + f1(fomA_cr["p50"]), f1(fomB_cr["p50"]), MCKNIGHT_CITATION, + band_str(sailA["value_lo"], sailA["value_hi"], f0))) + w("") + + # ---- Catalog B ----------------------------------------------------- + w("## 2. %s (priority 2)" % CAT_B) + w("") + w("### Figure of merit (debris-risk reduction per cost), p50 [kg/CU]") + w("") + w("| weighting | FoM p50 | FoM p05..p95 | band weight w(h) |") + w("|---|---:|---|---:|") + w("| spatial_density | %s | %s..%s | %s |" % ( + f1(fomB_sp["p50"]), f1(fomB_sp["p05"]), f1(fomB_sp["p95"]), + f2(bwB_sp["estimate"]))) + w("| criticality | %s | %s..%s | %s |" % ( + f1(fomB_cr["p50"]), f1(fomB_cr["p05"]), f1(fomB_cr["p95"]), + f2(bwB_cr["estimate"]))) + w("") + nB = baseline_n[CAT_B] + cuB = cu_baseline[CAT_B] + w("### Cost per removal (baseline N=%d, the Delta-v-limited amortization " + "minimum)" % nB) + w("") + w("- CU: p05 %s / p50 %s / p95 %s CU/removal (relative cost units, " + "primary metric)." % (f2(cuB["p05"]), f2(cuB["p50"]), f2(cuB["p95"]))) + w("- MUSD (external, low-standing; see the honesty framing quoted in " + "the WP14 evidence-pack cost section), p50 per cost_scenario -- note " + "this catalog reuses catalog A's currency anchor (see the CSV notes " + "column for the stated simplifying assumption):") + for s in SCENARIOS: + p05, p50, p95 = cu_to_musd(cuB, anchors[s]) + w(" - %s: p05 %s / p50 %s / p95 %s MUSD/removal" % ( + s, f3(p05), f3(p50), f3(p95))) + w("") + w("### Recommended kit") + w("") + rkB = d.wp13v(CAT_B, "recommended_kit") + sailB = d.wp13v(CAT_B, "sail_area_25yr") + w("- kit_option: `%s`" % rkB["kit_option"]) + w("- sail area, 25-yr guideline (solar max..min): %s m^2 -- tens of " + "square meters, a practical kit; no EDT kit needed for this class." + % band_str(sailB["value_lo"], sailB["value_hi"], f0)) + w("- verbatim rationale, quoted from `generated/wp13_kit_trade.csv` (never " + "hand-transcribed): \"%s\"" % rkB["notes"]) + w("") + w("### Prioritization defense (one paragraph)") + w("") + w("The %s class ranks second: its FoM is smaller than %s's under both " + "weightings (mass-dominated, as above), but it closes today with a " + "cheap, practical sail kit (%s m^2 for the 25-year guideline, tens of " + "square meters -- no EDT branch and no open T7 caveat needed), and it " + "represents a much larger per-class population than the SL-16 top-20 " + "concern list: %s Cost per removal at its own baseline N is " + "essentially the same as %s's in CU terms (%s vs %s CU/removal p50), " + "so the batch-amortization economics are not a differentiator between " + "the two classes -- kit simplicity and closure certainty are. Legal " + "accessibility is a gate, not a factor in this ranking (D12, above)." + % (short(CAT_B), short(CAT_A), + band_str(sailB["value_lo"], sailB["value_hi"], f0), + SL8_POPULATION_CITATION, short(CAT_A), f2(cuB["p50"]), + f2(cuA["p50"]))) + w("") + + # ---- Catalog C (Envisat) ------------------------------------------- + w("## 3. %s (separate mission class, not kit-ranked)" % CAT_C) + w("") + w("No WP5/WP6 campaign rows exist for this catalog (it is not part of " + "the kit-installer Monte Carlo sweep), so FoM and cost_per_removal " + "are undefined for it here -- stated rather than silently omitted.") + w("") + rkC = d.wp13v(CAT_C, "recommended_kit") + sailC = d.wp13v(CAT_C, "sail_area_25yr") + edtC = d.wp13v(CAT_C, "edt_years") + w("### Why it is not a kit choice") + w("") + w("- sail area, 25-yr guideline (solar max..min): %s m^2 (context " + "only -- not the recommendation)." % band_str( + sailC["value_lo"], sailC["value_hi"], f0)) + w("- EDT deorbit-time band: %s yr (context only -- not the " + "recommendation)." % band_str(edtC["value_lo"], edtC["value_hi"], f1)) + w("- kit_option: `%s`" % rkC["kit_option"]) + w("- verbatim rationale, quoted from `generated/wp13_kit_trade.csv` (never " + "hand-transcribed): \"%s\"" % rkC["notes"]) + w("") + w("### Prioritization defense (one paragraph)") + w("") + w("The %s is evaluated as a separate controlled-reentry mission class " + "(spec WP13 Class C), not ranked against %s or %s: %s Controlled-" + "deorbit delta-v, casualty-risk framing and cost detail for this " + "candidate live in `generated/wp13_classC.{csv,md}`, out of scope for " + "this cost/FoM joined view; what belongs here is the negative result " + "itself, stated plainly -- a sail or EDT kit-only deorbit is not the " + "recommended path for an 8,211 kg, ~98.4 deg payload, so it is not " + "assigned a kit-ranked priority number alongside the SL-16/SL-8 " + "classes above. Legal accessibility is a gate, not a factor here " + "either (D12, above)." % (short(CAT_C), short(CAT_A), short(CAT_B), + ENVISAT_CITATION)) + w("") + + w("## Honesty note (T5, criticality weighting citation basis)") + w("") + w(HONESTY_NOTE) + w("") + w("---") + w("*Generated by `tools/prioritization/make_prioritization.py` from " + "committed artifacts only. This is not legal advice.*") + w("") + return "\n".join(L) + + +def main(): + if len(sys.argv) < 2: + print("usage: make_prioritization.py [out_dir]", + file=sys.stderr) + return 2 + repo = sys.argv[1] + gen = os.path.join(repo, "generated") + out_dir = sys.argv[2] if len(sys.argv) > 2 else gen + os.makedirs(out_dir, exist_ok=True) + + d = Data(gen) + rows, anchors, baseline_n, cu_baseline = build_rows(d) + + csv_path = os.path.join(out_dir, "wp14_prioritization.csv") + md_path = os.path.join(out_dir, "wp14_prioritization.md") + write_csv(csv_path, rows) + md_text = build_md(d, anchors, baseline_n, cu_baseline) + md_text.encode("ascii") # hard guarantee: ASCII only + write_text(md_path, md_text) + print("[WP14] wrote wp14_prioritization.csv (%d rows) + " + "wp14_prioritization.md (%d lines)" + % (len(rows), md_text.count("\n") + 1)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/prioritization/test_prioritization.py b/tools/prioritization/test_prioritization.py new file mode 100644 index 0000000..632f687 --- /dev/null +++ b/tools/prioritization/test_prioritization.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""WP14 prioritization-table tests (registered with ctest, run by CMake's +python3). + + test_prioritization.py + +Checks (returns non-zero on any failure, R4), mirroring tools/viz/test_viz.py: + 1. Regenerating from the committed CSVs (wp6_cost_summary.csv, + wp13_kit_trade.csv, wp5_campaign_summary.csv) produces both expected + files and the generator itself exits 0 (its own internal cross-check + assertion between this tool's arithmetic and the committed WP6 anchor + row is part of that exit code). + 2. ASCII-only, LF-only, no stub markers (TODO/FIXME/XXX/TBD), no wall-clock + timestamp markers (R6), and no banned overclaim language. + 3. D10/R6 currency discipline: no point-dollar glyph anywhere, and every + currency-bearing row (MUSD units) is a genuine range (p05 < p95). + 4. Content: all three catalogs present (SL-16, SL-8, Envisat-class); the + Envisat-class row states it has no campaign rows; the D12 sentence, + the McKnight citation and the T5/honesty note are present in the .md. + 5. Determinism: two generator runs are byte-identical, and both match the + committed generated/wp14_prioritization.{csv,md} exactly (reproducibility + -- the same contract CI enforces with git diff). +""" +import csv +import hashlib +import os +import re +import subprocess +import sys +import tempfile + +STUB_MARKERS = ("TODO", "FIXME", "XXX", "TBD") +TIMESTAMP_MARKERS = ("2026-", "2027-", "UTC", "GMT", "generated at", "generated on") +BANNED_CLAIM_PATTERNS = ( + (r"flight[\s-]*(ready|proven)", "flight-ready/-proven"), + (r"guarantee", "guarantee(d/s)"), + (r"approved", "approved"), + (r"licensed", "licensed"), + (r"compliant\b", "compliant"), +) +EXPECTED = ("wp14_prioritization.csv", "wp14_prioritization.md") + +fails = [] + + +def check(cond, msg): + if not cond: + fails.append(msg) + + +def sha(path): + with open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + +def load_csv(path): + with open(path, newline="", encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +def main(): + if len(sys.argv) < 2: + print("usage: test_prioritization.py ", file=sys.stderr) + return 2 + repo = os.path.abspath(sys.argv[1]) + gen = os.path.join(repo, "generated") + make = os.path.join(repo, "tools", "prioritization", "make_prioritization.py") + committed = {name: os.path.join(gen, name) for name in EXPECTED} + + for name, path in committed.items(): + if not os.path.exists(path): + print("FAIL: committed generated/%s is missing" % name) + return 1 + + # 1. Regenerate into a scratch dir; the generator's own exit code carries + # its internal cross-check (this tool's own CU x anchor arithmetic vs + # the committed WP6 cost_per_removal_musd row for catalog A). + tmp = tempfile.mkdtemp(prefix="wp14_prioritization_") + r = subprocess.run([sys.executable, make, repo, tmp], + capture_output=True, text=True) + check(r.returncode == 0, + "make_prioritization failed: %s" % (r.stderr.strip() or r.stdout.strip())) + + for name in EXPECTED: + path = os.path.join(tmp, name) + check(os.path.exists(path), "missing regenerated %s" % name) + if fails: + for f in fails: + print("FAIL:", f) + print("prioritization: %d failure(s)" % len(fails)) + return 1 + + csv_txt = open(os.path.join(tmp, "wp14_prioritization.csv"), encoding="utf-8").read() + md_txt = open(os.path.join(tmp, "wp14_prioritization.md"), encoding="utf-8").read() + + # 2. Hygiene: ASCII, LF-only, no stub markers / timestamps / banned claims. + for name, txt in (("wp14_prioritization.csv", csv_txt), + ("wp14_prioritization.md", md_txt)): + try: + txt.encode("ascii") + except UnicodeEncodeError: + check(False, "%s: non-ASCII content" % name) + with open(os.path.join(tmp, name), "rb") as f: + check(b"\r" not in f.read(), "%s: contains CR bytes (must be LF)" % name) + for m in STUB_MARKERS + TIMESTAMP_MARKERS: + check(m not in txt, "%s: contains forbidden marker '%s'" % (name, m)) + low = txt.lower() + for pat, why in BANNED_CLAIM_PATTERNS: + check(re.search(pat, low) is None, + "%s: contains forbidden claim language: %s" % (name, why)) + check(re.search(r"\bTRL\s*[56]\b", txt) is None, + "%s: claims TRL 5/6" % name) + + # 3. D10/R6 currency discipline: never a point-dollar glyph; every + # currency-bearing (MUSD) row is a genuine range (p05 strictly < p95). + check("$" not in csv_txt, "csv: contains a point-dollar glyph") + check("$" not in md_txt, "md: contains a point-dollar glyph") + rows = load_csv(os.path.join(tmp, "wp14_prioritization.csv")) + musd_rows = [r for r in rows if "MUSD" in r["units"]] + check(len(musd_rows) > 0, "csv: no MUSD-denominated rows found at all") + for r in musd_rows: + p05, p95 = float(r["p05"]), float(r["p95"]) + check(p05 < p95, + "csv: MUSD row (%s/%s/%s) is not a genuine range (p05 %.6f !< p95 %.6f)" + % (r["catalog"], r["record_type"], r["cost_scenario"], p05, p95)) + + # 4. Content checks. + catalogs = {r["catalog"] for r in rows if r["catalog"]} + for cat in ("SL-16 / Zenit-2 second stage", "SL-8 / Kosmos-3M second stage", + "Envisat-class massive SSO payload"): + check(cat in catalogs, "csv: missing catalog %r" % cat) + check(any(r["record_type"] == "mission_class" + and r["catalog"] == "Envisat-class massive SSO payload" + for r in rows), + "csv: Envisat-class row does not state it has no campaign rows") + check("no wp5/wp6 campaign rows" in md_txt.lower(), + "md: Envisat-class section does not state it has no campaign rows") + check("D12" in md_txt, "md: missing the D12 legal-gate reference") + check("gate and metadata flags, never a multiplier" in md_txt, + "md: missing the D12 gate-not-multiplier sentence") + check("McKnight" in md_txt, "md: missing the McKnight citation") + check("doi:10.1016/j.actaastro.2021.01.021" in md_txt, + "md: McKnight citation lacks its DOI") + check("Honesty note" in md_txt, "md: missing the honesty-note section") + check("T5" in md_txt, "md: missing a T5 reference in the honesty note") + check("PLACEHOLDER" in md_txt, + "md: honesty note must acknowledge the non-peak weight table is " + "still PLACEHOLDER") + + # every non-mission_class/priority_rank row must carry a source-bearing + # notes field (D10-style discipline: never a bare, unexplained number). + for r in rows: + if r["record_type"] in ("fom", "cost_per_removal_cu", + "cost_per_removal_musd", "recommended_kit", + "sail_area_25yr", "edt_years"): + check(r["notes"].strip() != "", + "csv row %s/%s/%s has an empty notes field" + % (r["catalog"], r["record_type"], r.get("weighting", ""))) + + # 5. Determinism + committed == regenerated. + tmp2 = tempfile.mkdtemp(prefix="wp14_prioritization_2_") + r2 = subprocess.run([sys.executable, make, repo, tmp2], + capture_output=True, text=True) + check(r2.returncode == 0, "second make_prioritization run failed") + if r2.returncode == 0: + for name in EXPECTED: + h1 = sha(os.path.join(tmp, name)) + h2 = sha(os.path.join(tmp2, name)) + check(h1 == h2, "%s: generator is not deterministic across runs" % name) + + for name, cpath in committed.items(): + tpath = os.path.join(tmp, name) + if not os.path.exists(tpath): + continue + with open(cpath, "rb") as a, open(tpath, "rb") as b: + cb, tb = a.read(), b.read() + # normalize CRLF->LF so a Windows checkout of the committed file still + # matches the LF the generator writes (CI enforces raw bytes via git). + check(cb.replace(b"\r\n", b"\n") == tb, + "%s: committed bytes != regenerated (reproducibility)" % name) + + if fails: + for f in fails: + print("FAIL:", f) + print("prioritization: %d failure(s)" % len(fails)) + return 1 + print("prioritization: all checks passed (%d artifacts, %d rows)" + % (len(EXPECTED), len(rows))) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/readme/fill_readme_numbers.py b/tools/readme/fill_readme_numbers.py new file mode 100644 index 0000000..74af405 --- /dev/null +++ b/tools/readme/fill_readme_numbers.py @@ -0,0 +1,462 @@ +#!/usr/bin/env python3 +"""README marker-block filler (WP14 review item 0.4b / C4). + + fill_readme_numbers.py [--check] + +Regenerates ONLY the text strictly between the two committed marker-comment +pairs in README.md -- + + + ... + + + + ... + + +-- from the two committed CSVs (generated/wp5_campaign_summary.csv, +generated/wp6_cost_summary.csv). Every NUMBER quoted inside a block (table +cell or inline prose) is read mechanically from those CSVs with a fixed +rounding; the surrounding prose/table skeleton is literal template text (it +does not reflow). This closes the review's C4 finding: before this tool, +README.md was hand-updated and could silently drift from the committed CSVs +it claims to summarize. + +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). + +Python 3 standard library only (spec v4.2 R9). This tool's own SOURCE stays +pure ASCII (spec R11): the README's existing Unicode punctuation/math glyphs +(em dash, Delta, times, Sigma, subscript i, ...) 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 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) +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 = ("") +WP5_END = "" +WP6_START = "" +WP6_END = "" + +# Fixed catalog display labels (literal template text, D2/D13: catalog +# identity/altitude is out of WP14's scope -- see the WP14 integration map's +# trap list on CATALOG_ALT_KM-style hardcoded mirrors). +CATALOG_SHORT = { + "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): + 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 load_csv(path): + with open(path, newline="", encoding="utf-8") as f: + return list(csv.DictReader(f)) + + +class Wp5Data: + """generated/wp5_campaign_summary.csv (schema 1.1, WP5's own additive + dispersion_set_id/worst_abort_clearance_m bump -- unrelated to WP14 and + unread here).""" + + 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"])) + + 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")) + + 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 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] + + +def rate_cell(row): + """'**0.556** [0.512, 0.599]' from a WP5 rate row (estimate/wilson_low/ + wilson_high), 3dp.""" + return "**%s** [%s, %s]" % (f3(row["estimate"]), f3(row["wilson_low"]), + f3(row["wilson_high"])) + + +def ko_cell(row): + """rate_cell() plus the '[L0, ds-v1]' fidelity/dispersion tag, both + parsed out of the row's own notes field (never hand-typed) so a future + dispersion-set or fidelity-level change is reflected automatically.""" + 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 build_wp5_block(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_block(d5, 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) + # 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 + + 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] + + 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("") + 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 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 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)) + return text + + +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_readme_numbers.py [--check]", + file=sys.stderr) + return 2 + root = os.path.abspath(positional[0]) + readme_path = os.path.join(root, "README.md") + + with open(readme_path, encoding="utf-8") as f: + original = f.read() + + d5 = Wp5Data(root) + d6 = Wp6Data(root) + new_text = rewrite(original, d5, d6) + + if check_mode: + if new_text == original: + print("[readme_numbers] README.md marker blocks match the " + "committed CSVs") + return 0 + print("[readme_numbers] README.md marker blocks are STALE vs the " + "committed CSVs", 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) + return 1 + + with open(readme_path, "w", encoding="utf-8", newline="\n") as f: + f.write(new_text) + if new_text == original: + print("[readme_numbers] README.md marker blocks already up to date") + else: + print("[readme_numbers] README.md marker blocks rewritten") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/readme/test_readme_numbers.py b/tools/readme/test_readme_numbers.py new file mode 100644 index 0000000..7c05431 --- /dev/null +++ b/tools/readme/test_readme_numbers.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""README marker-block filler test (registered with ctest as `readme_numbers`). + + test_readme_numbers.py + +Checks (non-zero exit on any failure, R4): + 1. --check mode passes against the real, committed README.md -- i.e. the + WP5-NUMBERS/WP6-NUMBERS marker blocks already reproduce byte-for-byte + from the committed CSVs (closes review finding C4: README cannot + silently drift from generated/wp5_campaign_summary.csv / + generated/wp6_cost_summary.csv 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__)) + +fails = [] +def check(cond, msg): + if not cond: + fails.append(msg) + + +def sha(path): + with open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + +def make_scratch_repo(repo): + """Copy just the inputs fill_readme_numbers.py needs (README.md plus the + two committed CSVs it reads) into a throwaway directory, so the + corruption test can mutate a copy without touching the real working + tree.""" + tmp = tempfile.mkdtemp(prefix="wp14_readme_") + os.makedirs(os.path.join(tmp, "generated")) + shutil.copy(os.path.join(repo, "README.md"), os.path.join(tmp, "README.md")) + for name in ("wp5_campaign_summary.csv", "wp6_cost_summary.csv"): + 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_readme_numbers.py"), root] + + list(args), + capture_output=True, text=True) + + +def main(): + if len(sys.argv) < 2: + print("usage: test_readme_numbers.py ", file=sys.stderr) + return 2 + repo = os.path.abspath(sys.argv[1]) + + # 1. --check against the real, committed README.md must pass. + r = run_tool(repo, "--check") + check(r.returncode == 0, + "fill_readme_numbers.py --check failed against the committed " + "README: %s" % (r.stdout + r.stderr).strip()) + + # 2. Corrupt a pinned number inside the WP6 marker block in a scratch + # copy; --check must now fail. + tmp1 = make_scratch_repo(repo) + try: + readme_path = os.path.join(tmp1, "README.md") + with open(readme_path, encoding="utf-8") as f: + text = f.read() + check("44.80" in text, + "fixture assumption stale: pinned number '44.80' not found " + "in README.md") + corrupted = text.replace("44.80", "99.99", 1) + check(corrupted != text, "corruption was a no-op -- fixture stale") + with open(readme_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; the local checkout's CRLF vs the tool's canonical + # LF output are not a real difference -- read both in text mode, + # which normalizes line endings, 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: + readme_path = os.path.join(tmp2, "README.md") + with open(readme_path, encoding="utf-8") as f: + before_text = f.read() + r3 = run_tool(tmp2) + check(r3.returncode == 0, + "default-mode run failed: %s" % (r3.stdout + r3.stderr).strip()) + with open(readme_path, encoding="utf-8") as f: + after_text = f.read() + check(before_text == after_text, + "rewriting an already-correct README changed its content " + "(not idempotent)") + after_sha = sha(readme_path) + r4 = run_tool(tmp2) + check(r4.returncode == 0, "second default-mode run failed: %s" + % (r4.stdout + r4.stderr).strip()) + after2_sha = sha(readme_path) + 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("readme_numbers: %d failure(s)" % len(fails)) + return 1 + print("readme_numbers: all checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/regenerate_all.sh b/tools/regenerate_all.sh index 357367e..25ab953 100644 --- a/tools/regenerate_all.sh +++ b/tools/regenerate_all.sh @@ -13,27 +13,29 @@ set -euo pipefail cd "$(dirname "$0")/.." # repo root BUILD="${1:-build}" -echo "[1/11] WP5 campaign Monte Carlo (N=500 x 2 catalogs)" +echo "[1/12] WP5 campaign Monte Carlo (N=500 x 2 catalogs)" "$BUILD/adsc_campaign" 500 generated -echo "[2/11] WP12 fidelity ladder (L0/L1/L2 re-verification of the WP5 abort events)" +echo "[2/12] WP12 fidelity ladder (L0/L1/L2 re-verification of the WP5 abort events)" "$BUILD/adsc_ladder" 500 generated -echo "[3/11] WP6 cost model + FoM (kit sweep, consumes WP5)" +echo "[3/12] WP6 cost model + FoM (kit sweep, consumes WP5)" "$BUILD/adsc_cost" 500 generated -echo "[4/11] WP3 decay-trade CSV" +echo "[4/12] WP3 decay-trade CSV" "$BUILD/decay_trade" generated -echo "[5/11] WP13 kit-class trade + class-C controlled-reentry comparison" +echo "[5/12] WP13 kit-class trade + class-C controlled-reentry comparison" "$BUILD/kit_trade" generated -echo "[6/11] T6 small-debris flux sweep" +echo "[6/12] WP14 prioritization table (joined view of WP6 cost + WP13 kit trade)" +python3 tools/prioritization/make_prioritization.py . generated +echo "[7/12] T6 small-debris flux sweep" "$BUILD/flux_sweep" generated -echo "[7/11] WP7 reference metrics (WP1/F1/WP2/WP3/WP4 pinned numbers)" +echo "[8/12] WP7 reference metrics (WP1/F1/WP2/WP3/WP4 pinned numbers)" "$BUILD/sim_metrics" generated -echo "[8/11] WP10c keep-out violation forensics (read-only replay of WP5)" +echo "[9/12] WP10c keep-out violation forensics (read-only replay of WP5)" python3 tools/forensics/make_forensics.py -echo "[9/11] WP7a visualization pack" +echo "[10/12] WP7a visualization pack" python3 tools/viz/make_viz.py . generated/viz -echo "[10/11] WP8 compliance precheck + matrix (not legal advice)" +echo "[11/12] WP8 compliance precheck + matrix (not legal advice)" python3 tools/compliance/check_compliance.py python3 tools/compliance/generate_matrix.py -echo "[11/11] WP7 evidence pack" +echo "[12/12] WP7 evidence pack" python3 tools/evidence/make_evidence.py echo "regenerate_all: complete"