1"""One graded list of limits for a Layer X burn (engine/layerx/diag/limits.py).
3What these check, and against what:
5* **Today's UI grades.** ``verdictItems`` in ``frontend/src/components/layerx/LayerXResult.tsx`` is
6 transcribed below, line for line, as an independent oracle (``_ui_verdict``), and every saved burn
7 in ``.userdata`` (read only) must grade the same through :func:`grade`. The saved runs are the
8 user's; when none is present the parity test skips and the hand-built results below still run.
9* **Worst-case times, by hand.** For the 2026-10-02 flown GN2 run ``7e47d1`` the audit (AUDIT 9.9 D)
10 worked out each limit's worst time from the series by hand: LOX dP/Pc 0.3082 at 0.75 s, fuel
11 0.2858 at 0.20 s, both tank peaks at burnout (3.466 s), the sag at 0.40 s (fuel), chug 1.2046 at
12 0.80 s. Those numbers are asserted.
13* **The index is the time.** For every series-backed limit on every saved run, the series value at
14 ``index_worst`` is the graded value and ``series.t[index_worst]`` is ``t_worst``.
15* **Each new grader on a hand-built result**: thresholds at, just inside and just outside their
16 edges, so a flipped comparison or a wrong default goes red.
19from __future__
import annotations
25from pathlib
import Path
31RUNS = Path(__file__).resolve().parents[1] /
".userdata" /
"local" /
"engine" /
"layerx" /
"runs"
32REFERENCE = RUNS /
"20261002-231658-7e47d1.json"
33PSI = 6894.757293168361
38 for f
in sorted(glob.glob(str(RUNS /
"*.json"))):
40 d = json.loads(Path(f).read_text())
43 if d.get(
"kind") ==
"run" and isinstance(d.get(
"result"), dict)
and d[
"result"].get(
"series"):
44 out.append((Path(f).name, d[
"result"]))
49needs_saved = pytest.mark.skipif(
not SAVED, reason=
"no saved Layer X burns in .userdata (the user's runs)")
53 return {e[
"key"]: e
for e
in entries}
59VERDICT = {
"copvHeadroomPsi": 100,
"droopPsi": (30, 60),
"residualKg": 0.2,
"stiffnessFloor": 0.15,
60 "chugMarginWarn": 1.2,
"ratingUse": 0.8,
"depletionTie": 0.03,
"replayAgreement": (0.005, 0.02)}
64 """``verdictItems`` + the context ``LayerXResultView`` builds, as the UI ran on 2026-10-02.
65 Returns {my key: grade} for the items the UI would show."""
67 prov = result.get(
"provenance")
or {}
68 d = prov.get(
"derived")
or {}
69 band = d.get(
"stiffness_band")
or {}
70 roles = d.get(
"roles")
or {}
71 mawps = d.get(
"tank_mawp_psi")
or {}
72 ambient = d[
"ambient_pa"] / PSI
if isinstance(d.get(
"ambient_pa"), (int, float))
else 14.6959
73 dvs = (result.get(
"delivered")
or {}).get(
"summary")
80 return "bad" if v < b[0]
else "warn" if v > b[1]
else "ok"
81 return "bad" if v < VERDICT[
"stiffnessFloor"]
else "ok"
83 if dvs
and dvs.get(
"chug_margin_min")
is not None and math.isfinite(dvs[
"chug_margin_min"]):
84 m = dvs[
"chug_margin_min"]
85 out[
"chug_margin"] =
"bad" if m < 1
else "warn" if m < VERDICT[
"chugMarginWarn"]
else "ok"
86 out[
"stiffness_ox"] = stiff(s[
"ox"].get(
"stiffness_min"), band.get(
"oxidiser"))
87 out[
"stiffness_fuel"] = stiff(s[
"fuel"].get(
"stiffness_min"), band.get(
"fuel"))
88 for side, short
in ((
"oxidiser",
"ox"), (
"fuel",
"fuel")):
89 peak = s[short].get(
"peak_psia")
90 rating = mawps.get(roles.get(side))
91 if peak
is None or rating
is None:
93 use = (peak - ambient) / rating
95 out[f
"tank_mawp_{short}"] =
"info"
96 lockup =
max(s[
"ox"][
"t0_psia"], s[
"fuel"][
"t0_psia"])
97 margin = s[
"copv_end_psia"] - lockup
if s.get(
"copv_end_psia")
is not None else None
98 h = VERDICT[
"copvHeadroomPsi"]
99 out[
"bottle_margin"] =
"warn" if margin
is None else "bad" if margin < h
else "warn" if margin < 2 * h
else "ok"
100 droop =
max(s[
"ox"][
"t0_psia"] - (s[
"ox"].get(
"min_psia")
if s[
"ox"].get(
"min_psia")
is not None else s[
"ox"][
"t0_psia"]),
101 s[
"fuel"][
"t0_psia"] - (s[
"fuel"].get(
"min_psia")
if s[
"fuel"].get(
"min_psia")
is not None else s[
"fuel"][
"t0_psia"]))
102 out[
"tank_sag"] =
"bad" if droop > VERDICT[
"droopPsi"][1]
else "warn" if droop > VERDICT[
"droopPsi"][0]
else "ok"
103 side = s.get(
"depleted_side")
104 residual = s[
"fuel"][
"residual_kg"]
if side ==
"oxidiser" else s[
"ox"][
"residual_kg"]
if side ==
"fuel" else None
105 if residual
is not None:
106 tie = VERDICT[
"depletionTie"] * (s[
"ox"][
"loaded_kg"] + s[
"fuel"][
"loaded_kg"])
108 out[
"depletion_tie"] =
"info"
109 if residual > VERDICT[
"residualKg"]:
110 out[
"residual"] =
"warn"
111 if result.get(
"converged")
is False:
112 out[
"unsettled"] =
"warn"
113 if s.get(
"card_outside_steps"):
114 out[
"card_outside"] =
"warn"
115 ec = result.get(
"engine_check")
or {}
116 if ec.get(
"available")
and ec.get(
"worst"):
118 gap =
max(w[
"pc"], w[
"mdot_O"], w[
"mdot_F"], 0
if ec.get(
"against") ==
"replay" else w[
"thrust"])
119 if gap >= VERDICT[
"replayAgreement"][0]:
120 out[
"engine_fit"] =
"warn" if gap < VERDICT[
"replayAgreement"][1]
else "bad"
121 if s.get(
"failed_steps")
or not s.get(
"t0_settled"):
122 out[
"solver"] =
"warn"
126@pytest.mark.skipif(not REFERENCE.is_file(), reason=
"the 2026-10-02 reference run 7e47d1 is not in .userdata")
128 """AUDIT 9.9 D, worked by hand from the series of run 7e47d1."""
129 result = json.loads(REFERENCE.read_text())[
"result"]
131 assert g[
"chug_margin"][
"value"] == pytest.approx(1.2046, abs=1e-4)
132 assert g[
"chug_margin"][
"t_worst"] == pytest.approx(0.80, abs=1e-9)
133 assert g[
"chug_margin"][
"grade"] ==
"ok"
134 assert g[
"stiffness_ox"][
"value"] == pytest.approx(0.3082, abs=1e-4)
135 assert g[
"stiffness_ox"][
"t_worst"] == pytest.approx(0.75, abs=1e-9)
136 assert g[
"stiffness_fuel"][
"value"] == pytest.approx(0.2858, abs=1e-4)
137 assert g[
"stiffness_fuel"][
"t_worst"] == pytest.approx(0.20, abs=1e-9)
139 assert g[
"stiffness_fuel_ignition"][
"value"] == pytest.approx(0.2860, abs=1e-4)
140 assert g[
"stiffness_fuel_ignition"][
"t_worst"] == pytest.approx(0.15, abs=1e-9)
141 assert g[
"stiffness_fuel_ignition"][
"grade"] ==
"info"
142 for short
in (
"ox",
"fuel"):
143 assert g[f
"tank_mawp_{short}"][
"t_worst"] == pytest.approx(3.4663, abs=1e-4)
145 assert g[
"tank_mawp_ox"][
"value"] == pytest.approx(580.28 - 101325.0 / PSI, abs=0.01)
146 assert g[
"bottle_margin"][
"value"] == pytest.approx(1405.71 - 578.05, abs=0.01)
147 assert g[
"tank_sag"][
"value"] == pytest.approx(578.0486 - 552.0311, abs=1e-3)
148 assert g[
"tank_sag"][
"t_worst"] == pytest.approx(0.40, abs=1e-9)
149 assert g[
"depletion_tie"][
"grade"] ==
"info" and g[
"depletion_tie"][
"value"] == pytest.approx(0.0177, abs=1e-4)
151 assert g[
"tank_cap_fuel"][
"limit"] == 600.0
and g[
"tank_cap_fuel"][
"grade"] ==
"info"
154 assert [k
for k, e
in g.items()
if e[
"grade"] ==
"bad"] == [
"rail_exit"]
155 assert overall([e
for k, e
in g.items()
if k !=
"rail_exit"]) ==
"ok"
158SERIES_BACKED = {
"stiffness_ox":
"ox.stiffness",
"stiffness_fuel":
"fuel.stiffness",
159 "tank_mawp_ox":
"ox.tank_psia",
"tank_mawp_fuel":
"fuel.tank_psia",
160 "tank_cap_ox":
"ox.tank_psia",
"tank_cap_fuel":
"fuel.tank_psia"}
164@pytest.mark.parametrize("name,result", SAVED, ids=[n for n, _ in SAVED])
166 series = result[
"series"]
167 d = result[
"provenance"].get(
"derived")
or {}
168 zero = d.get(
"gauge_zero_pa", 101325.0) / PSI
170 for e
in grade(result):
173 assert 0 <= i < len(t)
174 if e[
"group"] ==
"flight":
177 nearest =
min(range(len(t)), key=
lambda k: abs(t[k] - e[
"t_worst"]))
178 assert i == nearest, f
"{name}: {e['key']}"
180 assert t[i] == pytest.approx(e[
"t_worst"], abs=1e-9), f
"{name}: {e['key']}"
181 path = SERIES_BACKED.get(e[
"key"])
184 side, col = path.split(
".")
185 v = series[side][col][i]
186 expect = v - zero
if e[
"key"].startswith(
"tank_mawp")
else v
187 assert expect == pytest.approx(e[
"value"], rel=1e-12), f
"{name}: {e['key']} at index {i}"
194 """A small burn: two lead-in samples, ``n`` firing steps, dull numbers."""
195 t = [-0.05, 0.0] + [dt * (k + 1)
for k
in range(n)]
196 firing = [
False,
False] + [
True] * n
197 flat =
lambda v: [v] * len(t)
198 side =
lambda: {
"tank_psia": flat(578.0),
"stiffness": [0.0, 0.0] + [0.30] * n,
199 "liquid_kg": flat(5.0)}
200 s = {
"ox": side(),
"fuel": side()}
202 "depleted_side":
"oxidiser",
"burn_time_s": t[-1],
"copv_end_psia": 1500.0,
"copv_t0_psia": 4500.0,
203 "ox": {
"t0_psia": 578.0,
"min_psia": 570.0,
"stiffness_min": 0.30,
"peak_psia": 578.0,
204 "loaded_kg": 6.0,
"residual_kg": 0.0},
205 "fuel": {
"t0_psia": 578.0,
"min_psia": 572.0,
"stiffness_min": 0.30,
"peak_psia": 578.0,
206 "loaded_kg": 4.0,
"residual_kg": 0.5},
207 "failed_steps": 0,
"t0_settled":
True,
"card_outside_steps": 0,
"steps": len(t),
210 "series": {
"t": t,
"dt": [dt] * len(t),
"firing": firing,
"converged": [
True] * len(t),
211 "copv_psia": flat(1500.0), **s},
215 "provenance": {
"derived": {
"stiffness_band": {
"oxidiser": [0.2, 0.4],
"fuel": [0.2, 0.4]},
216 "roles": {
"oxidiser":
"OXT",
"fuel":
"FUT"},
217 "tank_mawp_psi": {
"OXT": 1000.0,
"FUT": 1000.0},
218 "ambient_pa": 101325.0}},
223 assert grade({}) == []
224 assert grade({
"series": {
"t": []}}) == []
229 r[
"diagnostics"] = {
"stability": {
230 "basis":
"drawing",
"t": r[
"series"][
"t"],
"margin": [
None,
None] + [1.15, 1.3, 1.31, 1.32, 1.33, 1.34, 1.35, 1.36],
231 "worst": {
"t": 0.05,
"index": 2,
"margin": 1.15,
"frequency_hz": 34.6},
232 "settled_min": {
"t": 0.10,
"index": 3,
"margin": 1.30,
"frequency_hz": 22.0},
"start_window_s": 0.1,
233 "other_basis": {
"basis":
"config",
"margin_min": 1.25,
"t": 0.1}}}
235 assert g[
"chug_margin"][
"value"] == 1.30
and g[
"chug_margin"][
"t_worst"] == 0.10
236 assert g[
"chug_margin"][
"index_worst"] == 3
238 assert g[
"chug_margin"][
"frequency_hz"] == 22.0
and g[
"chug_margin_start"][
"frequency_hz"] == 34.6
239 assert g[
"chug_margin"][
"series_ref"] ==
"diagnostics.stability.margin"
240 assert g[
"chug_margin"][
"grade"] ==
"ok" and "drawing feed basis" in g[
"chug_margin"][
"basis"]
241 assert "after the first 0.100 s" in g[
"chug_margin"][
"basis"]
243 r[
"diagnostics"][
"stability"][
"start_window_s"] = 0.0556569
244 assert "after the first 0.056 s" in _by_key(grade(r))[
"chug_margin"][
"basis"]
245 assert g[
"chug_margin_start"][
"grade"] ==
"info" and g[
"chug_margin_start"][
"value"] == 1.15
246 assert g[
"chug_margin_start"][
"index_worst"] == 2
247 assert g[
"chug_margin_other_basis"][
"grade"] ==
"info"
250@pytest.mark.parametrize("margin,expect", [(0.99, "bad"), (1.0,
"warn"), (1.19,
"warn"), (1.2,
"ok"), (1.5,
"ok")])
253 r[
"delivered"] = {
"t": r[
"series"][
"t"][2:],
"chug_margin": [margin] * 8,
254 "summary": {
"chug_margin_min": margin,
"chug_margin_min_t": 0.05}}
256 assert g[
"chug_margin"][
"grade"] == expect
257 assert g[
"chug_margin"][
"limit"] == 1.0
and g[
"chug_margin"][
"warn"] == 1.2
262 r[
"delivered"] = {
"t": r[
"series"][
"t"][2:],
"chug_margin": [1.3] * 8,
263 "summary": {
"chug_margin_min": 1.3,
"chug_margin_min_t": 0.05}}
266 class design_requirements:
267 min_stability_margin = 1.05
269 assert _by_key(grade(r, config=Cfg))[
"chug_margin"][
"warn"] == 1.2
270 Cfg.design_requirements.min_stability_margin = 1.5
271 g =
_by_key(grade(r, config=Cfg))[
"chug_margin"]
272 assert g[
"warn"] == 1.5
and g[
"grade"] ==
"warn"
273 assert "1.5" in g[
"hint"]
277 """A run graded on the drawing's feed (``options.chug_basis``, decided 2026-10-03) that carries no
278 stability block -- every set-point, Hardware, reconcile and sweep burn -- has only the replay's
279 margin, on EngineDesign's feed_system. It was graded as "the" chug margin: 1.21 'ok' on LE4 (3),
280 where the drawing basis grades 1.004 'warn'. It is now information, labelled with its basis. A run
281 saved before the decision (no options, or ``config``) grades it as it always did."""
283 r[
"delivered"] = {
"t": r[
"series"][
"t"][2:],
"chug_margin": [0.9] * 8,
284 "summary": {
"chug_margin_min": 0.9,
"chug_margin_min_t": 0.05}}
285 assert _by_key(grade(r))[
"chug_margin"][
"grade"] ==
"bad"
286 r[
"provenance"][
"derived"][
"options"] = {
"chug_basis":
"config"}
287 assert _by_key(grade(r))[
"chug_margin"][
"grade"] ==
"bad"
288 r[
"provenance"][
"derived"][
"options"] = {
"chug_basis":
"drawing"}
289 g =
_by_key(grade(r))[
"chug_margin"]
290 assert g[
"grade"] ==
"info" and g[
"value"] == 0.9
and g[
"feed_basis"] ==
"config"
291 assert "config basis" in g[
"label"]
and "not graded" in g[
"label"]
296 col = [1.4, 1.35, 1.3, 1.25, 1.21, 1.3, 1.4, 1.5]
297 r[
"delivered"] = {
"t": r[
"series"][
"t"][2:],
"chug_margin": col,
298 "summary": {
"chug_margin_min": 1.21,
"chug_margin_min_t": 0.25}}
299 g =
_by_key(grade(r))[
"chug_margin"]
300 assert g[
"index_worst"] == 2 + 4
and r[
"series"][
"t"][g[
"index_worst"]] == pytest.approx(0.25)
305 r[
"summary"][
"ox"][
"stiffness_min"] = 0.199
306 r[
"summary"][
"fuel"][
"stiffness_min"] = 0.401
308 assert g[
"stiffness_ox"][
"grade"] ==
"bad" and g[
"stiffness_fuel"][
"grade"] ==
"warn"
309 assert g[
"stiffness_ox"][
"band"] == [0.2, 0.4]
310 r[
"provenance"][
"derived"][
"stiffness_band"] = {
"oxidiser":
None,
"fuel":
None}
311 r[
"summary"][
"ox"][
"stiffness_min"] = 0.149
312 r[
"summary"][
"fuel"][
"stiffness_min"] = 0.9
314 assert g[
"stiffness_ox"][
"grade"] ==
"bad" and g[
"stiffness_ox"][
"limit"] == THRESHOLDS[
"stiffness_floor"][
"value"]
315 assert g[
"stiffness_fuel"][
"grade"] ==
"ok"
320 st = r[
"series"][
"ox"][
"stiffness"]
323 r[
"summary"][
"ox"][
"stiffness_min"] = 0.25
324 g =
_by_key(grade(r))[
"stiffness_ox"]
325 assert g[
"index_worst"] == 6
and g[
"t_worst"] == pytest.approx(0.25)
330 r[
"series"][
"fuel"][
"tank_psia"][5] = 619.0
331 r[
"summary"][
"fuel"][
"peak_psia"] = 619.0
332 ambient = 101325.0 / PSI
335 class design_requirements:
336 max_lox_tank_pressure_psi = 600.0
337 max_fuel_tank_pressure_psi = 600.0
339 g =
_by_key(grade(r, config=Cfg, meop_psi={
"fuel": 600.0}))
340 assert g[
"tank_mawp_fuel"][
"value"] == pytest.approx(619.0 - ambient)
341 assert g[
"tank_mawp_fuel"][
"index_worst"] == 5
344 assert {g[k][
"grade"]
for k
in (
"tank_mawp_fuel",
"tank_cap_fuel",
"tank_cap_ox",
"tank_meop_fuel")} == {
"info"}
345 assert "tank_meop_ox" not in g
346 r[
"provenance"][
"derived"][
"tank_mawp_psi"][
"FUT"] = 600.0
347 assert _by_key(grade(r))[
"tank_mawp_fuel"][
"grade"] ==
"info"
351 """``max_*_tank_pressure_psi`` default to 700 / 850 psi in the schema with no source. A design that
352 states its caps (LE4: 600 / 600) is graded against them; one that does not gets the default
353 reported as information, and says so, rather than amber against a number nobody chose."""
357 r[
"series"][
"ox"][
"tank_psia"][5] = 710.0
358 r[
"summary"][
"ox"][
"peak_psia"] = 710.0
361 design_requirements = DesignRequirementsConfig(max_lox_tank_pressure_psi=700.0,
362 max_fuel_tank_pressure_psi=850.0, min_stability_margin=1.05)
365 design_requirements = DesignRequirementsConfig()
367 stated =
_by_key(grade(r, config=Stated))[
"tank_cap_ox"]
368 assert stated[
"limit"] == 700.0
and stated[
"grade"] ==
"info" and stated[
"cap_source"] ==
"design_requirements"
369 unstated =
_by_key(grade(r, config=Unstated))[
"tank_cap_ox"]
370 assert unstated[
"limit"] == 700.0
and unstated[
"grade"] ==
"info" and unstated[
"cap_source"] ==
"schema default"
371 assert "schema's default" in unstated[
"basis"]
373 r[
"delivered"] = {
"t": r[
"series"][
"t"][2:],
"chug_margin": [1.3] * 8,
374 "summary": {
"chug_margin_min": 1.3,
"chug_margin_min_t": 0.05}}
375 assert "does not state" in _by_key(grade(r, config=Unstated))[
"chug_margin"][
"hint"]
376 assert "does not state" not in _by_key(grade(r, config=Stated))[
"chug_margin"][
"hint"]
381 r[
"summary"][
"copv_end_psia"] = 578.0 + 99.0
382 r[
"summary"][
"fuel"][
"min_psia"] = 578.0 - 31.0
383 r[
"series"][
"fuel"][
"tank_psia"][4] = 578.0 - 31.0
384 r[
"summary"][
"fuel"][
"residual_kg"] = 0.29
386 assert g[
"bottle_margin"][
"grade"] ==
"bad" and g[
"bottle_margin"][
"value"] == pytest.approx(99.0)
387 assert g[
"tank_sag"][
"grade"] ==
"warn" and g[
"tank_sag"][
"index_worst"] == 4
388 assert g[
"depletion_tie"][
"grade"] ==
"info" and g[
"residual"][
"grade"] ==
"warn"
389 assert "LOX runs dry first" in g[
"depletion_tie"][
"detail"]
390 r[
"summary"][
"copv_end_psia"] = 578.0 + 201.0
391 r[
"summary"][
"fuel"][
"residual_kg"] = 0.2
393 assert g[
"bottle_margin"][
"grade"] ==
"ok" and g[
"residual"][
"grade"] ==
"ok"
398 r[
"summary"][
"depleted_side"] =
""
400 assert g[
"depletion"][
"grade"] ==
"warn" and "depletion_tie" not in g
405 r[
"tripped"] = {
"vessel":
"FUT",
"t": 0.2,
"p_psia": 764.7,
"mawp_psia": 764.7}
406 r[
"converged"] =
False
408 assert g[
"vessel_trip"][
"grade"] ==
"bad" and g[
"vessel_trip"][
"index_worst"] == 5
409 assert g[
"unsettled"][
"grade"] ==
"warn"
410 assert any(
"Vessel trip" in b
for b
in breaks(list(g.values())))
415 base = {e[
"key"]
for e
in grade(r)}
416 assert not any(k.startswith((
"saturation_",
"cavitation_",
"water_hammer_",
"regulator_",
"conservation_"))
418 n = len(r[
"series"][
"t"])
420 "saturation": {
"nodes": [{
"id":
"OXT.out",
"label":
"LOX tank outlet",
"side":
"ox",
421 "margin_psi": [
None, 30.0] + [20.0, 9.0, 12.0, 15.0, 16.0, 17.0, 18.0, 19.0],
422 "min_psi": 9.0,
"t_min": 0.10}]},
423 "cavitation": {
"ox": {
"margin": [
None,
None, 2.0, 1.9, 1.8, 1.1, 1.5, 1.6, 1.7, 1.8],
"min_margin": 1.1,
424 "min_K": 1.76,
"K_incipient": 1.6,
"t_min": 0.2,
"flip_risk":
False},
425 "fuel": {
"margin": [
None] * n,
"min_margin": 0.9,
"min_K": 1.4,
"K_incipient": 1.6,
426 "t_min": 0.2,
"flip_risk":
True}},
427 "water_hammer": [{
"line":
"l_ox1",
"side":
"ox",
"closure_s": 0.05,
"peak_psia": 2900.0,
"rating_psia": 1014.7,
428 "ok":
False,
"opening": {
"peak_psia": 900.0}}],
429 "regulator": {
"t": r[
"series"][
"t"],
"use_frac": [0.1] * (n - 1) + [0.85],
"wide_open": [
False] * n},
430 "vv": {
"mass": {
"ox": {
"error_pct": 1e-7},
"fuel": {
"error_pct": 0.5}},
431 "pressurant": {
"error_pct": 3.0},
"energy": {
"error_pct": 12.0,
"basis":
"x"}},
434 assert g[
"saturation_OXT.out"][
"grade"] ==
"warn" and g[
"saturation_OXT.out"][
"index_worst"] == 3
435 assert g[
"cavitation_ox"][
"grade"] ==
"warn" and g[
"cavitation_ox"][
"index_worst"] == 5
436 assert g[
"cavitation_fuel"][
"grade"] ==
"bad" and g[
"cavitation_fuel"][
"index_worst"] == 5
439 assert g[
"water_hammer_l_ox1"][
"grade"] ==
"info" and g[
"water_hammer_l_ox1"][
"value"] == 900.0
440 assert g[
"regulator_wide_open"][
"grade"] ==
"warn" and g[
"regulator_wide_open"][
"index_worst"] == n - 1
441 assert g[
"conservation_mass_ox"][
"grade"] ==
"ok"
442 assert g[
"conservation_mass_fuel"][
"grade"] ==
"warn"
443 assert g[
"conservation_pressurant"][
"grade"] ==
"bad"
444 assert g[
"conservation_energy"][
"grade"] ==
"info"
445 r[
"diagnostics"][
"regulator"][
"wide_open"][-2] =
True
446 assert _by_key(grade(r))[
"regulator_wide_open"][
"grade"] ==
"bad"
449@pytest.mark.parametrize("ratio,expect", [(0.99, "bad"), (1.0,
"warn"), (1.19,
"warn"), (1.2,
"ok")])
452 r[
"diagnostics"] = {
"cavitation": {
"ox": {
"min_margin": ratio,
"t_min": 0.1,
"flip_risk":
False}}}
453 assert _by_key(grade(r))[
"cavitation_ox"][
"grade"] == expect
458 node = {
"id":
"MVO.in",
"side":
"ox",
"min_psi": 40.0,
"t_min": 0.1,
"min_static_psi": 20.0,
"t_min_static": 0.15}
459 r[
"diagnostics"] = {
"saturation": {
"nodes": [node]}}
460 g =
_by_key(grade(r))[
"saturation_MVO.in"]
462 assert g[
"value"] == 20.0
and g[
"t_worst"] == 0.15
and g[
"grade"] ==
"warn" and g[
"warn"] == 25.0
463 r[
"diagnostics"][
"saturation"][
"flag_margin_psi"] = 15.0
464 assert _by_key(grade(r))[
"saturation_MVO.in"][
"grade"] ==
"ok"
469 r[
"diagnostics"] = {
"hardware": {
"t": [0.05, 0.2, 0.4],
"separation": {
470 "ratio": [1.1, 1.03, 1.2],
"summerfield": [
False] * 3,
"schmucker": [
False,
False,
False]}}}
471 g =
_by_key(grade(r))[
"separation"]
472 assert g[
"value"] == 1.03
and g[
"t_worst"] == 0.2
and g[
"index_worst"] == 5
and g[
"grade"] ==
"ok"
473 r[
"diagnostics"][
"hardware"][
"separation"][
"schmucker"][1] =
True
474 assert _by_key(grade(r))[
"separation"][
"grade"] ==
"warn"
479 r[
"delivered"] = {
"t": r[
"series"][
"t"][2:],
"p_exit_psia": [15.0, 14.0, 6.0, 15.0, 15.0, 15.0, 15.0, 15.0],
480 "ambient_psia": [14.0] * 8,
"summary": {}}
481 g =
_by_key(grade(r))[
"separation"]
482 assert g[
"value"] == pytest.approx(6.0 / 14.0)
and g[
"grade"] ==
"ok" and g[
"index_worst"] == 4
483 r[
"delivered"][
"p_exit_psia"][2] = 5.5
484 assert _by_key(grade(r))[
"separation"][
"grade"] ==
"bad"
488 """A design that states no rail-exit or static-margin requirement is graded against FAR-OUT's
489 (2024-25 rules 5.1.4 and 5.1.5), and says so: a 21.7 m/s rail exit used to drop out of the
490 limits altogether and leave the Overview reading "Within limits" (D-59)."""
492 r[
"flight"] = {
"ok":
True,
"apogee_agl_m": 3000.0,
"rail_exit_velocity_m_s": 21.7,
"rail_exit_time_s": 0.3,
493 "stability": {
"liftoff_static_margin_cal": 1.4,
"min_static_margin_cal": 1.3,
494 "min_static_margin_t": 0.2,
"max_q_pa": 36800.0,
"max_q_t": 0.4}}
496 assert g[
"rail_exit"][
"grade"] ==
"bad" and g[
"rail_exit"][
"limit"] == pytest.approx(85 * 0.3048)
497 assert "FAR-OUT" in g[
"rail_exit"][
"hint"]
and "5.1.4" in g[
"rail_exit"][
"hint"]
498 assert g[
"static_margin_min"][
"grade"] ==
"bad" and g[
"static_margin_min"][
"limit"] == 1.5
499 assert "5.1.5" in g[
"static_margin_min"][
"hint"]
500 assert g[
"max_q"][
"grade"] ==
"info" and g[
"max_q"][
"index_worst"] == 9
501 fast = copy.deepcopy(r)
502 fast[
"flight"][
"rail_exit_velocity_m_s"] = 26.0
503 fast[
"flight"][
"stability"].update(liftoff_static_margin_cal=7.7, min_static_margin_cal=7.3)
505 assert g[
"rail_exit"][
"grade"] ==
"ok" and g[
"static_margin_min"][
"grade"] ==
"ok"
508 class design_requirements:
509 min_rail_exit_velocity_m_s = 25.91
510 min_static_margin_cal = 1.5
512 g =
_by_key(grade(r, config=Cfg))
513 assert g[
"rail_exit"][
"grade"] ==
"bad" and g[
"rail_exit"][
"t_worst"] == 0.3
514 assert g[
"static_margin_min"][
"grade"] ==
"bad"
517 class design_requirements:
518 min_rail_exit_velocity_m_s = 20.0
519 min_static_margin_cal = 1.0
522 g =
_by_key(grade(r, config=Own))
523 assert g[
"rail_exit"][
"grade"] ==
"ok" and g[
"rail_exit"][
"limit"] == 20.0
and "FAR-OUT" not in g[
"rail_exit"][
"hint"]
524 assert g[
"static_margin_min"][
"grade"] ==
"ok" and g[
"static_margin_min"][
"limit"] == 1.0
529 r[
"provenance"][
"reproduce"] = {
"design_yaml":
"design_requirements:\n max_fuel_tank_pressure_psi: 650\n"}
530 assert requirements(r)[
"max_fuel_tank_pressure_psi"] == 650
531 assert _by_key(grade(r))[
"tank_cap_fuel"][
"limit"] == 650.0
536 r[
"diagnostics"] = {
"saturation": {
"nodes": [{
"id":
"x",
"margin_psi":
"not a list",
"min_psi": object()}]}}
537 keys = {e[
"key"]
for e
in grade(copy.deepcopy(r))}
538 assert "bottle_margin" in keys
and "stiffness_ox" in keys
543 want = {
"key",
"label",
"group",
"value",
"unit",
"limit",
"warn",
"direction",
"grade",
"t_worst",
544 "index_worst",
"series_ref",
"basis",
"hint"}
545 groups = {
"stability",
"injector",
"tanks",
"pressurant",
"propellant",
"flight",
"hardware",
"model"}
547 assert want <= set(e), e[
"key"]
548 assert e[
"group"]
in groups
and e[
"grade"]
in (
"ok",
"warn",
"bad",
"info")
549 assert e[
"direction"]
in (
"min",
"max")
and e[
"hint"]
test_tank_peak_against_mawp_meop_and_the_design_cap()
test_diagnostics_blocks_are_graded_when_present_and_skipped_when_absent()
test_the_chug_amber_edge_follows_a_requirement_above_one_point_two_but_never_below_it()
test_separation_from_the_hardware_block_and_schmucker_as_amber()
test_chug_margin_is_red_below_one_and_amber_below_one_point_two(margin, expect)
test_the_delivered_chug_minimum_maps_onto_the_series_by_firing_step()
test_a_burn_with_nothing_to_grade_grades_nothing()
test_a_drawing_basis_run_without_its_stability_block_does_not_grade_the_config_margin()
test_stiffness_worst_time_ignores_the_first_fifth_of_a_second()
test_chug_from_the_stability_diagnostics_grades_the_settled_minimum_and_reports_the_start()
test_a_malformed_block_costs_only_its_own_grades()
test_flight_limits_fall_back_to_the_competition_rules()
test_a_vessel_trip_is_red_at_its_instant()
test_bottle_sag_and_depletion_edges()
test_cavitation_is_red_once_the_orifice_cavitates_and_amber_near_it(ratio, expect)
test_the_worst_index_points_at_the_graded_value(name, result)
test_a_burn_that_never_ran_dry_says_so()
test_a_design_cap_the_design_never_stated_is_reported_not_graded()
test_a_stated_flag_margin_moves_the_saturation_amber_edge()
test_nozzle_separation_from_the_delivered_exit_pressure()
test_the_reference_run_matches_the_audits_hand_worked_worst_times()
test_stiffness_band_edges_and_the_rule_of_thumb_without_a_band()
test_every_entry_carries_the_contract_keys()
test_requirements_are_read_from_the_recorded_yaml_without_a_config()
Everything a case may vary.