1"""Layer X phase 5: measured inputs, and which unmeasured ones move the burn.
3* An override writes a stated number into the drawing before the twin reads it, with its
4 provenance, so the twin's own assembly report counts it as measured.
5* The uncertainty sweep varies each unmeasured input on its own. Checked against what each
6 input can and cannot do, not against a stored answer:
7 * nozzle efficiency changes thrust and nothing upstream of the nozzle;
8 * a lower mixing E_m costs impulse;
9 * a larger LOX Cd flows faster and burns out no later;
10 * a measured input's stated uncertainty replaces the default range.
13from __future__
import annotations
17from pathlib
import Path
22pytest.importorskip(
"feedtwin", reason=
"lib/feedtwin is not installed")
24from engine.layerx import DrawingStore, LayerXSettings, prepare
29FIXTURE = Path(__file__).parent /
"fixtures" /
"ethalox_6500N_doublet_cad_2026-09-28.yaml"
34 pytest.mark.skipif(
not (shipped_drawings_dir() / f
"{GN2}.json").is_file(),
35 reason=
"feed-twin's shipped drawings are not next to this checkout"),
41 pytest.mark.timeout(600),
45@pytest.fixture(scope="module")
47 return load_config(str(FIXTURE))
50@pytest.fixture(scope="module")
52 return {d.name: d
for d
in DrawingStore(
None).list()}[GN2]
55def droop(value: float, uncertainty=
None) -> Override:
56 return Override(target=
"node:PR_D", parameter=
"flow_droop", value=value, unit=
"psi",
57 source=
"regulator flow test, GN2, 2026-10-05", uncertainty=uncertainty)
61 payload, applied, missing = apply_overrides(drawing.payload, [
droop(6.0, 0.4)])
62 assert not missing
and applied[0][
"was"][
"value"] == 8.3
63 node = next(n
for n
in payload[
"nodes"]
if n[
"id"] ==
"PR_D")
64 p = node[
"data"][
"params"][
"flow_droop"]
65 assert p == {
"value": 6.0,
"unit":
"psi",
"source":
"measured",
66 "reference":
"regulator flow test, GN2, 2026-10-05; ±0.4 psi"}
68 assert next(n
for n
in drawing.payload[
"nodes"]
if n[
"id"] ==
"PR_D")[
"data"][
"params"][
"flow_droop"][
"value"] == 8.3
69 _, _, missing = apply_overrides(drawing.payload, [
Override(target=
"node:NOPE", parameter=
"x", value=1, unit=
"",
71 assert missing == [
"node:NOPE.x"]
75 """Rail -> Measured values listed 'COPV pressure 3750 psi' beside a rail reading 3765 psia, and sent what
76 was typed with the bare 'psi', which the twin reads as gauge: typing the rail's 3765 gave the burn 3779.7
77 psia (ADR 0004/0007). The table now carries the psia each absolute pressure means, by the drawing rule."""
80 atm = 101325.0 / 6894.757293168361
81 rows = {(r[
"target"], r[
"parameter"]): r
for r
in parameter_table(drawing.payload)}
82 assert rows[(
"node:KB1",
"pressure")][
"psia"] == pytest.approx(4500.0 + atm)
83 assert rows[(
"node:OXT",
"MAWP")][
"psia"] == pytest.approx(1000.0 + atm)
84 assert rows[(
"node:PR_D",
"setpoint")][
"psia"] == pytest.approx(500.0 + atm)
85 assert rows[(
"node:ENG",
"pressure")][
"psia"] == pytest.approx(350.0)
86 assert "psia" not in rows[(
"node:PR_D",
"flow_droop")]
87 assert "psia" not in rows[(
"node:PR_D",
"dome_bias")]
89 old =
Override(target=
"node:KB1", parameter=
"pressure", value=3750.0, unit=
"psi", source=
"fill log")
90 assert override_view(old, list(rows.values()))[
"psia"] == pytest.approx(3750.0 + atm)
92 typed =
Override(target=
"node:KB1", parameter=
"pressure", value=3765.0, unit=
"psia", source=
"fill log")
93 payload, _, _ = apply_overrides(drawing.payload, [typed])
94 landed = {(r[
"target"], r[
"parameter"]): r
for r
in parameter_table(payload)}[(
"node:KB1",
"pressure")]
95 assert landed[
"unit"] ==
"psia" and landed[
"psia"] == pytest.approx(3765.0)
99 """The override is not a label: the regulator component the twin builds carries the
100 restated value, with the measured provenance, and the burn plan is prepared from it."""
103 def droop_param(prep):
104 built = prep.model.built
105 branch = next(built.network.branches[b]
for b
in built.branches_of[
"PR_D"]
if b
in built.network.branches)
106 return branch.component.instance.params[
"flow_droop"]
108 base = prepare(config,
None, drawing,
LayerXSettings(drawing_id=drawing.id, ack_gn2_condensation=
True))
109 restated = prepare(config,
None, drawing,
LayerXSettings(drawing_id=drawing.id, ack_gn2_condensation=
True), [
droop(6.0, 0.4)])
110 assert droop_param(base).value == pytest.approx(8.3)
111 p = droop_param(restated)
112 assert p.source
is Provenance.MEASURED
and p.value == pytest.approx(6.0)
113 assert p.reference.startswith(
"regulator flow test, GN2, 2026-10-05")
114 assert any(c.key ==
"overrides" for c
in restated.checks)
115 assert restated.derived[
"overrides"][0][
"key"] ==
"node:PR_D.flow_droop"
119 with pytest.raises(ValueError, match=
"source"):
120 Override.from_dict({
"target":
"node:PR_D",
"parameter":
"flow_droop",
"value": 6,
"unit":
"psi",
"source":
" "})
122 store.put(
"0123456789abcdef", [
droop(6.0)])
123 assert store.get(
"0123456789abcdef")[0].value == 6.0
124 with pytest.raises(ValueError, match=
"same parameter"):
125 store.put(
"0123456789abcdef", [
droop(6.0),
droop(7.0)])
131 prep = prepare(config,
None, drawing,
LayerXSettings(drawing_id=drawing.id, ack_gn2_condensation=
True), [
droop(8.3, 0.5)])
132 found, _ = factors(prep, config)
133 f = next(x
for x
in found
if x.key ==
"node:PR_D.flow_droop")
134 lo, hi = (c.overrides[0].value
for c
in f.cases)
135 assert (lo, hi) == pytest.approx((7.8, 8.8))
136 assert f.basis.startswith(
"measured")
139 plain = prepare(config,
None, drawing,
LayerXSettings(drawing_id=drawing.id, ack_gn2_condensation=
True))
140 found, notes = factors(plain, config)
141 assert not any(x.key ==
"node:PR_D.flow_droop" for x
in found)
142 assert any(
"marked measured on the drawing" in n
and "droop" in n.lower()
for n
in notes)
143 spe = next(x
for x
in found
if x.key ==
"node:PR_D.supply_coefficient")
144 assert [c.overrides[0].value
for c
in spe.cases] == pytest.approx([0.5 * spe.nominal, 1.5 * spe.nominal])
147@pytest.fixture(scope="module")
151 return run_sweep(config, drawing,
LayerXSettings(drawing_id=drawing.id, ack_gn2_condensation=
True), [], workers=4)
155 f = next(x
for x
in sweep[
"factors"]
if x[
"key"] == key)
157 assert c[
"ok"], c.get(
"error")
162 keys = {f[
"key"]
for f
in sweep[
"factors"]}
163 assert {
"node:PR_D.supply_coefficient",
"ullage_collapse",
"engine.cd_O",
164 "engine.cd_F",
"engine.em",
"engine.nozzle"} <= keys
165 assert "node:PR_D.flow_droop" not in keys
166 assert sweep[
"band"][
"total_impulse_Ns"] > 0
168 assert {
"drawing.valve_cv",
"drawing.line_k",
"line_walls",
"ullage_vapour",
"op.lockup",
"op.fill"} <= keys
169 assert sweep[
"cases"] == 1 + sum(len(f[
"cases"])
for f
in sweep[
"factors"])
173 """A fixed load burnt dry gives the same impulse whatever the valves: the tornado ranked by
174 impulse said "the top bar is the next measurement" while the feed was invisible to it."""
175 lo =
_case(sweep,
"drawing.valve_cv",
"low")
176 assert abs(lo[
"mean_thrust_N"]) > 5 * abs(lo[
"total_impulse_Ns"]) /
max(sweep[
"nominal"][
"burn_time_s"], 1e-9)
177 assert lo[
"mean_thrust_N"] < 0
181 """Mixing E_m costs far more impulse below than it adds above (0.70 vs 0.85 about 0.80):
182 one symmetric ± overstated the upside."""
183 lo, hi = sweep[
"band_low"][
"total_impulse_Ns"], sweep[
"band_high"][
"total_impulse_Ns"]
184 assert lo > 0
and hi > 0
and lo != pytest.approx(hi, rel=0.05)
185 assert isinstance(sweep[
"crossings"], list)
189 for side
in (
"low",
"high"):
190 d =
_case(sweep,
"engine.nozzle", side)
191 assert d[
"burn_time_s"] == pytest.approx(0.0, abs=1e-9)
192 assert abs(d[
"pc_mean_psia"]) < 0.05
193 assert abs(d[
"total_impulse_Ns"]) > 100.0
194 assert _case(sweep,
"engine.nozzle",
"low")[
"total_impulse_Ns"] < 0 <
_case(sweep,
"engine.nozzle",
"high")[
"total_impulse_Ns"]
198 assert _case(sweep,
"engine.em",
"low")[
"total_impulse_Ns"] < 0
199 assert _case(sweep,
"engine.cd_O",
"high")[
"burn_time_s"] <= 0
200 assert _case(sweep,
"engine.cd_O",
"high")[
"of_mean"] > 0 >
_case(sweep,
"engine.cd_O",
"low")[
"of_mean"]
204 """The card samples the engine with the config's measurements applied, as the runner and the
205 replay do. A measured LOX Cd 5 % above the model's has to show up as more LOX flow at the
206 same line pressures; before this was fixed the card's fast path sampled the unmeasured
207 config and the measurement reached the replay but not the card."""
211 p = 578.0 * 6894.757293168361
212 plain = EngineSampler(config, 94070.0)
213 cd = plain.runner.evaluate(p, p, silent=
True)[
"Cd_O"]
214 measured = config.model_copy(deep=
True)
215 measured.measurements = MeasurementsConfig(cd_O=MeasuredValue(value=1.05 * cd, source=
"cold flow, water"))
216 sampler = EngineSampler(measured, 94070.0)
217 assert sampler.config.discharge[
"oxidizer"].Cd_inf == pytest.approx(1.05 * cd)
218 a, b = plain(p, p), sampler(p, p)
219 assert b[
"mdot_O"] > a[
"mdot_O"] * 1.02
223 """Keyed by what the drawing is (its shipped file, its pid-designer document), not by its
224 content: an edited drawing has a new id and keeps its restatements, and says which revision
225 they were entered against. An old id-keyed file still reads."""
227 from dataclasses
import replace
230 store.put(drawing, [
droop(6.0, 0.4)])
231 edited = copy.deepcopy(drawing.payload)
232 edited[
"nodes"][0].setdefault(
"data", {})[
"label"] =
"renamed"
233 revised = replace(drawing, payload=edited, id=
"fedcba9876543210", sha256=
"0" * 64)
234 overrides, problems, revision = store.load(revised)
235 assert [o.value
for o
in overrides] == [6.0]
and not problems
236 assert revision == drawing.sha256 != revised.sha256
238 other = replace(drawing, source=
"shipped:copv_study_he.json", id=
"1111111111111111")
239 assert store.get(other) == []
242 legacy.put(
"2222222222222222", [
droop(7.0)])
243 assert [o.value
for o
in legacy.get(replace(drawing, id=
"2222222222222222", source=
"upload"))] == [7.0]
245 with pytest.raises(ValueError, match=
"finite"):
246 Override.from_dict({
"target":
"node:PR_D",
"parameter":
"flow_droop",
"value": float(
"nan"),
"unit":
"psi",
test_the_band_is_told_each_way(sweep)
test_a_measured_engine_input_reaches_the_card(config)
Override droop(float value, uncertainty=None)
test_impulse_is_blind_to_the_feed_and_thrust_is_not(sweep)
test_a_measured_uncertainty_replaces_the_default_range(config, drawing)
test_nozzle_efficiency_moves_thrust_and_nothing_upstream(sweep)
test_worse_mixing_costs_impulse_and_more_cd_flows_faster(sweep)
test_the_sweep_runs_every_factor(sweep)
test_a_restated_parameter_reaches_the_twin_as_measured(config, drawing)
test_the_store_wants_a_source_and_one_value_per_parameter(tmp_path)
test_an_override_lands_in_the_drawing_with_its_provenance(drawing)
test_the_restatement_table_reads_and_takes_absolute_pressures_in_psia(drawing)
test_restatements_follow_the_drawing_through_an_edit(tmp_path, drawing)