STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_layerx_reconcile.py
Go to the documentation of this file.
1"""The injector reconciler: holes resized so the design point holds through the drawing's feed.
2
3Checked against things the reconciler does not compute itself:
4 * the orifice law. At restored flows, m = Cd A sqrt(2 rho dp) on each side, so the new area over
5 the old is (Cd_old / Cd_new) sqrt(dp_old / dp_new), with Cd and dp from EngineDesign's solve;
6 * the design's own feed asks for no change (the opt-in rule: nothing to do, nothing done);
7 * drill diameters are ASME B94.11M's, in inches;
8 * on the 6.8 kN engine and the stand drawing, the burn after reconciling sees its injector at the
9 pressure Forward mode sized it for (the feed fit's manifold gap closes), and the burn's mean
10 thrust rises toward the design point it fell short of.
11"""
12
13from __future__ import annotations
14
15import copy
16import math
17from pathlib import Path
18
19import pytest
20
21
22#: The study drawings' lockup these tests were written at [psia] (scripts/layerx_baseline.py).
23STUDY_LOCKUP_PSIA = 564.7
24
25from engine.core.runner import PintleEngineRunner
26from engine.layerx import reconcile as R
27from engine.pipeline.io import load_config
28
29CFG = Path(__file__).parent.parent / "configs" / "ethalox_6800N.yaml"
30PSI = 6894.757293168361
31P_TANK = 578.0 * PSI
32AMBIENT = 94070.0
33SIDES = ("oxidizer", "fuel")
34
35
36@pytest.fixture(autouse=True)
37def _python_physics(monkeypatch):
38 monkeypatch.setenv("ED_ACCEL", "off")
39
40
41@pytest.fixture(scope="module")
42def config():
43 return load_config(str(CFG))
44
45
46def _feed(config, K0):
47 cfg = copy.deepcopy(config)
48 for side, k in zip(SIDES, K0):
49 cfg.feed_system[side] = cfg.feed_system[side].model_copy(update={"K0": k})
50 return cfg
51
52
53def _lengths(config):
54 return {s: R._passage_length(config, s) for s in SIDES}
55
56
58 design = R._forward(config, P_TANK, AMBIENT)
59 d, point, _ = R._solve_holes(config, _lengths(config), (design["thrust_N"], design["of"]), P_TANK, AMBIENT)
60 for side in SIDES:
61 assert d[side] == pytest.approx(getattr(config.injector.geometry, side).d_jet, rel=1e-5)
62
63
65 lengths = _lengths(config)
66 design = R._forward(config, P_TANK, AMBIENT)
67 lossy = _feed(config, (1.27, 2.86)) # the stand drawing's fitted K0, 2026-10-01
68 before = R._forward(lossy, P_TANK, AMBIENT)
69 assert before["thrust_N"] < design["thrust_N"] - 100.0
70 d, point, _ = R._solve_holes(lossy, lengths, (design["thrust_N"], design["of"]), P_TANK, AMBIENT)
71 assert point["thrust_N"] == pytest.approx(design["thrust_N"], rel=1e-3)
72 assert point["of"] == pytest.approx(design["of"], rel=1e-3)
73
74 old = PintleEngineRunner(copy.deepcopy(config)).evaluate(P_TANK, P_TANK, P_ambient=AMBIENT, silent=True)
75 new = PintleEngineRunner(R._with_holes(lossy, d, lengths)).evaluate(P_TANK, P_TANK, P_ambient=AMBIENT, silent=True)
76 for side, k in (("oxidizer", "O"), ("fuel", "F")):
77 assert new[f"mdot_{k}"] == pytest.approx(old[f"mdot_{k}"], rel=2e-3)
78 dp_old = old["injector_pressure"][f"delta_p_injector_{k}"]
79 dp_new = new["injector_pressure"][f"delta_p_injector_{k}"]
80 expected = (new[f"mdot_{k}"] / old[f"mdot_{k}"]) * (old[f"Cd_{k}"] / new[f"Cd_{k}"]) * math.sqrt(dp_old / dp_new)
81 got = (d[side] / getattr(config.injector.geometry, side).d_jet) ** 2
82 assert got == pytest.approx(expected, rel=5e-3), side
83 # the passage keeps its drilled length; only its L/d moves
84 assert new_lod(lossy, d, lengths, side) * d[side] == pytest.approx(lengths[side], rel=1e-9)
85
86
87def new_lod(config, d, lengths, side):
88 return R._with_holes(config, d, lengths).discharge[side].orifice_l_over_d
89
90
92 lengths = _lengths(config)
93 design = R._forward(config, P_TANK, AMBIENT)
94 light = _feed(config, (0.2, 1.0))
95 d, _, _ = R._solve_holes(light, lengths, (design["thrust_N"], design["of"]), P_TANK, AMBIENT)
96 rows = R._changes(config, R._with_holes(light, d, lengths), lengths)
97 holes = [r for r in rows if r["item"].endswith("orifice diameter")]
98 assert all(r["to"] < r["from"] for r in holes)
99 assert all(r["fabrication"].startswith("new plate") for r in holes)
100
101
102@pytest.mark.parametrize("no, inch", [(51, 0.0670), (53, 0.0595), (60, 0.0400)])
104 out = R.drills(inch * 25.4)
105 assert out["number"]["drill"] == f"#{no}"
106 assert out["number"]["d_mm"] == pytest.approx(inch * 25.4)
107 assert out["number"]["area_error"] == pytest.approx(0.0, abs=1e-12)
108
109
110# ------------------------------------------------------------------ the whole loop, on the drawing
111
112
113feedtwin = pytest.importorskip("feedtwin", reason="lib/feedtwin is not installed")
114from engine.layerx import DrawingStore, LayerXSettings # noqa: E402
115from engine.layerx.sources import shipped_drawings_dir # noqa: E402
116
117GN2 = "copv_study_gn2"
118# The GN2 drawing is a nitrogen-over-LOX hot fire, refused since 2026-10-03 unless acknowledged
119# (engine/layerx/prepare.py gn2_on_lox); these tests study its burn, so their settings acknowledge it.
120
121
122@pytest.mark.skipif(not (shipped_drawings_dir() / f"{GN2}.json").is_file(),
123 reason="feed-twin's shipped drawings are not next to this checkout")
125 drawing = {d.name: d for d in DrawingStore(None).list()}[GN2]
126 settings = LayerXSettings(drawing_id=drawing.id, tank_pressure_psia=STUDY_LOCKUP_PSIA, ack_gn2_condensation=True, replay=False)
127 res = R.run_reconcile(config, drawing, settings, [], R.ReconcileRequest(max_passes=4))
128 assert res["converged"]
129 target = res["design"]["thrust_N"]
130 # Checked on the burn itself, not on a fit or on Forward mode: the twin's mean thrust (no replay
131 # here, so the throat is as built) against the design point. The holes are sized to the
132 # burn-mean feed, so the burn mean is what they must hit.
133 assert res["before_burn"]["mean_thrust_N"] < 0.985 * target # the drawn design under-feeds
134 assert res["after_burn"]["mean_thrust_N"] == pytest.approx(target, rel=3e-3)
135 assert res["after"]["thrust_N"] == pytest.approx(target, rel=1e-3)
136 assert abs(res["after_burn"]["of_mean"] - res["design"]["of"]) < abs(res["before_burn"]["of_mean"] - res["design"]["of"])
137 upd = res["design_update"]
138 assert set(upd) >= {"injector", "feed_system", "discharge"}
139 assert upd["feed_system"]["oxidizer"]["derived_from"]["by"] == "Layer X injector reconcile"
test_the_designs_own_feed_needs_no_change(config)
test_a_lighter_feed_shrinks_the_holes_and_says_it_needs_a_new_plate(config)
new_lod(config, d, lengths, side)
test_a_lossier_feed_opens_the_holes_by_the_orifice_law(config)
test_reconciling_closes_the_gap_on_the_stand(config)