STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_layer1_search_and_signoff.py
Go to the documentation of this file.
1"""Layer 1 search mechanics and sign-off: the design that is scored is the design that is shipped.
2
3L1-01 validation never raises tank pressure or swaps the candidate to make a design "solve".
4L1-04 the derived throat is solved to tolerance, independent of where CMA proposed it.
5L1-05 sign-off re-checks every declared geometric limit at face value.
6L1-07 the block stage searches no derived DOF and uses per-coordinate scaling.
7L1-08 valley escape fires only on the infeasible plateau, not on a shaping-term floor.
8DEF-09 the chamber OD floor follows the smallest reachable throat, not the envelope cap.
9"""
10from __future__ import annotations
11
12import copy
13import math
14from pathlib import Path
15from unittest.mock import MagicMock, patch
16
17import numpy as np
18import pytest
19
21from engine.pipeline.io import load_config
22
23ROOT = Path(__file__).resolve().parents[1]
24PSI = 6894.757
25
26
28 """L1-04: two secant steps from the proposal left +0.4..0.9 % thrust depending on the start."""
29 from engine import accel
30 from engine.core.runner import PintleEngineRunner
31 cfg = load_config(str(ROOT / "configs" / "ethalox_6500N.yaml"))
32 runner = PintleEngineRunner(copy.deepcopy(cfg))
33 cg = cfg.chamber_geometry
34 wall = L1._layer1_total_wall_thickness_m(cfg)
35 P = 583.1 * PSI
36
37 def ev():
38 r = accel.evaluate(runner.config, runner.cea_cache, P, P, 94070.0)
39 if r is None:
40 try:
41 r = runner.evaluate(P, P, P_ambient=94070.0, silent=True)
42 except Exception:
43 r = None
44 return r
45
46 c = dict(layer1_derive_expansion_ratio=True, layer1_derive_throat_from_thrust=True,
47 layer1_derive_max_iters=getattr(L1, "_LAYER1_DERIVE_MAX_ITERS_DEFAULT", 2),
48 layer1_derive_thrust_tol_rel=1e-3, derive_eps_min=4.0, derive_eps_max=14.0,
49 derive_At_min=5e-4, derive_At_max=6e-3, P_ambient=94070.0, target_thrust=6500.0,
50 TOTAL_WALL_THICKNESS_M=wall, max_nozzle_exit=0.2032)
51 errs = []
52 for f0 in (0.8, 1.0, 1.3, 1.66):
53 x = np.array([cg.A_throat * f0, cg.Lstar, cg.expansion_ratio, cg.chamber_diameter + wall])
54 L1._layer1_apply_chamber_geometry_to_config(
55 runner.config, A_throat=x[0], Lstar=x[1], expansion_ratio=x[2],
56 D_chamber_outer=x[3], max_nozzle_exit=0.2032, wall_thickness_m=wall)
57 res = L1._layer1_solve_derived_geometry(x, runner.config, c, ev, ev())
58 errs.append(abs(res["F"] / 6500.0 - 1.0))
59 assert max(errs) <= 1.0e-3, errs
60
61
63 """L1-05: 24 doublets in a 127 mm bore is a 22.97 mm pitch against the 6500 N config's
64 22.5 mm limit; the 4.4e-4 squared miss hid under gate_eps and every seed said VALID."""
65 cfg = load_config(str(ROOT / "configs" / "ethalox_6500N.yaml"))
66 req = cfg.design_requirements.model_dump()
67 gates = L1._layer1_declared_limit_gates(cfg, req, {})
68 ok, msg = gates["element_pitch"]
69 pitch = math.sqrt(math.pi / 4.0 * 0.127 ** 2 / 24)
70 assert pitch == pytest.approx(0.02297, abs=1e-5)
71 assert not ok and "22.97 mm > limit 22.50 mm" in msg
72 assert gates["engine_length"][0] # 244 mm against 400 mm
73 assert gates["free_jet"][0] # 5-6 d against SP-8089's 7 d
74
75
77 """L1-01: the replay retried at 1.03-1.72x tank pressure, took the first that solved,
78 and stamped the design VALID with the un-raised pressures written to the YAML."""
79 real_cfg = load_config(str(ROOT / "configs" / "impinging_smoke.yaml"))
80 state = {"val": False, "nom": None}
81 base = {"F": 7000.0, "Pc": 2.5e6, "MR": 2.3, "Cf": 1.55, "Cf_actual": 1.55,
82 "mdot_O": 1.0, "mdot_F": 0.45, "stability_results": {}}
83
84 def evaluate(po, pf, **kw):
85 if state["val"]:
86 if state["nom"] is None:
87 state["nom"] = po
88 if po < 1.02 * state["nom"]:
89 raise ValueError("Supply < Demand at all Pc")
90 return dict(base)
91
92 engine_mock = MagicMock()
93 engine_mock.evaluate.side_effect = evaluate
94 x13 = [0.002, 1.0, 8.0, 0.14, 12.0, 0.002, 45.0, 0.012, 0.0022, 45.0, 0.011, 550.0, 650.0]
95
96 def _dc(obj, memo=None):
97 return real_cfg if obj is real_cfg else copy.deepcopy(obj)
98
99 def _rebuild(*a, **k):
100 state["val"] = True
101 return None
102
103 with patch("engine.optimizer.layers.layer1_static_optimization.copy.deepcopy", side_effect=_dc), \
104 patch("engine.optimizer.layers.layer1_static_optimization.PintleEngineRunner",
105 return_value=engine_mock), \
106 patch("engine.optimizer.layers.layer1_static_optimization._layer1_rebuild_final_config",
107 side_effect=_rebuild), \
108 patch("engine.optimizer.layers.layer1_static_optimization.cma.CMAEvolutionStrategy") as mcma, \
109 patch("engine.optimizer.layers.layer1_static_optimization.minimize") as mmin, \
110 patch("engine.optimizer.layers.layer1_static_optimization.ProcessPoolExecutor"):
111 mcma.return_value.stop.return_value = True
112 mcma.return_value.result.xbest = x13
113 mcma.return_value.result.fbest = 0.1
114 lb = MagicMock(); lb.x = np.array(x13); lb.fun = 0.05; lb.success = True
115 mmin.return_value = lb
116 req = real_cfg.design_requirements.model_dump()
117 _, results = L1.run_layer1_optimization(
118 config_obj=real_cfg, runner=MagicMock(), requirements=req, target_burn_time=10.0,
119 tolerances={"thrust": 0.1}, pressure_config={"mode": "optimizer_controlled"},
120 layer1_max_iterations=1, layer1_cma_restarts=1)
121 perf = results["performance"]
122 assert perf["layer1_validation_tank_pressure_scale"] == pytest.approx(1.03)
123 assert perf["pressure_candidate_valid"] is False
124 assert any("own tank pressures" in r for r in perf["failure_reasons"])
125
126
128 """L1-07: blocks ran isotropic CMA (sigma = 0.2 x median span) over coordinates whose
129 ranges differ by 1e5 and searched the solved throat, eps and fuel pitch freely."""
130 from engine.pipeline.config_schemas import HybridOptimizerConfig
131 calls = []
132
133 def fake_core(objective_fn, x0, sigma0, bounds, budget, popsize, **kw):
134 calls.append(dict(kw, x0=np.asarray(x0), sigma0=sigma0, n=len(bounds)))
135 return np.asarray(x0, dtype=float), float(objective_fn(np.asarray(x0, dtype=float))), 1
136
137 lo = np.array([1e-4, 0.8, 4.0, 0.1, 6, 5e-4, 20, 3e-3, 5e-4, 20, 3e-3, 300, 300], float)
138 hi = np.array([4e-3, 1.5, 14.0, 0.2, 30, 4e-3, 80, 3e-2, 4e-3, 80, 3e-2, 600, 600], float)
139 x0 = 0.5 * (lo + hi)
140 fixed = {0: x0[0], 2: x0[2], 10: x0[10]}
141 hc = HybridOptimizerConfig(num_blocks=3, cycles=2, per_block_budget_fraction=0.5,
142 refresh_every_pass=False, block_method="random")
143 with patch.object(L1, "run_cma_core", side_effect=fake_core):
144 L1.run_hybrid_optimization(lambda x: float(np.sum(((x - lo) / (hi - lo)) ** 2)),
145 list(zip(lo, hi)), x0, hc, total_budget=20000,
146 fixed_variables=fixed, integer_dims=[4, 6, 9], seed=3)
147 blocks = [c for c in calls if c.get("true_objective_fn") is not None]
148 assert blocks
149 for c in blocks:
150 assert c.get("cma_stds") is not None
151 assert len(c["cma_stds"]) == c["n"]
152 # derived dims never appear in a block: one cycle's blocks cover the 10 searched dims only
153 assert sum(c["n"] for c in blocks[:3]) == 13 - len(fixed)
154
155
157 """L1-08: 'best > 100/150/300' read the ~1970-point mass floor as stuck on every 6500 N
158 run (7 sigma kicks) and never fired on blank runs."""
159 tracker = {"function_evaluations": 2000, "last_best_eval": 1600, "best_objective": 2000.0,
160 "valley_escape_tier": 0, "cooldown_until": 0}
161 L1.run_cma_core(lambda x: 2000.0 + float(np.sum(x ** 2)), np.ones(3), 0.3,
162 [(-2.0, 2.0)] * 3, 64, 8, seed=1, valley_escape_tracker=tracker)
163 assert tracker["valley_escape_tier"] == 0
164 stuck = dict(tracker, best_objective=1.5e6)
165 L1.run_cma_core(lambda x: 1.5e6 + float(np.sum(x ** 2)), np.ones(3), 0.3,
166 [(-2.0, 2.0)] * 3, 64, 8, seed=1, valley_escape_tracker=stuck)
167 assert stuck["valley_escape_tier"] == 1
168
169
170def _captured_bounds(cfg, monkeypatch):
171 """Run Layer-1 setup up to the worker pool and return (bounds, constants)."""
172 from engine.core.runner import PintleEngineRunner
173 cap = {}
174
175 class _Stop(Exception):
176 pass
177
178 def spy(config_dict, bounds_array, requirements_dict, constants_dict, debug_strict):
179 cap.update(b=np.asarray(bounds_array), c=constants_dict)
180 raise _Stop()
181
182 monkeypatch.setenv("ED_L1_WORKERS", "1")
183 monkeypatch.setattr(L1, "_init_worker", spy)
184 req = cfg.design_requirements.model_dump()
185 with pytest.raises(_Stop):
186 L1.run_layer1_optimization(
187 copy.deepcopy(cfg), PintleEngineRunner(copy.deepcopy(cfg)), req,
188 float(req.get("target_burn_time") or 4.0), {"thrust": 0.1},
189 {"mode": "optimizer_controlled",
190 "max_lox_pressure_psi": req["max_lox_tank_pressure_psi"],
191 "max_fuel_pressure_psi": req["max_fuel_tank_pressure_psi"]})
192 return cap["b"], cap["c"]
193
194
196 """DEF-09: the OD floor was 0.5 x cap -- a 72.9 mm bore under a 1 kN engine whose
197 D/Dt band (2.2-3.2) wants a 41-60 mm bore."""
198 from engine.pipeline.config_schemas import PintleEngineConfig
199 cfg = load_config(str(ROOT / "configs" / "ethalox_6500N.yaml"))
200 dr = cfg.design_requirements
201 dr.target_thrust = 1000.0
202 dr.max_chamber_outer_diameter = 0.2032
203 dr.target_chamber_pressure_psi = None
204 dr.frozen_parameters = None
205 b, c = _captured_bounds(cfg, monkeypatch)
206 wall = float(c["TOTAL_WALL_THICKNESS_M"])
207 At_1kN = 1000.0 / (1.40 * 378.0 * PSI) # hand: F / (Cf Pc) at the blank point
208 assert b[3][0] - wall <= 2.2 * math.sqrt(4.0 * At_1kN / math.pi)
209
210
212 """Dropping the derived DOFs can leave a block with one coordinate; CMA_stds on a 1-D
213 problem raised IndexError inside cma (seen on configs/ethalox_6500N.yaml, seeds 3 and 4)."""
214 from engine.pipeline.config_schemas import HybridOptimizerConfig
215 lo, hi = np.zeros(4), np.array([1.0, 1.0, 100.0, 1e-3])
216 x0 = 0.5 * (lo + hi)
217 hc = HybridOptimizerConfig(num_blocks=2, cycles=1, per_block_budget_fraction=0.5,
218 refresh_every_pass=False, block_method="random")
219 x, f, n = L1.run_hybrid_optimization(
220 lambda v: float(np.sum(((v - 0.3 * hi) / hi) ** 2)), list(zip(lo, hi)), x0, hc,
221 total_budget=600, fixed_variables={0: x0[0], 1: x0[1]}, seed=5)
222 assert np.isfinite(f)
223
224
226 """Seeds ended at n = 5, 5, 7, 13 with objectives 20 % apart: at fixed jet diameters each
227 count is a different injector, so an integer CMA step never crossed between them. Scored
228 along constant flow area and pitch circle, every count is reachable in one batch."""
229 A0 = 5 * 4.0e-3 ** 2 # n d^2 of the start: 5 holes of 4 mm
230
231 def fake_eval(x):
232 x = np.asarray(x, dtype=float)
233 n, d, s = x[4], x[5], x[7]
234 area = n * d * d / A0 - 1.0
235 ring = n * s / (5 * 0.0071) - 1.0
236 return {"value": float((n - 12.0) ** 2 + 1e4 * area ** 2 + 1e4 * ring ** 2),
237 "success": True, "x_solved": x.tolist()}
238
239 lo = np.array([1e-4, 0.8, 4, 0.1, 5, 5e-4, 20, 2e-3, 5e-4, 20, 2e-3, 300, 300], float)
240 hi = np.array([4e-3, 1.5, 14, 0.2, 20.499, 6e-3, 80, 3e-2, 6e-3, 80, 5e-2, 600, 600], float)
241 x = np.array([1.7e-3, 0.76, 5.0, 0.13, 5.0, 4.0e-3, 39, 0.0071, 3.6e-3, 52, 0.047, 600, 574])
242 with patch.object(L1, "_eval_candidate", side_effect=fake_eval):
243 xb, fb, n_ev = L1._layer1_iso_area_count_scan(
244 x, fake_eval(x)["value"], list(zip(lo, hi)), L1._LocalSerialExecutor(), [4, 6, 9],
245 (4, (5, 8), (7, 10)))
246 assert int(round(xb[4])) == 12 and fb < 1e-6
247 assert xb[5] == pytest.approx(4.0e-3 * math.sqrt(5 / 12), rel=1e-12)
248 assert n_ev <= 2 * 15
249 # a count whose rescaled holes leave the search box is not proposed
250 lo[7] = 3e-3
251 with patch.object(L1, "_eval_candidate", side_effect=fake_eval):
252 xb2, _, _ = L1._layer1_iso_area_count_scan(
253 x, fake_eval(x)["value"], list(zip(lo, hi)), L1._LocalSerialExecutor(), [4, 6, 9],
254 (4, (5, 8), (7, 10)))
255 assert int(round(xb2[4])) == 11
#define max(a, b)
Definition Arduino.h:43