STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_burn_trip.py
Go to the documentation of this file.
1"""A vessel trip ends the burn.
2
3The session trips the stand when a vessel goes over the pressure its drawing
4rates it for, and from then on returns the frame it failed on. A burn that
5carried on stepping integrated that frozen frame to its horizon: on the LE4
6audit's fuel tank restated at 600 psi it reported 99,478 N.s over 14 s against
7the real ~24,000 (EngineDesign/docs/layerx/AUDIT.md 5.2). The burn now stops on
8the step that trips and says which vessel, when, at what pressure, against what.
9
10The helium drawing's regulator climbs the tanks over a burn (its supply-pressure
11effect), so a fuel tank restated just above the primed pressure trips a few
12tenths of a second after Fire -- not at T-0, and long before the horizon.
13"""
14
15from __future__ import annotations
16
17import copy
18import json
19from pathlib import Path
20
21import pytest
22
23from feedtwin.pid import read_diagram
24from feedtwin.session import PSI, assemble_model, load_machine
25from feedtwin.session.burn import (
26 BurnPlan,
27 burn_setup,
28 open_session,
29 plan_with,
30 run_burn,
31 trip_record,
32)
33from feedtwin.session.gauge import ATMOSPHERE, from_psig, say
34
35STAR = Path(__file__).resolve().parents[3]
36DRAWING = STAR / "feed-twin" / "backend" / "diagrams" / "copv_study_he.json"
37TABLES = STAR / "feed-twin" / "backend" / "statemachines"
38ENGINE = STAR / "EngineDesign" / "configs" / "ethalox_doublet_7000N.yaml"
39CEA = STAR / "EngineDesign" / "output" / "cache" / "cea_cache_LOX_Ethanol_3D.npz"
40
41pytestmark = pytest.mark.skipif(
42 not (DRAWING.exists() and TABLES.is_dir() and ENGINE.exists() and CEA.exists()),
43 reason="helium drawing, tables, engine config or CEA table absent",
44)
45
46#: Restated fuel tank rating [psig]: above the 473.5 psig the tanks lock up at
47#: off a full bottle (500 + 50 - 17 x 4.5), below the ~490 psig the regulator's
48#: supply effect carries them to over the burn; crossed at about a second.
49MAWP_PSIG = 480.0
50
51PLAN = BurnPlan(
52 tank_psi=473.5, loads={"OXT": 6.0, "FUT": 4.0}, settle=False, lead_in_s=0.1,
53 horizon_s=2.0,
54) # fmt: skip
55
56
57def _session(mawp_psig: float | None): # type: ignore[no-untyped-def]
58 import yaml
59
60 from feedtwin.engine.importer import engine_from_config
61
62 payload = copy.deepcopy(json.loads(DRAWING.read_text()))
63 if mawp_psig is not None:
64 fuel = next(n for n in payload["nodes"] if n["id"] == "FUT")
65 fuel["data"]["params"]["MAWP"] = {
66 "value": mawp_psig,
67 "unit": "psi",
68 "source": "estimated",
69 "reference": "test: restated so the regulator's climb trips it",
70 }
71 diagram = read_diagram(payload, name="copv_study_he_trip")
72 design = engine_from_config(yaml.safe_load(ENGINE.read_text()), name="7000N")
73 model = assemble_model(
74 diagram, diagram_id="copv_study_he_trip", engine=design, cea_cache=str(CEA)
75 )
76 return open_session(model, load_machine(tables=TABLES), setup=burn_setup())
77
78
80 session = _session(MAWP_PSIG)
81 trace = run_burn(session, PLAN)
82 end = trace.end
83 assert end is not None and end.tripped is not None
84 trip = end.tripped
85 assert trip.vessel == "FUT" and trip.kind == "tank"
86 limit = from_psig(MAWP_PSIG)
87 assert trip.limit == pytest.approx(limit, rel=1e-12)
88 assert trip.pressure > limit
89 # Over by no more than one step's climb (a psi a second or so).
90 assert trip.pressure - limit < 0.5 * PSI
91 # Mid-burn: after ignition, and long before the horizon.
92 assert 0.0 < trip.t < 1.5
93 # The trace stops on the step that tripped -- not at the horizon.
94 assert trace.t[-1] == pytest.approx(trip.t, abs=PLAN.dt + 1e-9)
95 assert trace.t[-1] < PLAN.horizon_s - 0.4
96 assert len(trace.t) == end.steps
97 assert end.depleted_s is None and end.tank == ""
98 # The last sample is the frame the stand failed on: the tank over its rating.
99 assert trace.tank["FUT"]["pressure_Pa"][-1] == pytest.approx(trip.pressure)
100 assert all(p <= limit for p in trace.tank["FUT"]["pressure_Pa"][:-1])
101
102 record = trip_record(trace)
103 assert record is not None
104 assert record["vessel"] == "FUT"
105 assert record["t"] == trip.t
106 assert record["mawp_psia"] == pytest.approx((MAWP_PSIG * PSI + ATMOSPHERE) / PSI)
107 assert record["p_psia"] > record["mawp_psia"]
108 assert "MAWP" in record["message"]
109 # Said in psia, from the same numbers (ADR 0007): the trip text a person
110 # reads, not the gauge the wire carries.
111 assert say(trip.pressure, 1) in record["message"], record["message"]
112 assert say(trip.limit, 1) in record["message"], record["message"]
113 assert "psig" not in record["message"]
114 # And the notes say it, so a reader of the notes alone is not misled.
115 assert any(note.startswith("Burn stopped at t = ") for note in trace.notes)
116
117
119 session = _session(None)
120 trace = run_burn(session, plan_with(PLAN, horizon_s=0.3))
121 assert trace.end is not None and trace.end.tripped is None
122 assert trip_record(trace) is None
123 assert trace.t[-1] > 0.3 - 1e-9
124 assert not any(note.startswith("Burn stopped") for note in trace.notes)
125
126
128 """Restated below the lockup the settle presses to: the stand trips before
129 T-0, the settle reports it, and the burn records no steps."""
130 session = _session(400.0)
131 trace = run_burn(session, plan_with(PLAN, settle=True, settle_max_s=3.0))
132 assert trace.t0_settled is False
133 assert trace.end is not None and trace.end.tripped is not None
134 assert trace.end.steps == 0 and trace.t == []
135 assert trace.end.tripped.t == pytest.approx(-PLAN.lead_in_s)
136 assert any("tripped during the settle" in note for note in trace.notes)
None test_a_trip_during_the_settle_burns_nothing()
_session(float|None mawp_psig)
None test_no_trip_no_record()
None test_a_mid_burn_trip_ends_the_trace_at_the_trip()