1"""Layer X holds each vessel to the pressure the feed twin trips the stand at.
3The twin trips a vessel when its absolute pressure is over ``_trip_limit``: the drawing's burst
4pressure over ``Setup.burst_safety_factor`` (across the wall) when one is drawn, else its MAWP --
5both read as the drawing reads them, a bare psi being gauge, so ``.si`` is already absolute.
7Before 2026-11-02 Layer X read the MAWP as a pressure *across* the wall: preflight passed a lockup
8up to one atmosphere plus the site's over the trip (``target - ambient > MAWP.si``), the optimiser
9and set point capped the lockup at ``MAWP.si + ambient``, the water-hammer rating added another
10atmosphere, and the limits graded ``peak - ambient`` against the absolute figure. And it read no
11burst pressure at all, which is how LE4 (3) rates every vessel: preflight said "nothing checks
12those vessels" while the twin tripped its tanks at 764.7 psia.
14Worked by hand: copv_study_gn2's tanks are drawn MAWP 1000 psi -> 1014.70 psia. LE4 (3)-style burst
151500 psi over a factor of 2 -> (1500 / 2) psig = 764.70 psia; 15000 psi -> 7514.70 psia.
18from __future__
import annotations
21from dataclasses
import replace
22from pathlib
import Path
23from types
import SimpleNamespace
27pytest.importorskip(
"feedtwin", reason=
"lib/feedtwin is not installed")
29from engine.layerx import DrawingStore, LayerXSettings, prepare
36PSI = 6894.757293168361
37ATM_PSI = 101325.0 / PSI
39CFG = Path(__file__).resolve().parent /
"fixtures" /
"ethalox_6500N_doublet_cad_2026-09-28.yaml"
41needs_drawing = pytest.mark.skipif(
not (shipped_drawings_dir() / f
"{GN2}.json").is_file(),
42 reason=
"feed-twin's shipped drawings are not next to this checkout")
45@pytest.fixture(scope="module")
47 return load_config(str(CFG))
50@pytest.fixture(scope="module")
52 return {d.name: d
for d
in DrawingStore(
None).list()}[GN2]
55def _prep(config, drawing, **settings):
57 return prepare(config,
None, drawing,
LayerXSettings(drawing_id=drawing.id, replay=
False, engine_model=
"native",
61def _burst_rated(drawing, tank_psi=1500.0, bottle_psi=15000.0, relief_psi=None):
62 """``drawing`` rated as LE4 (3) is: burst pressures, no MAWP; optionally a relief on the bottle's manifold."""
63 payload = copy.deepcopy(drawing.payload)
64 for node
in payload[
"nodes"]:
65 params = (node.get(
"data")
or {}).get(
"params")
or {}
66 if node[
"id"]
in (
"OXT",
"FUT",
"KB1"):
67 params.pop(
"MAWP",
None)
68 params[
"burst_pressure"] = {
"value": bottle_psi
if node[
"id"] ==
"KB1" else tank_psi,
"unit":
"psi",
69 "source":
"estimated"}
70 if relief_psi
is not None:
71 payload[
"nodes"].append({
"id":
"RV9",
"position": {
"x": 150,
"y": 380},
"data": {
72 "componentType":
"RV",
"label":
"COPV-RV",
"fluid":
"nitrogen",
"options": {
"flowCoefficient":
"Cd"},
73 "params": {
"set_pressure": {
"value": relief_psi,
"unit":
"psi",
"source":
"estimated"}}}})
74 payload[
"edges"].append({
"id":
"l_rv9",
"source":
"MF1",
"target":
"RV9",
"targetHandle":
"r",
75 "data": {
"lineType":
"pipe",
"params": {}}})
76 return replace(drawing, id=drawing.id +
"-burst", payload=payload)
80 return {c.key: c.detail
for c
in prep.checks
if c.status ==
"fail"}
85 """1000 psi MAWP trips at 1014.70 psia. 1012 passes; 1022 -- under MAWP.si plus the site's 13.64
86 psia, which the old check allowed -- fails, naming the trip. The session holds the same limit."""
89 ok =
_prep(config, drawing, tank_pressure_psia=1012.0)
90 assert not any(k.startswith(
"mawp_")
for k
in _fails(ok))
91 assert ok.derived[
"tank_mawp_psi"] == pytest.approx({
"OXT": 1000.0 + ATM_PSI,
"FUT": 1000.0 + ATM_PSI})
92 session = open_session(ok.assembler(), ok.machine, setup=ok.setup)
93 assert {k: s.mawp / PSI
for k, s
in session.tanks.items()} == pytest.approx(ok.derived[
"tank_mawp_psi"])
95 over =
_prep(config, drawing, tank_pressure_psia=1022.0)
97 assert {
"mawp_OXT",
"mawp_FUT"} <= set(fails)
98 assert "1014.7 psia (MAWP 1000 psig" in fails[
"mawp_OXT"]
106 prep =
_prep(config, burst, tank_pressure_psia=600.0)
107 assert prep.ok,
_fails(prep)
108 trip = 750.0 + ATM_PSI
109 assert prep.derived[
"tank_mawp_psi"] == pytest.approx({
"OXT": trip,
"FUT": trip})
110 assert prep.derived[
"copv_mawp_psi"] == pytest.approx(7500.0 + ATM_PSI)
111 assert prep.derived[
"vessel_ratings"][
"OXT"][
"basis"] ==
"burst"
112 ratings = next(c
for c
in prep.checks
if c.key ==
"ratings")
113 assert "nothing checks" not in ratings.detail
114 assert "TK-LOX trips at 764.7 psia (burst 1500 psig over a safety factor of 2, estimated)" in ratings.detail
115 session = open_session(prep.assembler(), prep.machine, setup=prep.setup)
116 assert {k: s.mawp / PSI
for k, s
in session.tanks.items()} == pytest.approx(prep.derived[
"tank_mawp_psi"])
117 assert [b.mawp / PSI
for b
in session.bottles.values()] == pytest.approx([prep.derived[
"copv_mawp_psi"]])
119 assert {
"mawp_OXT",
"mawp_FUT"} <= set(
_fails(
_prep(config, burst, tank_pressure_psia=770.0)))
120 assert "mawp_KB1" in _fails(
_prep(config, burst, tank_pressure_psia=600.0, copv_pressure_psia=7600.0))
125 """A relief on the bottle's manifold at 4000 psi lifts at 4014.7 psia: a 4500 psia fill vents in
126 the settle, so preflight warns; a 3900 psia one does not."""
128 over =
_prep(config, burst, tank_pressure_psia=600.0, copv_pressure_psia=4500.0)
129 relief = next((c
for c
in over.checks
if c.key ==
"relief"),
None)
130 assert relief
is not None and relief.status ==
"warn"
131 assert "COPV-RV's 4000 psi set pressure on COPV-GN2 (4014.7 psia)" in relief.detail
132 under =
_prep(config, burst, tank_pressure_psia=600.0, copv_pressure_psia=3900.0)
133 assert not any(c.key ==
"relief" for c
in under.checks)
138 """The 4500 psia fill vents through the 4000 psi relief in the settle (to ~3895 psia) and the tanks
139 follow the regulator's supply effect: T-0 is not the burn asked for. The run said only "T-0 did not
140 settle" and stood as converged; it now says what it started from and is not converged."""
144 prep =
_prep(config, burst, tank_pressure_psia=600.0, copv_pressure_psia=4500.0, horizon_s=1.0)
145 result = run_prepared(prep, replay=
False)
146 assert result[
"summary"][
"t0_settled"]
is False
147 assert result[
"converged"]
is False
148 event = next(e
for e
in result[
"events"]
if e[
"label"] ==
"T-0 did not settle")
149 assert "Asked for a 600.0 psia lockup from a 4500 psia bottle; T-0 is LOX" in event[
"detail"]
150 assert result[
"summary"][
"copv_t0_psia"] < 4100.0
152 t0 = next(e
for e
in result[
"events"]
if e[
"kind"] ==
"t0")[
"t"]
153 assert t0 == 0.0
and repr(t0) ==
"0.0"
154 assert 0.0
in result[
"series"][
"t"]
157def _burn(peak_ox: float, peak_fuel: float):
158 """Two lead-in samples and four firing steps, flat but for each tank's peak."""
159 t = [-0.05, 0.0, 0.05, 0.10, 0.15, 0.20]
160 tank =
lambda peak: [600.0, 600.0, 600.0, peak, 600.0, 600.0]
161 side =
lambda peak: {
"tank_psia": tank(peak),
"stiffness": [0.0, 0.0, 0.3, 0.3, 0.3, 0.3],
162 "liquid_kg": [5.0] * 6}
163 block =
lambda peak: {
"t0_psia": 600.0,
"min_psia": 600.0,
"stiffness_min": 0.3,
"peak_psia": peak,
164 "loaded_kg": 5.0,
"residual_kg": 0.0}
166 "series": {
"t": t,
"dt": [0.05] * 6,
"firing": [
False,
False,
True,
True,
True,
True],
"converged": [
True] * 6,
167 "copv_psia": [1500.0] * 6,
"ox": side(peak_ox),
"fuel": side(peak_fuel)},
168 "summary": {
"depleted_side":
"oxidiser",
"burn_time_s": 0.2,
"copv_end_psia": 1500.0,
"copv_t0_psia": 4500.0,
169 "ox": block(peak_ox),
"fuel": block(peak_fuel),
"failed_steps": 0,
"t0_settled":
True,
170 "card_outside_steps": 0,
"steps": 6},
171 "events": [],
"converged":
True,
172 "provenance": {
"derived": {
"roles": {
"oxidiser":
"OXT",
"fuel":
"FUT"},
"ambient_pa": 94069.72,
173 "gauge_zero_pa": 101325.0,
174 "tank_mawp_psi": {
"OXT": 750.0 + ATM_PSI,
"FUT": 750.0 + ATM_PSI},
175 "vessel_ratings": {
"OXT": {
"basis":
"burst",
"label":
"TK-LOX",
176 "words":
"764.7 psia (burst 1500 psig "
177 "over a safety factor of 2, estimated)"}}}},
182 """``tank_mawp_psi`` is absolute: a 770 psia peak against a 764.70 psia trip is 755.30 psig of 750.00,
183 100.7 % -- over the red line, where reading the trip as across the wall (peak less the site's
184 13.64 psia, against 764.70) put it at 756.36, under. 700 psia is 685.30 of 750.00, 91 %: amber."""
185 rows = {r[
"key"]: r
for r
in grade(
_burn(770.0, 700.0))}
186 ox, fuel = rows[
"tank_mawp_ox"], rows[
"tank_mawp_fuel"]
187 assert ox[
"limit"] == pytest.approx(750.0)
and ox[
"value"] == pytest.approx(770.0 - ATM_PSI)
188 assert ox[
"value"] > ox[
"limit"]
189 assert fuel[
"value"] == pytest.approx(700.0 - ATM_PSI)
and ox[
"warn"] < fuel[
"value"] < ox[
"limit"]
190 assert ox[
"grade"] ==
"info"
191 assert "burst 1500 psig" in ox[
"basis"]
and ox[
"rating_source"] ==
"burst"
194 assert ox[
"unit"] == fuel[
"unit"] ==
"psi"
196 assert ox[
"basis"].endswith(
"TK-LOX trips the stand at")
197 assert fuel[
"basis"].endswith(
"FUT trips the stand at")
201 """The optimiser's lockup ceiling, the set point's MAWP cap and the set point's fallback grade all
202 stop at the trip itself; each used to add the site's atmosphere (13.64 psia) on top."""
204 d = {
"target_lockup_psia": 700.0,
"ambient_pa": 94069.72,
"copv_psig": 4500.0,
"copv_drawn_psig": 4500.0,
205 "tank_mawp_psi": {
"OXT": 764.6959,
"FUT": 764.6959},
"copv_mawp_psi": 7514.6959}
206 variables = {v.key: v
for v
in opt.default_variables(SimpleNamespace(derived=d), SimpleNamespace(design_requirements=
None))}
207 assert variables[
"lockup_psia"].hi == pytest.approx(764.7)
208 assert sp.lockup_ceiling_psia(d) == pytest.approx(764.6959)
209 assert sp.lockup_ceiling_psia({
"tank_mawp_psi": {}}) == float(
"inf")
210 res = {
"summary": {
"ox": {
"peak_psia": 770.0},
"fuel": {
"peak_psia": 700.0}},
"converged":
True}
211 prep = SimpleNamespace(derived=d, roles={
"oxidiser":
"OXT",
"fuel":
"FUT"})
212 rows = {r[
"key"]: r
for r
in sp._fallback_limits(res, prep, SimpleNamespace(design_requirements=
None), {})}
213 assert rows[
"tank_peak_mawp.oxidiser"][
"limit"] == pytest.approx(764.6959)
214 assert rows[
"tank_peak_mawp.oxidiser"][
"grade"] ==
"bad" and rows[
"tank_peak_mawp.fuel"][
"grade"] ==
"ok"
test_the_limits_grade_the_tank_peak_on_the_trip()
_burst_rated(drawing, tank_psi=1500.0, bottle_psi=15000.0, relief_psi=None)
test_burst_pressure_is_the_rating_when_drawn(config, drawing)
test_the_set_point_and_optimiser_caps_are_the_trip()
_burn(float peak_ox, float peak_fuel)
test_a_fill_over_the_bottles_relief_is_said(config, drawing)
test_a_burn_that_starts_off_its_datum_is_not_a_settled_answer(config, drawing)
_prep(config, drawing, **settings)
test_a_lockup_over_the_mawp_trip_fails_preflight(config, drawing)