1"""Layer X integration: the wave-1 diagnostics wired into the burn, the run record and the API.
3What these hold (fast; the LE4 burns are tests/test_layerx_integration_le4.py, LAYERX_GOLDEN=1):
5* **Defaults are today's.** Every new setting is ``None`` and resolves to the behaviour before it
6 existed; the router mirrors them with the same defaults; the baseline's list of defaults is
7 unchanged by their arrival.
8* **GN2 on LOX** is refused for a hot fire on the physical criterion (nitrogen's saturation pressure
9 at the LOX temperature, CoolProp), checked by hand, and runs with the acknowledgement.
10* **The tanks at burnout** (preflight ``tank_rise``) follow the drawing's regulator law at the
11 bottle's isentropic end pressure, re-derived here independently.
12* **Limits** keep today's chug grade on the default basis with the settled and drawing-basis
13 margins beside it (D7), and cap the new diagnostics at amber without touching the old limits.
14* **Events** carry unique stable keys; **models** are flattened into the run record.
15* **Router**: sidecars are written beside the run, served, pruned and deleted with it; exports;
16 trade and optimise runs are listed as legacy.
19from __future__
import annotations
28from dataclasses
import asdict, fields
29from pathlib
import Path
30from types
import SimpleNamespace
34sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
36ROOT = Path(__file__).resolve().parents[1]
37BASELINE = ROOT /
"docs" /
"layerx" /
"baseline-2026-10-07.json"
38PSI = 6894.757293168361
40NEW_SETTINGS = (
"chug_basis",
"chug_eroded",
"card_eroded_nozzle",
"flight_coupling",
"fuel_lead_s",
41 "valve_travel_s",
"outlet_d_mm",
"ack_gn2_condensation",
"test_mode")
48 """Unset resolves to the default; for the chug that is D7 as the user decided it on 2026-10-03
49 (the drawing's lines, the eroded engine, graded from the first full-flow step)."""
52 s = LayerXSettings(drawing_id=
"d")
53 for name
in NEW_SETTINGS:
54 assert getattr(s, name)
is None, name
55 assert options(s) == {
56 "chug_basis":
"drawing",
"chug_eroded":
True,
"card_eroded_nozzle":
True,
"flight_coupling":
"inline",
57 "fuel_lead_s": 0.0,
"valve_travel_s":
None,
"outlet_d_mm": [
None,
None],
"ack_gn2_condensation":
False,
58 "test_mode":
"hotfire"}
61 was = json.loads(BASELINE.read_text())[
"layerx_settings_defaults"]
62 now = {k: v
for k, v
in asdict(s).items()
if k !=
"drawing_id"}
64 twin = (
"ullage_collapse",
"ullage_vapour",
"chilldown",
"line_walls")
65 assert all(now[k]
is None for k
in twin)
66 assert {k: v
for k, v
in now.items()
if k
in was
and k
not in twin} == {k: v
for k, v
in was.items()
if k
not in twin}
67 assert all(now[k]
is None for k
in now
if k
not in was)
71 """2026-10-03: the eroded nozzle's thrust, the eroded chug, the inline ascent are the physics;
72 the valve opening and the tank outlet bore are the drawing's. Saved settings that carry them
73 are accepted and change nothing."""
76 want = options(LayerXSettings(drawing_id=
"d"))
77 old = LayerXSettings(drawing_id=
"d", chug_basis=
"config", chug_eroded=
False, card_eroded_nozzle=
False,
78 flight_coupling=
"outer", valve_travel_s=0.2, outlet_d_mm=9.5)
79 assert options(old) == want
82@pytest.mark.parametrize(
"bad", [{
"chug_basis":
"both"}, {
"flight_coupling":
"sideways"}, {
"test_mode":
"coldflow_water"},
83 {
"fuel_lead_s": -1.0}, {
"outlet_d_mm": [1.0, 2.0, 3.0]}, {
"outlet_d_mm": 0.01}])
87 drawing = SimpleNamespace(payload={
"nodes": [],
"edges": []}, name=
"x", id=
"x", sha256=
"")
88 prep = prepare(SimpleNamespace(model_dump=
lambda **_: {}),
None, drawing, LayerXSettings(drawing_id=
"x", **bad))
90 assert [c.key
for c
in prep.checks] == [
"options"]
98 engine = {f.name: f.default
for f
in fields(LayerXSettings)
if f.name
not in (
"drawing_id",
"card_center_psia")}
99 router = Settings(drawing_id=
"d").model_dump()
100 for name, default
in engine.items():
101 assert name
in router, name
102 assert router[name] == default, (name, router[name], default)
104 for bad
in ({
"chug_basis":
"both"}, {
"flight_coupling":
"x"}, {
"test_mode":
"coldflow_ln2"},
105 {
"fuel_lead_s": -0.1}, {
"outlet_d_mm": [1.0, 2.0, 3.0]}, {
"outlet_d_mm": 0.1}):
106 with pytest.raises(Exception):
107 Settings(drawing_id=
"d", **bad)
108 ok = Settings(drawing_id=
"d", chug_basis=
"drawing", flight_coupling=
"inline", outlet_d_mm=[9.0,
None],
109 ack_gn2_condensation=
True, test_mode=
"hotfire")
112 assert options(LayerXSettings.from_dict(ok.model_dump()))[
"outlet_d_mm"] == [
None,
None]
118def _tank(T=None, fluid="oxygen", source="measured"):
121 params[
"temperature"] = SimpleNamespace(si=T, source=SimpleNamespace(value=source))
122 return SimpleNamespace(id=
"OXT", fluid=fluid, params=params)
126 import CoolProp.CoolProp
as CP
131 psat_n2 = CP.PropsSI(
"P",
"T", T,
"Q", 0,
"Nitrogen")
132 pv_o2 = CP.PropsSI(
"P",
"T", T,
"Q", 0,
"Oxygen")
133 assert 51.5 < psat_n2 / PSI < 53.0
134 edge = psat_n2 + pv_o2
135 below = gn2_on_lox(
"nitrogen",
_tank(T), edge - 0.1 * PSI)
136 above = gn2_on_lox(
"nitrogen",
_tank(T), edge + 0.1 * PSI)
138 assert below[
"status"] ==
"info" and above[
"status"] ==
"warn"
139 assert f
"{psat_n2 / PSI:.1f} psia" in above[
"detail"]
and "optimistic" in above[
"detail"]
141 assert "OXT" not in above[
"detail"]
and "ack_gn2" not in above[
"detail"]
and "twin" not in above[
"detail"]
143 assert gn2_on_lox(
"nitrogen",
_tank(T), 578.0 * PSI, acknowledged=
True) == gn2_on_lox(
"nitrogen",
_tank(T), 578.0 * PSI)
145 assert gn2_on_lox(
"helium",
_tank(T), 578.0 * PSI)
is None
146 assert gn2_on_lox(
"nitrogen",
_tank(T, fluid=
"ethanol"), 578.0 * PSI)
is None
147 assert gn2_on_lox(
"nitrogen",
_tank(T), 578.0 * PSI, test_mode=
"coldflow_water")
is None
149 assert gn2_on_lox(
"nitrogen",
_tank(130.0), 2000.0 * PSI)[
"status"] ==
"info"
151 nbp = gn2_on_lox(
"nitrogen",
_tank(
None), 578.0 * PSI)
152 assert nbp[
"status"] ==
"warn" and "normal boiling point" in nbp[
"detail"]
156 p = importlib.import_module(
"engine.layerx.prepare")
158 assert not hasattr(p,
"N2_CRITICAL_PSIA")
159 assert "492.5" not in Path(p.__file__).read_text().replace(
"(492.5 psia)",
"")
166 """An affine regulator law (the drawing's: outlet rises ``k`` per psi the supply falls) and a
167 helium bottle, solved here by a separate fixed point."""
168 import CoolProp.CoolProp
as CP
170 p = importlib.import_module(
"engine.layerx.prepare")
172 k, lock0, p0, T0, Vb = 0.017, 578.0 * PSI, 4514.7 * PSI, 293.15, 4.6871e-3
173 ull, exp = 3.6e-3, 11.37e-3
174 law =
lambda model, dome, supply: lock0 + k * (p0 - supply)
175 orig = p.lockup_for_dome
176 p.lockup_for_dome = law
178 got = p.expected_tank_rise(
None, dome_psig=513.0, gas=
"Helium", bottle_pa=p0, bottle_K=T0, bottle_m3=Vb,
179 lockup_pa=lock0, ullage_m3=ull, expelled_m3=exp)
181 p.lockup_for_dome = orig
182 s0 = CP.PropsSI(
"S",
"P", p0,
"T", T0,
"Helium")
183 m0 = CP.PropsSI(
"D",
"P", p0,
"T", T0,
"Helium") * Vb
184 rho_lock = CP.PropsSI(
"D",
"P", lock0,
"T", T0,
"Helium")
187 need = CP.PropsSI(
"D",
"P", end,
"T", T0,
"Helium") * (ull + exp) - rho_lock * ull
188 pb = CP.PropsSI(
"P",
"D", (m0 - need) / Vb,
"S", s0,
"Helium")
189 end = lock0 + k * (p0 - pb)
191 assert got[
"converged"]
192 assert abs(got[
"end_pa"] - end) < 0.01 * PSI
193 assert abs(got[
"bottle_end_pa"] - pb) < 0.1 * PSI
194 assert abs(got[
"rise_pa"] - k * (p0 - pb)) < 0.01 * PSI
195 assert 40.0 < got[
"rise_pa"] / PSI < 60.0
197 pb_isothermal = CP.PropsSI(
"P",
"D", (m0 - need) / Vb,
"T", T0,
"Helium")
198 assert pb < pb_isothermal
202 p = importlib.import_module(
"engine.layerx.prepare")
204 orig = p.lockup_for_dome
205 p.lockup_for_dome =
lambda model, dome, supply: 578.0 * PSI
207 got = p.expected_tank_rise(
None, dome_psig=513.0, gas=
"Helium", bottle_pa=1000.0 * PSI, bottle_K=293.15,
208 bottle_m3=1e-3, lockup_pa=578.0 * PSI, ullage_m3=1e-3, expelled_m3=20e-3)
210 p.lockup_for_dome = orig
211 assert got[
"end_pa"]
is None and got[
"bottle_end_pa"]
is None and got[
"gas_kg"] > 0
218 t = [-0.05, 0.0] + [dt * (k + 1)
for k
in range(n)]
219 firing = [
False,
False] + [
True] * n
220 flat =
lambda v: [v] * len(t)
221 side =
lambda: {
"tank_psia": flat(578.0),
"stiffness": [0.0, 0.0] + [0.30] * n,
"liquid_kg": flat(5.0)}
223 "depleted_side":
"oxidiser",
"burn_time_s": t[-1],
"copv_end_psia": 1500.0,
"copv_t0_psia": 4500.0,
224 "ox": {
"t0_psia": 578.0,
"min_psia": 570.0,
"stiffness_min": 0.30,
"peak_psia": 578.0,
"loaded_kg": 6.0,
"residual_kg": 0.0},
225 "fuel": {
"t0_psia": 578.0,
"min_psia": 572.0,
"stiffness_min": 0.30,
"peak_psia": 1100.0,
"loaded_kg": 4.0,
"residual_kg": 0.5},
226 "failed_steps": 0,
"t0_settled":
True,
"card_outside_steps": 0,
"steps": len(t),
229 "series": {
"t": t,
"dt": [dt] * len(t),
"firing": firing,
"converged": [
True] * len(t),
"copv_psia": flat(1500.0),
230 "ox": side(),
"fuel": side()},
231 "summary": summary,
"events": [],
"converged":
True,
232 "delivered": {
"t": t[2:],
"chug_margin": [1.31, 1.4, 1.41, 1.42, 1.43, 1.44, 1.45, 1.46],
233 "summary": {
"chug_margin_min": 1.31,
"chug_margin_min_t": 0.05}},
234 "provenance": {
"derived": {
"stiffness_band": {
"oxidiser": [0.2, 0.4],
"fuel": [0.2, 0.4]},
235 "roles": {
"oxidiser":
"OXT",
"fuel":
"FUT"},
236 "tank_mawp_psi": {
"OXT": 1000.0,
"FUT": 1000.0},
"ambient_pa": 101325.0}},
238 "stability": {
"basis":
"config",
"t": t,
"margin": [
None,
None] + [1.20, 1.30, 1.31, 1.32, 1.33, 1.34, 1.35, 1.36],
239 "worst": {
"t": 0.05,
"index": 2,
"margin": 1.20,
"frequency_hz": 34.6},
240 "settled_min": {
"t": 0.10,
"index": 3,
"margin": 1.30,
"frequency_hz": 22.0},
241 "start_window_s": 0.1,
"other_basis": {
"basis":
"drawing",
"margin_min": 1.45,
"t": 0.1}},
242 "water_hammer": [{
"line":
"l_fu1",
"side":
"fuel",
"peak_psia": 2906.0,
"rating_psia": 1014.7,
"ok":
False}],
248 return {e[
"key"]: e
for e
in entries}
255 g =
_by_key(grade_limits(r,
None,
None, {
"chug_basis":
"config"}))
257 assert g[
"chug_margin"][
"value"] == 1.31
and g[
"chug_margin"][
"series_ref"] ==
"delivered.chug_margin"
258 assert g[
"chug_margin"][
"decision"] ==
"D7" and "D7" in g[
"chug_margin"][
"hint"]
260 assert g[
"chug_margin_settled"][
"value"] == 1.30
and g[
"chug_margin_settled"][
"grade"] ==
"info"
261 assert g[
"chug_margin_settled"][
"frequency_hz"] == 22.0
262 assert g[
"chug_margin_other_basis"][
"value"] == 1.45
and g[
"chug_margin_other_basis"][
"grade"] ==
"info"
263 assert "chug_margin_start" not in g
267 before = {**r,
"diagnostics": {k: v
for k, v
in r[
"diagnostics"].items()
if k !=
"stability"}}
268 old =
_by_key(grade(before))[
"chug_margin"]
269 assert {k: g[
"chug_margin"][k]
for k
in (
"value",
"grade",
"t_worst",
"index_worst",
"basis")} == \
270 {k: old[k]
for k
in (
"value",
"grade",
"t_worst",
"index_worst",
"basis")}
277 r[
"diagnostics"][
"stability"][
"basis"] =
"drawing"
278 g =
_by_key(grade_limits(r,
None,
None, {
"chug_basis":
"drawing"}))
279 assert g[
"chug_margin"][
"value"] == 1.30
and g[
"chug_margin"][
"series_ref"] ==
"diagnostics.stability.margin"
280 assert g[
"chug_margin_start"][
"grade"] ==
"info" and "chug_margin_settled" not in g
287 r[
"diagnostics"][
"saturation"] = {
"nodes": [{
"id":
"FUT.out",
"label":
"Fuel tank outlet",
"side":
"fuel",
288 "margin_psi": [-5.0] * len(r[
"series"][
"t"]),
"min_psi": -5.0,
"t_min": 0.1}]}
289 g =
_by_key(grade_limits(r,
None,
None, {
"chug_basis":
"config"}))
290 sat = g[
"saturation_FUT.out"]
291 assert sat[
"grade"] ==
"warn" and sat[
"capped_from"] ==
"bad" and sat[
"review_pending"]
292 assert "reviewed" in sat[
"hint"]
294 assert "water_hammer_l_fu1" not in g
296 assert g[
"tank_mawp_fuel"][
"grade"] ==
"info" and "capped_from" not in g[
"tank_mawp_fuel"]
305 assert [e[
"key"]
for e
in grade_limits(r,
None,
None, {
"chug_basis":
"config"})] == [e[
"key"]
for e
in grade(r)]
316 {
"t": -0.05,
"kind":
"t0",
"key":
"t0",
"label":
"T-0 state",
"detail":
""},
317 {
"t": 0.0,
"kind":
"fire",
"key":
"fire",
"label":
"Fire",
"detail":
""},
318 {
"t": 0.05,
"kind":
"min",
"key":
"min_tank_ox",
"label":
"LOX tank lowest",
"detail":
""},
319 {
"t": 0.4,
"kind":
"end",
"key":
"dry_ox",
"label":
"LOX tank dry",
"detail":
""},
320 {
"t": 0.4,
"kind":
"warn",
"label":
"Unconverged steps",
"detail":
""},
321 {
"t": 0.1,
"kind":
"warn",
"label":
"Something earlier",
"detail":
""},
323 r[
"diagnostics"][
"start"] = {
"available":
True,
"fuel_lead_s": 0.3,
"ignition_s": 0.017,
"prime_ox_s": 0.0096,
324 "prime_fuel_s": 0.0173}
325 r[
"limits"] = [{
"key":
"chug_margin",
"value": 1.31,
"t_worst": 0.05,
"basis":
"config"}]
326 r[
"tripped"] = {
"vessel":
"FUT",
"t": 0.3,
"p_psia": 1020.0,
"mawp_psia": 1014.7,
"message":
"FUT over MAWP"}
327 _key_events(r,
None, {})
328 keys = [e[
"key"]
for e
in r[
"events"]]
329 assert len(keys) == len(set(keys))
330 assert {
"t0",
"fire",
"fuel_lead",
"ignition",
"min_tank_ox",
"min_chug",
"dry_ox",
"burnout",
"trip"} <= set(keys)
331 assert [e[
"key"]
for e
in r[
"events"]
if e[
"key"].startswith(
"warn:")] == [
"warn:0",
"warn:1"]
332 assert next(e
for e
in r[
"events"]
if e[
"key"] ==
"warn:0")[
"label"] ==
"Something earlier"
333 assert next(e
for e
in r[
"events"]
if e[
"key"] ==
"fuel_lead")[
"t"] == -0.3
334 assert [e[
"t"]
for e
in r[
"events"]] == sorted(e[
"t"]
for e
in r[
"events"])
340 m =
lambda n: {
"name": n,
"source":
"s",
"assumptions": [
"a"],
"inputs": {
"x": {
"value": 1}}}
341 r = {
"diagnostics": {
"stability": {
"model": m(
"chug")},
"water_hammer": [{
"model": m(
"moc")}, {
"available":
False}],
342 "hardware": {
"soak": {
"model": m(
"soak")},
"model": m(
"hw")}},
343 "flight": {
"stability": {
"model": m(
"barrowman")}},
"replay": {
"soak": {
"model": m(
"soak")}}}
344 got = {(x[
"block"], x[
"name"])
for x
in model_record(r)}
345 assert got == {(
"diagnostics.stability",
"chug"), (
"diagnostics.water_hammer[0]",
"moc"), (
"diagnostics.hardware",
"hw"),
346 (
"diagnostics.hardware.soak",
"soak"), (
"flight.stability",
"barrowman")}
354 raise RuntimeError(
"broken on purpose")
356 monkeypatch.setattr(stability,
"stability_block", boom)
357 monkeypatch.setattr(vv,
"check",
lambda result, prep=
None: {
"mass": {},
"model": {
"name":
"vv"}})
358 prep = SimpleNamespace(link=
None, settings=
None, roles={}, derived={}, ambient_pa=101325.0)
359 out, wall = analysis.diagnostics_of(prep,
_result(),
None,
None, {
360 "chug_basis":
"config",
"chug_eroded":
False,
"fuel_lead_s": 0.0,
"valve_travel_s":
None,
"outlet_d_mm": [
None,
None]})
361 assert out[
"stability"] == {
"available":
False,
"error":
"RuntimeError: broken on purpose"}
362 assert out[
"vv"][
"model"][
"name"] ==
"vv"
363 assert set(out) == set(analysis.DIAGNOSTIC_KEYS)
364 assert set(wall) >= {
"stability",
"hardware",
"feed",
"start",
"outflow",
"shutdown",
"water_hammer",
"vv"}
371def api(tmp_path, monkeypatch):
372 from fastapi
import FastAPI
373 from fastapi.testclient
import TestClient
378 monkeypatch.setenv(
"USERDATA_DIR", str(tmp_path))
379 session = UserSession(f
"integ-{tmp_path.name}")
381 app.include_router(lx.router)
382 app.dependency_overrides[get_session] =
lambda: session
383 yield TestClient(app), session, lx
385 for job_id
in [j.id
for j
in lx._JOBS.values()
if j.user == session.user]:
386 lx._JOBS.pop(job_id,
None)
390 """A small finished burn the exporters can read (as tests/test_layerx_export.py builds it)."""
391 t = [-0.1, -0.05, 0.0, 0.05, 0.1, 0.15, 0.2, 0.25]
393 fire = [
False,
False,
False,
True,
True,
True,
True,
True]
394 ramp =
lambda a, b: [a + (b - a) * k / (n - 1)
for k
in range(n)]
395 side =
lambda p: {
"tank_psia": ramp(p, p + 3.0),
"stiffness": [0.0] * 3 + [0.31, 0.32, 0.33, 0.34, 0.35],
396 "mdot": [0.0] * 3 + [1.9] * 5}
398 "series": {
"t": t,
"dt": [0.05] * n,
"firing": fire,
"converged": [
True] * n,
"copv_psia": ramp(4500.0, 4300.0),
399 "ox": side(578.0),
"fuel": side(578.1),
400 "chamber": {
"pc_psia": [0.0] * 3 + [395.0, 396.0, 397.0, 398.0, 399.0],
"thrust_N": [0.0] * 3 + [6900.0] * 5}},
401 "summary": {
"burn_time_s": 0.25,
"total_impulse_Ns": 1725.0},
402 "provenance": {
"config_sha256":
"abc",
"drawing": {
"name":
"stand",
"sha256":
"def"}},
406def _save(lx, session, run_id, kind="run", sidecars=None, result=None):
407 job = lx.Job(session.user, lx.Settings(drawing_id=
"d"),
"design", kind=kind)
409 job.status, job.finished =
"done", 1.0
410 job.result = result
or {
"summary": {
"burn_time_s": 3.5,
"total_impulse_Ns": 24000.0},
"provenance": {
"drawing": {
"name":
"stand"}}}
411 job.sidecars = sidecars
or {}
412 lx._persist(lx._runs_dir(session), job)
416AXIAL = {
"x_mm": [-10.0, 0.0, 10.0],
"t": [0.05, 0.25],
"q_MW_m2": [[1.0, 9.0, 3.0], [1.1, 9.5, 3.1]],
417 "T_wall_K": [[500.0, 2000.0, 900.0], [520.0, 2100.0, 950.0]]}
421 client, session, lx = api
422 rid =
"20261003-000001-aaaaaa"
423 _save(lx, session, rid, sidecars={
"axial": AXIAL,
"unknown": {
"x": 1}})
424 runs_dir = lx._runs_dir(session)
425 assert (runs_dir /
"_sidecar" / f
"{rid}.axial.json").is_file()
426 assert not (runs_dir /
"_sidecar" / f
"{rid}.unknown.json").exists()
427 assert client.get(f
"/api/layerx/runs/{rid}/sidecar/axial").
json() == AXIAL
428 assert client.get(f
"/api/layerx/runs/{rid}/sidecar/unknown").status_code == 404
429 assert client.get(
"/api/layerx/runs/20261003-000002-bbbbbb/sidecar/axial").status_code == 404
430 assert client.get(
"/api/layerx/runs/..%2F..%2Fetc/sidecar/axial").status_code == 404
432 assert [r[
"id"]
for r
in client.get(
"/api/layerx/runs").
json()] == [rid]
433 assert client.delete(f
"/api/layerx/runs/{rid}").
json()[
"deleted"]
434 assert not (runs_dir /
"_sidecar" / f
"{rid}.axial.json").exists()
438 client, session, lx = api
439 monkeypatch.setattr(lx,
"KEEP_RUNS", 1)
440 _save(lx, session,
"20261003-000001-aaaaaa", sidecars={
"axial": AXIAL})
441 _save(lx, session,
"20261003-000002-bbbbbb", sidecars={
"axial": AXIAL})
442 left = sorted(p.name
for p
in (lx._runs_dir(session) /
"_sidecar").iterdir())
443 assert left == [
"20261003-000002-bbbbbb.axial.json"]
447 client, session, lx = api
448 job = lx.Job(session.user, lx.Settings(drawing_id=
"d"),
"design")
450 lx._JOBS[job.id] = job
451 lx._execute(job,
lambda progress, cancelled: {**
_export_run(),
"sidecars": [
"axial"],
"_sidecars": {
"axial": AXIAL}},
452 lx._runs_dir(session))
453 assert job.status ==
"done" and "_sidecars" not in job.result
454 assert client.get(f
"/api/layerx/runs/{job.id}/sidecar/axial").
json() == AXIAL
458 client, session, lx = api
459 rid =
"20261003-000001-aaaaaa"
461 csv = client.get(f
"/api/layerx/runs/{rid}/export/csv")
462 assert csv.status_code == 200
and csv.headers[
"content-type"].startswith(
"text/csv")
463 assert f
'filename="layerx-{rid}.csv"' in csv.headers[
"content-disposition"]
464 assert csv.text.splitlines()[0].startswith(
"t [s],dt [s],firing [bool]")
465 pq = client.get(f
"/api/layerx/runs/{rid}/export/parquet")
469 assert pq.status_code == 200
and pq.content[:4] == b
"PAR1"
471 assert pq.status_code == 501
472 fea = client.get(f
"/api/layerx/runs/{rid}/export/fea")
473 names = zipfile.ZipFile(io.BytesIO(fea.content)).namelist()
474 assert fea.status_code == 200
and {
"pc_t.csv",
"thrust_t.csv",
"heatflux_xt.csv",
"loads.json"} <= set(names)
475 assert client.get(f
"/api/layerx/runs/{rid}/export/xls").status_code == 404
476 _save(lx, session,
"20261003-000002-bbbbbb", kind=
"setpoint", result={
"mode":
"setpoint"})
477 assert client.get(
"/api/layerx/runs/20261003-000002-bbbbbb/export/csv").status_code == 400
481 client, session, lx = api
482 for i, kind
in enumerate((
"run",
"trade",
"optimize",
"setpoint",
"hardware",
"reconcile",
"uncertainty")):
483 _save(lx, session, f
"20261003-00000{i}-aaaaa{i}", kind=kind)
484 listed = {r[
"kind"]: bool(r.get(
"legacy"))
for r
in client.get(
"/api/layerx/runs").
json()}
485 assert listed == {
"run":
False,
"trade":
True,
"optimize":
True,
"setpoint":
False,
"hardware":
False,
486 "reconcile":
False,
"uncertainty":
False}
487 assert client.get(
"/api/layerx/runs/20261003-000002-aaaaa2").
json()[
"legacy"]
is True
491 client, session, lx = api
492 body = client.get(
"/api/layerx/catalog").
json()
493 assert body[
"problems"] == []
and body[
"drills"]
494 shipped = {k: v
for k, v
in body.items()
if k
not in (
"drills",
"problems")}
495 assert shipped
and all(r[
"origin"] ==
"shipped" for rows
in shipped.values()
for r
in rows)
496 kind, rows = next((k, v)
for k, v
in shipped.items()
if v)
497 udir = lx._user_dir(session) /
"layerx" /
"catalog"
498 udir.mkdir(parents=
True, exist_ok=
True)
499 (udir / f
"{kind}.json").write_text(
"not json")
500 again = client.get(
"/api/layerx/catalog").
json()
501 assert again[
"problems"]
and again[kind] == rows
505 """The helium study drawing as LE4 has it: the dome loaded from the cart (its loader, PR-CTRL,
506 gone with the GSE) and a transducer on the dome line, clipped to a junction there."""
511 payload = json.loads((shipped_drawings_dir() /
"copv_study_he.json").read_text())
512 out = _copy.deepcopy(payload)
513 out[
"nodes"] = [n
for n
in out[
"nodes"]
if n[
"id"] !=
"PR_C"]
514 out[
"edges"] = [e
for e
in out[
"edges"]
if "PR_C" not in (e[
"source"], e[
"target"])]
515 for n
in out[
"nodes"]:
516 if n[
"id"] ==
"PR_D":
517 n[
"data"].setdefault(
"options", {})[
"domeLoaded"] =
"yes"
519 {
"id":
"JD",
"position": {
"x": 250,
"y": 380},
"data": {
"componentType":
"JUNCTION",
"label":
"JD"}},
520 {
"id":
"PT_DOME",
"position": {
"x": 250,
"y": 420},
"data": {
"componentType":
"PT",
"label":
"DP-PT"}},
522 out[
"edges"] += [{
"id":
"PR_D-JD",
"source":
"PR_D",
"sourceHandle":
"dome",
"target":
"JD",
"targetHandle":
"l"},
523 {
"id":
"JD-PT_DOME",
"source":
"JD",
"sourceHandle":
"b",
"target":
"PT_DOME",
"targetHandle":
"b"}]
528 """LE4's DP-PT-R sits on the dome line of DPR_HP, whose loader is on the cart: the burn left the
529 line at atmosphere and the Stand page predicted 0.0 psig under a 550 psia dome (D-27). The
530 line's transducers read the dome signal; every other one reads the network as before."""
531 pytest.importorskip(
"feedtwin")
538 built = build_network(diagram)
539 domes = dome_line_instruments(diagram, built)
540 pt = next(i
for i
in built.instruments
if i.id ==
"PT_DOME")
541 assert domes == {pt.node:
"PR-DOME.dome"}
545 shipped = read_diagram(json.loads((shipped_drawings_dir() /
"copv_study_he.json").read_text()), name=
"study")
546 assert dome_line_instruments(shipped, build_network(shipped)) == {}
548 other = next(i
for i
in built.instruments
if i.id !=
"PT_DOME")
549 trace = SimpleNamespace(t=[0.0, 0.05], pressure={pt.node: [101325.0] * 2, other.node: [3e6, 2.9e6]}, temperature={},
550 dome_readings={pt.node: [101325.0 + 535 * PSI] * 2})
551 prep = SimpleNamespace(model=SimpleNamespace(built=built))
552 series = _instrument_series(prep, trace)
553 assert series[
"PT_DOME"][
"values"] == pytest.approx([549.6959] * 2, abs=1e-3)
554 assert series[other.id][
"values"] == pytest.approx([3e6 / PSI, 2.9e6 / PSI])
a class to store JSON values
test_without_diagnostics_the_limits_are_diag_limits_own()
test_pruning_takes_a_runs_sidecar_with_it(api, monkeypatch)
test_gn2_on_lox_is_flagged_above_nitrogens_saturation_pressure_by_hand()
test_the_catalogue_lists_shipped_rows_and_a_users_over_them(api)
test_a_runs_sidecar_is_written_beside_it_served_and_deleted_with_it(api)
_tank(T=None, fluid="oxygen", source="measured")
test_the_492_psia_constant_is_gone()
test_a_bottle_that_cannot_hold_lockup_says_so()
test_a_failing_diagnostic_is_reported_and_the_rest_stand(monkeypatch)
_save(lx, session, run_id, kind="run", sidecars=None, result=None)
test_trade_and_optimise_runs_are_listed_and_read_as_legacy(api)
test_what_the_physics_or_the_drawing_decides_is_not_a_setting()
test_the_drawing_basis_grades_the_stability_blocks_settled_minimum()
test_a_transducer_on_a_cut_dome_line_reads_the_dome()
test_the_model_record_flattens_every_block()
test_a_finished_burn_exports_csv_parquet_and_the_fea_bundle(api)
test_expected_tank_rise_is_the_regulator_law_at_the_isentropic_bottle_by_hand()
test_new_diagnostics_grade_amber_at_worst_and_old_limits_keep_their_grades()
test_new_settings_are_unset_and_resolve_to_the_decided_defaults()
test_every_event_gets_a_unique_stable_key()
test_the_router_mirrors_every_setting_with_the_same_default()
test_the_default_chug_grade_is_todays_whole_burn_minimum_with_the_new_margins_beside_it()
_study_with_a_cut_dome_line()
test_an_option_it_cannot_take_fails_preflight_before_anything_is_built(bad)
api(tmp_path, monkeypatch)
test_the_run_job_hands_its_sidecars_to_the_job_not_the_result(api)