STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_layerx_feedfit.py
Go to the documentation of this file.
1"""The drawing's feed written into the design the injector is sized with.
2
3EngineDesign sizes the injector at lockup less one K per side. The drawing's tank does not hold
4lockup while firing (regulator droop, the press line), and its lines are not the design's K. The
5fit expresses both in the design's own form; checked against the twin, not against itself:
6
7* the design as it stands misses the twin's manifold pressure by more than 7 psi on each side;
8* with the fitted K0, EngineDesign's own forward solve at lockup lands on the twin's burn: its
9 flows within 1.5 %, its O/F within 0.5 %;
10* writing the fit into the design does not move Layer X's burn, which never used the design's K0.
11"""
12
13from __future__ import annotations
14
15from pathlib import Path
16
17import pytest
18
19
20#: The study drawings' lockup these tests were written at [psia] (scripts/layerx_baseline.py).
21STUDY_LOCKUP_PSIA = 564.7
22
23pytest.importorskip("feedtwin", reason="lib/feedtwin is not installed")
24
25from engine.layerx import DrawingStore, LayerXSettings, prepare, run_prepared # noqa: E402
26from engine.layerx.feedfit import design_update # noqa: E402
27from engine.layerx.sources import shipped_drawings_dir # noqa: E402
28from engine.pipeline.io import load_config # noqa: E402
29
30FIXTURE = Path(__file__).parent / "fixtures" / "ethalox_6500N_doublet_cad_2026-09-28.yaml"
31GN2 = "copv_study_gn2"
32# The GN2 drawing is a nitrogen-over-LOX hot fire, refused since 2026-10-03 unless acknowledged
33# (engine/layerx/prepare.py gn2_on_lox); these tests study its burn, so their settings acknowledge it.
34PSI = 6894.757293168361
35pytestmark = pytest.mark.skipif(not (shipped_drawings_dir() / f"{GN2}.json").is_file(),
36 reason="feed-twin's shipped drawings are not next to this checkout")
37
38
39@pytest.fixture(scope="module")
40def config():
41 return load_config(str(FIXTURE))
42
43
44@pytest.fixture(scope="module")
45def drawing():
46 return {d.name: d for d in DrawingStore(None).list()}[GN2]
47
48
49@pytest.fixture(scope="module")
50def burned(config, drawing):
51 # No replay: the twin's engine is the card at the as-built throat, the same engine EngineDesign's
52 # forward solve runs, so what is left between them is the feed.
53 prep = prepare(config, None, drawing, LayerXSettings(drawing_id=drawing.id, tank_pressure_psia=STUDY_LOCKUP_PSIA, ack_gn2_condensation=True, replay=False))
54 return run_prepared(prep, replay=False, config=config)
55
56
57def _with(config, update):
58 from engine.pipeline.config_schemas import PintleEngineConfig
59
60 raw = config.model_dump()
61 for side, u in update["feed_system"].items():
62 raw["feed_system"][side].update(u)
63 return PintleEngineConfig(**raw)
64
65
67 fit = burned["feed_fit"]
68 assert fit["available"]
69 for side in ("oxidizer", "fuel"):
70 s = fit["sides"][side]
71 # 9.6-15 psi on the 6.5 kN and 6.8 kN designs: ~7-11 % of a ~135 psi injector drop.
72 assert s["manifold_gap_psi"] < -7.0, (side, s["manifold_gap_psi"])
73 assert s["supply_deficit_psi"] > 5.0 # the tank does not hold lockup while firing
74 assert s["K_line"] > 0.0 and s["K_supply"] > 0.0
75 assert s["K0"] == pytest.approx(s["K_line"] + s["K_supply"])
76 # By construction the fitted K0 lands the design on the twin's mean manifold.
77 assert s["manifold_fitted_psia"] == pytest.approx(s["manifold_psia"], abs=0.05)
78
79
81 from engine.core.runner import PintleEngineRunner
82
83 fit = burned["feed_fit"]
84 lockup = fit["sides"]["oxidizer"]["lockup_psia"] * PSI
85
86 def solve(cfg):
87 return PintleEngineRunner(cfg).evaluate(lockup, lockup, silent=True)
88
89 today, fitted = solve(config), solve(_with(config, fit["design_update"]))
90 twin = {"mdot_O": fit["sides"]["oxidizer"]["mdot_kg_s"], "mdot_F": fit["sides"]["fuel"]["mdot_kg_s"]}
91 twin_of = twin["mdot_O"] / twin["mdot_F"]
92 for key in ("mdot_O", "mdot_F"):
93 assert fitted[key] == pytest.approx(twin[key], rel=0.015), key
94 # and the design as it stands is further off than the fit
95 assert abs(today[key] / twin[key] - 1.0) > abs(fitted[key] / twin[key] - 1.0), key
96 assert fitted["MR"] == pytest.approx(twin_of, rel=0.005)
97
98
100 upd = burned["feed_fit"]["design_update"]["feed_system"]
101 for side in ("oxidizer", "fuel"):
102 u = upd[side]
103 assert u["roughness_m"] is None and u["fittings"] == [] and u["phi_type"] == "none" and u["K1"] == 0.0
104 rec = u["derived_from"]
105 assert rec["by"] == "Layer X feed fit" and rec["drawing"] == GN2 and len(rec["drawing_sha256"]) == 64
106 assert rec["K_line"] + rec["K_supply"] == pytest.approx(u["K0"], abs=2e-4)
107 assert rec["condition"] == "on the pad"
108
109
111 """Layer X burns through the drawing's lines and samples EngineDesign with its feed zeroed, so
112 the design's K0 is not in its burn: written in, the burn is the same burn."""
113 upd = design_update(burned["feed_fit"], run_id="t")
114 prep = prepare(_with(config, upd), None, drawing, LayerXSettings(drawing_id=drawing.id, tank_pressure_psia=STUDY_LOCKUP_PSIA, ack_gn2_condensation=True, replay=False))
115 again = run_prepared(prep, replay=False)
116 assert again["summary"]["total_impulse_Ns"] == pytest.approx(burned["summary"]["total_impulse_Ns"], rel=1e-9)
117
118
120 """LE4 (6), flown on helium: its LOX side is drawn with no line (tank, OM-R, injector) and the
121 regulator's outlet climbs as the bottle empties, so the fit came out at K0 -0.448, and the Record
122 page offered to write it into the design (D-58). A loss coefficient at or below zero is refused,
123 in words, side by side; a positive one is written as before."""
124 from engine.layerx.feedfit import _refusal
125
126 side = {"label": "LOX", "K0": -0.448, "K_line": -0.0118, "K_supply": -0.4359, "line_loss_psi": -0.33,
127 "line_loss_design_psi": 16.15, "tank_firing_psia": 550.77, "lockup_psia": 539.82, "K0_design": 0.643,
128 "mdot_kg_s": 1.86, "supply_deficit_psi": -10.95, "design_path": "K0"}
129 why = _refusal("LOX", side["K0"], side["K_line"], side["K_supply"], side["line_loss_psi"],
130 side["line_loss_design_psi"], side["tank_firing_psia"], side["lockup_psia"])
131 assert why and "likely undrawn" in why and "550.8 against 539.8 psia" in why
132 with pytest.raises(ValueError, match="not a loss"):
133 design_update({"sides": {"oxidizer": side}, "drawing": {"name": "LE4 (6)", "sha256": "a" * 64}}, run_id="t")
134 ok = dict(side, K0=0.31, K_line=0.29, K_supply=0.02, line_loss_psi=7.0, tank_firing_psia=538.0)
135 assert _refusal("LOX", ok["K0"], ok["K_line"], ok["K_supply"], ok["line_loss_psi"], 16.15, 538.0, 539.82) is None
136 upd = design_update({"sides": {"oxidizer": ok}, "drawing": {"name": "LE4 (6)", "sha256": "a" * 64}}, run_id="t")
137 assert upd["feed_system"]["oxidizer"]["K0"] == 0.31
138
139
141 assert burned["feed_fit"]["writable"] is True and burned["feed_fit"]["write_refused"] is None
test_writing_the_fit_does_not_move_layer_x(burned, config, drawing)
burned(config, drawing)
test_a_k0_that_is_not_a_loss_is_never_offered_for_the_design()
test_with_the_fit_enginedesign_lands_on_the_burn(burned, config)
test_the_update_is_the_lumped_path_with_its_record(burned)
test_the_design_misses_the_drawings_manifold(burned)