1"""Set point (engine/layerx/setpoint.py): the dome, lockup and fill for a target mean thrust.
3The solve is checked against closed-form stands, where the root is known by hand, through the same
4``evaluate`` seam the burns go through. One real burn smoke test runs with ``LAYERX_SLOW=1``.
6Hand numbers used below:
8* Linear stand, F(L) = 7013 + 10.1 (L - 578) N (LE4's slope, docs/layerx/AUDIT.md 9.10 3.2):
9 F = 7200 N at L = 578 + 187/10.1 = 596.5149 psia. The secant is exact on a line, so two opening
10 burns and one Newton burn.
11* Coupled stand, the same thrust plus 0.015 N per psig of fill above 4500, and the bottle spare
12 S = F_fill + 14.7 - 6 L psi (a draw of 5 psi per psia of lockup, so 3000 psi at 600 psia):
13 S = 100 gives F_fill = 85.3 + 6 L; substituted, (10.1 + 0.09) L = 7200 - 7013 + 5837.8
14 + 0.015 * 4414.7 = 6091.0205, so L = 597.7449 psia and F_fill = 3671.77 psig.
17from __future__
import annotations
22from types
import SimpleNamespace
23from typing
import Any, Dict, List, Optional, Tuple
27sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
34def _stand(thrust, spare=None, *, peak_rise: float = 41.0, meop: Optional[float] =
None,
35 trip_above: Optional[float] =
None, calls: Optional[list] =
None):
36 """A closed-form stand behind the ``evaluate`` seam."""
38 def evaluate(points: List[Tuple[float, Optional[float], bool]]) -> List[Dict[str, Any]]:
40 for L, F, rp
in points:
42 calls.append((L, F, rp))
43 if trip_above
is not None and L > trip_above:
44 out.append({
"ok":
True,
"preflight": [],
"lockup_psia": L,
"fill_psig": F,
"replay": rp,
45 "tripped": {
"vessel":
"TK_FUEL",
"t": 2.1,
"p_psia": 764.7},
48 "figures": {
"mean_thrust_N": 7150.0,
"copv_spare_psi": 900.0},
"limits": []})
53 limits.append({
"key":
"tank_peak_meop.fuel",
"label":
"Fuel tank peak vs MEOP",
"value": peak,
54 "limit": meop + AMB,
"direction":
"max",
55 "grade":
"bad" if peak > meop + AMB
else "ok"})
56 figs = {
"mean_thrust_N": thrust(L, F),
"of_mean": 1.5212 - 3e-5 * (L - 578.0),
57 "copv_spare_psi": spare(L, F)
if spare
else 900.0,
"fuel_peak_psia": peak,
58 "total_impulse_Ns": thrust(L, F) * 3.45,
"burn_time_s": 3.45}
59 out.append({
"ok":
True,
"preflight": [],
"lockup_psia": L,
"fill_psig": F,
"replay": rp,
60 "dome_psig": L - 64.4 + 0.017 * (F - 4500.0),
"dome_per_1000psi_fill": 17.0,
61 "figures": figs,
"limits": limits,
"tripped":
None,
62 "derived": {
"stand_ids": {
"loader":
"PR_C",
"regulator":
"PR_D",
"bottle":
"KB1"},
63 "config_sha256":
"c" * 64,
"drawing": {
"id":
"d",
"name":
"he",
"sha256":
"s"}}})
69def linear(L: float, F: float) -> float:
70 return 7013.0 + 10.1 * (L - 578.0)
74 return linear(L, F) + 0.015 * (F - 4500.0)
78 return F + 14.7 - 6.0 * L
81CONFIG = SimpleNamespace(design_requirements=SimpleNamespace(target_thrust=7200.0, optimal_of_ratio=1.5),
82 lox_tank=SimpleNamespace(initial_pressure_psi=578.0))
83SETTINGS = SimpleNamespace(tank_pressure_psia=578.0, copv_pressure_psig=4500.0)
84STAND = {
"lockup_psia": 578.0,
"fill_psig": 4500.0,
"max_fill_psig": 4500.0,
"reference_thrust_N": 6751.0}
88 return sp.run_setpoint(CONFIG,
None, SETTINGS, [], sp.SetpointRequest(**req), evaluate=evaluate, stand=STAND)
96 assert out[
"converged"]
and out[
"burns"] == 3
97 assert out[
"settings_card"][
"lockup_psia"] == pytest.approx(578.0 + 187.0 / 10.1, abs=1e-6)
98 assert out[
"solution"][
"mean_thrust_N"] == pytest.approx(7200.0, abs=1e-6)
100 assert [round(h[
"asked"][
"lockup_psia"], 2)
for h
in out[
"history"][:2]] == [578.0, 616.44]
104 """F = 300 sqrt(L): F = 7200 N at L = (7200/300)^2 = 576 psia."""
105 out =
_run(
_stand(
lambda L, F: 300.0 * math.sqrt(L)), solve_fill=
False, thrust_tol_rel=1e-5)
106 assert out[
"converged"]
107 assert out[
"settings_card"][
"lockup_psia"] == pytest.approx(576.0, rel=2e-5)
108 assert out[
"burns"] <= 5
112 out =
_run(
_stand(coupled, coupled_spare), solve_fill=
True, margin_psi=100.0, margin_tol_psi=1.0, max_burns=10)
113 assert out[
"converged"]
and out[
"fill_status"] ==
"solved"
114 L, F = out[
"settings_card"][
"lockup_psia"], out[
"settings_card"][
"copv_fill_psig"]
117 assert L == pytest.approx(597.7449, abs=0.75)
118 assert F == pytest.approx(3671.77, abs=6.0)
119 assert abs(out[
"solution"][
"mean_thrust_N"] - 7200.0) <= 7.2
120 assert abs(out[
"solution"][
"copv_spare_psi"] - 100.0) <= 1.0
121 assert out[
"burns"] <= 7
125 """Near dropout the spare rises only 0.5 psi per psi of fill, not the 1.0 the first Jacobian
126 assumes, and the thrust-fill coupling is not the 0 it assumes. Stand: thrust as ``coupled``;
127 spare S = 944 + 0.5 (F - 4500) - 5.5 (L - 600.36) (AUDIT 9.10 3.2/3.4 numbers). With
128 u = L - 578, v = F - 4500 the root solves 10.1 u + 0.015 v = 187, 0.5 v - 5.5 u = -966.98:
129 v = -1702.48, u = 21.0433, i.e. L = 599.043 psia, F = 2797.5 psig. A fixed first Jacobian closes the
130 spare only by half each burn and runs out of burns; Broyden's update does not."""
132 return 944.0 + 0.5 * (F - 4500.0) - 5.5 * (L - 600.36)
134 out =
_run(
_stand(coupled, spare), solve_fill=
True, margin_psi=100.0, margin_tol_psi=1.0, max_burns=8)
135 assert out[
"converged"]
and out[
"fill_status"] ==
"solved"
136 assert out[
"settings_card"][
"lockup_psia"] == pytest.approx(599.043, abs=0.75)
137 assert out[
"settings_card"][
"copv_fill_psig"] == pytest.approx(2797.5, abs=4.0)
142 assert out[
"burns"] <= 5
146 """The fill band is one-sided. Stand: spare rises 1.02 psi per psi of fill, where the first
147 Jacobian assumes 1.0, so the first fill step overshoots low by 2 % of the distance: from 965 psi
148 at the full bottle, a two-sided +-20 psi band around 100 would accept the 82.7 psi it lands on --
149 a "solved" fill that the shared ``bottle_margin`` limit (100 psi) grades bad. Aiming at 110 +- 10
150 it lands at 92.9, is refused, and Broyden's update brings it into [100, 120]."""
152 return 944.0 + 1.02 * (F - 4500.0) - 5.5 * (L - 600.36)
154 out =
_run(
_stand(linear, spare), solve_fill=
True, margin_psi=100.0, margin_tol_psi=20.0, max_burns=8)
155 assert out[
"converged"]
and out[
"fill_status"] ==
"solved"
156 assert 100.0 <= out[
"solution"][
"copv_spare_psi"] <= 120.0
160 """A burn's flat figures, on the delivered basis where there is one. The bottle's spare is over the
161 higher tank's T-0 pressure (the shared ``bottle_margin``'s basis), not the lockup asked for:
162 706.38 - 597.93 = 108.45 psi (the builder's real LE4 burn: 108.57 over the 597.81 lockup)."""
163 res = {
"summary": {
"mean_thrust_N": 7158.0,
"peak_thrust_N": 7380.0,
"min_thrust_N": 6950.0,
164 "total_impulse_Ns": 24700.0,
"burn_time_s": 3.45,
"of_mean": 1.5212,
"copv_end_psia": 706.38,
165 "ox": {
"peak_psia": 634.5,
"t0_psia": 597.93,
"loaded_kg": 6.5,
"residual_kg": 0.0},
166 "fuel": {
"peak_psia": 635.1,
"t0_psia": 597.90,
"loaded_kg": 4.3,
"residual_kg": 0.06}},
167 "delivered": {
"summary": {
"mean_thrust_N": 7199.4,
"peak_thrust_N": 7420.0,
"min_thrust_N": 6990.0},
168 "thrust_N": [
None, 6980.0, 7000.0]}}
169 f = sp.figures(res, 597.81)
170 assert f[
"mean_thrust_N"] == 7199.4
and f[
"thrust_t0_N"] == 6980.0
and f[
"replayed"]
is True
171 assert f[
"thrust_spread_pct"] == pytest.approx((7420.0 - 6990.0) / 7199.4 * 100.0)
172 assert f[
"copv_spare_psi"] == pytest.approx(706.38 - 597.93)
173 assert f[
"fuel_used_kg"] == pytest.approx(4.24)
and f[
"ox_used_kg"] == pytest.approx(6.5)
175 bare = {
"summary": {**res[
"summary"],
"ox": {},
"fuel": {}}}
176 assert sp.figures(bare, 597.81)[
"copv_spare_psi"] == pytest.approx(706.38 - 597.81)
177 assert sp.figures(bare, 597.81)[
"replayed"]
is False
181 out =
_run(
_stand(linear,
lambda L, F: F + 14.7 - 7.9 * L), solve_fill=
True, margin_psi=100.0)
183 assert out[
"fill_status"] ==
"bottle_short"
184 assert out[
"settings_card"][
"copv_fill_psig"] == 4500.0
185 assert any(
"cannot keep the margin" in n
for n
in out[
"notes"])
189 """Peak = lockup + 41 psi; MEOP 585 psi + 13.64 = 598.64 psia, reached at 557.64 psia lockup,
190 where the line gives 7013 + 10.1 (557.64 - 578) = 6807.4 N."""
191 out =
_run(
_stand(linear, meop=585.0), solve_fill=
False)
192 assert out[
"converged"]
and not out[
"feasible"]
193 (b,) = out[
"binding"]
194 assert b[
"key"] ==
"tank_peak_meop.fuel"
195 assert b[
"lockup_psia_est"] == pytest.approx(557.64, abs=1e-6)
196 assert b[
"mean_thrust_N_est"] == pytest.approx(6807.36, abs=0.01)
200 """The stand trips above 610 psia: the opening guess (616.4) trips, the solve still finds 596.5."""
202 out =
_run(
_stand(linear, trip_above=610.0, calls=calls), solve_fill=
False)
203 assert out[
"converged"]
204 assert out[
"settings_card"][
"lockup_psia"] == pytest.approx(596.5149, abs=0.75)
205 assert not out[
"solution"].get(
"tripped")
209 """7400 N needs 616.3 psia on the line, but the stand trips above 605 psia. Each burn that trips
210 bounds the next step at halfway to it, so the solve closes on the highest usable lockup."""
211 out =
_run(
_stand(linear, trip_above=605.0), solve_fill=
False, target_thrust_N=7400.0)
212 assert not out[
"converged"]
213 assert 604.0 < out[
"settings_card"][
"lockup_psia"] <= 605.0
214 assert not out[
"solution"].get(
"tripped")
215 assert any(
"trips the stand" in n
for n
in out[
"notes"])
219 """The replay adds a constant +41 N (AUDIT 9.10 4a: 7013 vs 6972 N at 578 psia)."""
225 base =
_stand(thrust, calls=calls)
227 def evaluate(points):
229 for p, (_, _, rp)
in zip(out, points):
231 p[
"figures"][
"mean_thrust_N"] += 41.0
234 out =
_run(evaluate, solve_fill=
False, replay=
False, verify=
True)
235 assert [c[2]
for c
in calls][-2:] == [
True,
True]
236 assert out[
"solution"][
"mean_thrust_N"] == pytest.approx(7200.0, abs=1e-6)
237 assert out[
"settings_card"][
"lockup_psia"] == pytest.approx(578.0 + (187.0 - 41.0) / 10.1, abs=1e-6)
247 card = out[
"settings_card"]
248 assert card[
"dome_psig"] == pytest.approx(card[
"lockup_psia"] - 64.4)
249 assert card[
"fuel_lead"].startswith(
"not modelled")
250 cl = out[
"change_list"]
251 assert cl[
"schema"] == diff.SCHEMA
and cl[
"tool"] ==
"setpoint"
252 by = {c[
"target"]: c
for c
in cl[
"changes"]}
253 assert set(by) == {
"op:dome_psig",
"op:lockup_psia"}
254 for c
in cl[
"changes"]:
255 assert c[
"cad_impact"] ==
"setting only" and c[
"domain"] ==
"operation" and c[
"provenance"].startswith(
"solved")
256 assert set(c) >= {
"component",
"pid_node_id",
"field",
"before",
"after",
"unit",
"provenance",
"effect",
"cad_impact"}
257 assert by[
"op:lockup_psia"][
"pid_node_id"] ==
"PR_D" and by[
"op:dome_psig"][
"pid_node_id"] ==
"PR_C"
258 assert by[
"op:lockup_psia"][
"effect"][
"mean_thrust_N"] == pytest.approx(187.0, abs=1e-6)
260 assert cl[
"exports"][
"settings_patch"] == {
"tank_pressure_psia": pytest.approx(596.51, abs=0.01),
"dome_psia":
None}
262 assert m[
"name"]
and "Broyden" in m[
"source"]
and m[
"assumptions"]
263 for name, v
in m[
"inputs"].items():
264 assert set(v) == {
"value",
"unit",
"provenance"}, name
265 assert out[
"of"][
"settable_here"]
is False and out[
"of"][
"offset_rel"] == pytest.approx(
266 out[
"solution"][
"of_mean"] / 1.5 - 1.0)
267 assert out[
"unmeasured"]
and out[
"summary"][
"lockup_psia"] == card[
"lockup_psia"]
271 assert sp.target_thrust(CONFIG, sp.SetpointRequest())[0] == 7200.0
272 assert sp.target_thrust(CONFIG, sp.SetpointRequest(target_thrust_N=6800.0)) == (6800.0,
"request")
273 with pytest.raises(ValueError, match=
"target thrust"):
274 sp.target_thrust(SimpleNamespace(design_requirements=
None), sp.SetpointRequest())
281 """Peaks 642.0 (LOX) / 642.5 (fuel) psia at 13.64 psia ambient: the tanks trip at 1000 psia (``tank_mawp_psi``
282 is absolute, prepare.vessel_trip; it is held as it is, not with the site's atmosphere added), ok;
283 the design cap 600 -> warn (AUDIT D11); fuel MEOP 625 psi -> 638.64 psia, bad by 3.86 psi."""
284 pytest.importorskip(
"feedtwin")
285 res = {
"summary": {
"ox": {
"peak_psia": 642.0,
"stiffness_min": 0.37,
"min_psia": 600.0,
"residual_kg": 0.0},
286 "fuel": {
"peak_psia": 642.5,
"stiffness_min": 0.36,
"min_psia": 600.0,
"residual_kg": 0.07},
287 "copv_end_psia": 1544.4,
"total_impulse_Ns": 24000.0,
"impulse_to_depletion_Ns": 24000.0,
290 prep = SimpleNamespace(derived={
"target_lockup_psia": 600.36,
"ambient_pa": AMB * sp.PSI,
291 "tank_mawp_psi": {
"TK_LOX": 1000.0,
"TK_FUEL": 1000.0},
292 "stiffness_band": {
"oxidiser": [0.2, 0.4],
"fuel": [0.2, 0.4]}},
293 roles={
"oxidiser":
"TK_LOX",
"fuel":
"TK_FUEL"})
294 config = SimpleNamespace(design_requirements=SimpleNamespace(max_lox_tank_pressure_psi=600.0,
295 max_fuel_tank_pressure_psi=600.0))
296 rows = {r[
"key"]: r
for r
in sp._fallback_limits(res, prep, config, {
"margin_psi": 100.0})}
297 assert rows[
"dropout"][
"value"] == pytest.approx(1544.4 - 600.36)
and rows[
"dropout"][
"grade"] ==
"ok"
298 assert rows[
"tank_peak_mawp.fuel"][
"limit"] == pytest.approx(1000.0)
and rows[
"tank_peak_mawp.fuel"][
"grade"] ==
"ok"
299 assert rows[
"tank_peak_cap.oxidiser"][
"grade"] ==
"warn"
300 graded, basis = sp.grade_limits(res, prep, config, {
"margin_psi": 100.0,
"meop_psi": {
"fuel": 625.0}})
301 by = {r[
"key"]: r
for r
in graded}
302 assert by[
"tank_peak_meop.fuel"][
"limit"] == pytest.approx(638.64)
and by[
"tank_peak_meop.fuel"][
"grade"] ==
"bad"
303 assert by[
"vessel_trip"][
"grade"] ==
"ok"
304 assert not sp.feasible({
"ok":
True,
"figures": {
"mean_thrust_N": 1.0},
"limits": graded})
305 tripped, _ = sp.grade_limits({**res,
"tripped": {
"vessel":
"TK_FUEL"}}, prep, config, {
"margin_psi": 100.0})
306 assert next(r
for r
in tripped
if r[
"key"] ==
"vessel_trip")[
"grade"] ==
"bad"
310 """With engine/layerx/diag/limits.py present, the request's MEOP is handed to it (graded on the
311 contract's basis, psi across the wall) and the set point adds no second MEOP row of its own."""
316 def shared(result, prep=None, config=None, *, meop_psi=None):
317 seen[
"meop_psi"] = meop_psi
319 return [{
"key":
"tank_meop_fuel",
"label":
"Fuel tank peak vs MEOP",
"group":
"tanks",
"value": across,
320 "unit":
"psi",
"limit": 625.0,
"warn":
None,
"direction":
"max",
"grade":
"bad"}]
322 monkeypatch.setattr(limits,
"grade", shared)
323 prep = SimpleNamespace(derived={
"ambient_pa": AMB * sp.PSI})
324 rows, basis = sp.grade_limits({
"summary": {
"fuel": {
"peak_psia": 642.5}}}, prep,
None,
325 {
"margin_psi": 100.0,
"meop_psi": {
"fuel": 625.0}})
326 assert seen[
"meop_psi"] == {
"fuel": 625.0}
and basis ==
"engine.layerx.diag.limits.grade"
327 assert [r[
"key"]
for r
in rows
if "meop" in r[
"key"]] == [
"tank_meop_fuel"]
328 assert [r[
"key"]
for r
in rows
if r[
"key"] ==
"vessel_trip"] == [
"vessel_trip"]
335 from fastapi
import FastAPI
336 from fastapi.testclient
import TestClient
341 monkeypatch.setenv(
"USERDATA_DIR", str(tmp_path))
342 session = UserSession(f
"setpoint-{tmp_path.name}")
344 app.include_router(lx.router)
345 app.dependency_overrides[get_session] =
lambda: session
346 client = TestClient(app)
347 bad = client.post(
"/api/layerx/setpoint", json={
"settings": {
"drawing_id":
"d"},
"meop_psi": {
"lox": 750}})
348 assert bad.status_code == 422
349 r = client.post(
"/api/layerx/setpoint", json={
"settings": {
"drawing_id":
"d"}})
350 assert r.status_code
in (400, 404)
352 assert not [j
for j
in lx._JOBS.values()
if j.user == session.user]
358@pytest.mark.skipif(os.environ.get("LAYERX_SLOW") !=
"1", reason=
"burns: set LAYERX_SLOW=1")
360 """LE4 on the He drawing, on the pad, no replay (as-built throat): AUDIT 9.10 3.2 measured 6972 N at
361 578 psia and 7196 N at 600 psia. Target 7100 N: the answer lies between, ~589.5 psia."""
362 pytest.importorskip(
"feedtwin")
366 drawings = {d.name: d
for d
in DrawingStore(
None).list()}
367 if "copv_study_he" not in drawings:
368 pytest.skip(
"feed-twin's shipped drawings are not next to this checkout")
369 config = load_config(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
370 "configs",
"ethalox_6800N.yaml"))
371 settings = LayerXSettings(drawing_id=drawings[
"copv_study_he"].id, replay=
False, flight=
False)
372 out = sp.run_setpoint(config, drawings[
"copv_study_he"], settings, [],
373 sp.SetpointRequest(target_thrust_N=7100.0, solve_fill=
False, replay=
False, verify=
False,
374 thrust_tol_rel=5e-3, max_burns=3), workers=2)
375 assert out[
"burns"] <= 3
376 assert abs(out[
"solution"][
"mean_thrust_N"] / 7100.0 - 1.0) < 5e-3
377 assert 580.0 < out[
"settings_card"][
"lockup_psia"] < 600.0
378 assert out[
"settings_card"][
"dome_psig"]
is not None
379 assert out[
"limits"],
"every burn is graded"
test_smoke_the_real_stand_reaches_a_target_in_three_burns()
test_the_secant_lands_on_the_hand_root_of_a_linear_stand_in_three_burns()
test_an_accepted_fill_never_leaves_less_than_the_margin()
float coupled(float L, float F)
test_the_card_change_list_and_model_block()
test_the_fallback_grade_by_hand()
test_broyden_learns_the_fill_column_it_was_not_given()
test_the_target_defaults_to_the_design_and_refuses_none()
test_a_curved_stand_converges_to_its_closed_form_root()
_stand(thrust, spare=None, *, float peak_rise=41.0, Optional[float] meop=None, Optional[float] trip_above=None, Optional[list] calls=None)
test_a_bottle_too_small_for_the_margin_is_said_not_hidden()
test_the_route_refuses_before_a_slot_is_taken(tmp_path, monkeypatch)
float linear(float L, float F)
test_lockup_and_fill_together_land_on_the_hand_solution()
test_a_trip_is_never_a_root()
test_without_the_replay_the_answer_is_verified_with_it_and_corrected_once()
test_a_target_beyond_a_trip_homes_on_the_trip_and_says_so()
float coupled_spare(float L, float F)
test_the_shared_grade_gets_the_meop_and_it_is_graded_once(monkeypatch)
test_a_set_point_over_the_meop_is_infeasible_and_says_where_the_limit_binds()