STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
run_layer1_region_b_coop_pressures.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Region B Layer‑1: impinging geometry + **co-optimized** tank pressures (narrow bands).
3
4- ``layer1_A_throat_mm2_min/max`` = 2520–2600 mm²
5- LOX tank search: ``layer1_P_O_start_psi_min/max`` default 580–700 psi
6- Fuel tank search: ``layer1_P_F_start_psi_min/max`` default 350–500 psi
7- ``pressure_config`` caps: max LOX 700 psi, max fuel 500 psi (for ratio guidance in Layer‑1)
8- ``layer1_infeasibility_gate_eps`` = 2e-3 (see ``layer1_static_optimization``)
9
10Post-process: re-evaluates history + best point; ranks by lexicographic priorities:
11
12 1. Thrust near 8000 N
13 2. MR near 3.5
14 3. R near 1
15 4. Fuel ΔP/Pc near [0.40, 0.50]
16 5. Minimize LOX ΔP/Pc (lower is better)
17
18Does **not** change Cd or injector physics.
19
20Example::
21
22 python scripts/run_layer1_region_b_coop_pressures.py \\
23 --output-csv output/layer1_region_b_coop_ranked.csv
24"""
25
26from __future__ import annotations
27
28import argparse
29import copy
30import csv
31import math
32import sys
33from pathlib import Path
34from typing import Any, Dict, List, Optional, Tuple
35
36import numpy as np
37
38ROOT = Path(__file__).resolve().parents[1]
39sys.path.insert(0, str(ROOT))
40
41from engine.core.runner import PintleEngineRunner # noqa: E402
42from engine.optimizer.injector_dp_penalty import ( # noqa: E402
43 injector_dp_ratios_from_eval_result,
44 stream_injector_dp_band_hinge_squared,
45)
46from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization # noqa: E402
47from engine.pipeline.io import load_config # noqa: E402
48
49PSI_TO_PA = 6894.76
50F_TARGET = 8000.0
51MR_TARGET = 3.5
52LARGE = 1.0e9
53
54
55def _strip_none_fp(fp: Dict[str, Any]) -> Dict[str, Any]:
56 return {k: v for k, v in fp.items() if v is not None}
57
58
59def _apply_throat_mm2(cfg: Any, mm2: float) -> None:
60 cg = cfg.chamber_geometry
61 if cg is None:
62 raise ValueError("chamber_geometry required")
63 At = float(mm2) * 1.0e-6
64 eps = float(cg.expansion_ratio)
65 Ae = eps * At
66 cg.A_throat = At
67 cg.A_exit = Ae
68 cg.exit_diameter = 2.0 * math.sqrt(max(Ae, 1e-30) / math.pi)
69
70
71def _hist_signature(h: Dict[str, Any]) -> Tuple[Any, ...]:
72 return (
73 round(float(h["A_throat"]), 14),
74 int(h["n_elements_O"]),
75 int(h["n_elements_F"]),
76 round(float(h["d_jet_O"]), 8),
77 round(float(h["d_jet_F"]), 8),
78 round(float(h["spacing_O"]), 8),
79 round(float(h["spacing_F"]), 8),
80 round(float(h["impingement_angle_O"]), 4),
81 round(float(h["impingement_angle_F"]), 4),
82 round(float(h.get("P_O_start_psi", -1.0)), 6),
83 round(float(h.get("P_F_start_psi", -1.0)), 6),
84 )
85
86
87def _apply_hist_to_cfg(cfg: Any, h: Dict[str, Any]) -> None:
88 _apply_throat_mm2(cfg, float(h["A_throat"]) * 1e6)
89 ig = cfg.injector.geometry
90 ig.oxidizer.n_elements = int(h["n_elements_O"])
91 ig.oxidizer.d_jet = float(h["d_jet_O"])
92 ig.oxidizer.impingement_angle = float(h["impingement_angle_O"])
93 ig.oxidizer.spacing = float(h["spacing_O"])
94 ig.fuel.n_elements = int(h["n_elements_F"])
95 ig.fuel.d_jet = float(h["d_jet_F"])
96 ig.fuel.impingement_angle = float(h["impingement_angle_F"])
97 ig.fuel.spacing = float(h["spacing_F"])
98
99
100def _prioritized_sort_key(ev: Dict[str, Any]) -> Tuple[float, float, float, float, float]:
101 """Lower tuple is better: thrust err, MR err, |R−1|, fuel ΔP hinge, LOX ΔP/Pc."""
102 Fo = float(ev.get("F", float("nan")))
103 MRo = float(ev.get("MR", float("nan")))
104 Rv = float(ev.get("R", float("nan")))
105 r_o = float(ev.get("r_O", float("nan")))
106 r_f = float(ev.get("r_F", float("nan")))
107 thrust_e = abs(Fo / F_TARGET - 1.0) if np.isfinite(Fo) else LARGE
108 mr_e = abs(MRo / MR_TARGET - 1.0) if np.isfinite(MRo) else LARGE
109 r_e = abs(Rv - 1.0) if np.isfinite(Rv) else LARGE
110 fuel_hinge = stream_injector_dp_band_hinge_squared(r_f, 0.40, 0.50)
111 fuel_hinge = float(fuel_hinge) if np.isfinite(fuel_hinge) else LARGE
112 lox_r = float(r_o) if (r_o is not None and np.isfinite(r_o)) else LARGE
113 return (thrust_e, mr_e, r_e, fuel_hinge, lox_r)
114
115
117 cfg_template: Any,
118 h: Dict[str, Any],
119 *,
120 P_O_psi: float,
121 P_F_psi: float,
122) -> Optional[Dict[str, Any]]:
123 cfg = copy.deepcopy(cfg_template)
124 _apply_hist_to_cfg(cfg, h)
125 try:
126 runner = PintleEngineRunner(cfg)
127 res = runner.evaluate(float(P_O_psi) * PSI_TO_PA, float(P_F_psi) * PSI_TO_PA, silent=True)
128 except Exception as e:
129 return {"error": str(e)}
130 pc = float(res.get("Pc", float("nan")))
131 if not math.isfinite(pc) or pc <= 0:
132 return None
133 ro, rf = injector_dp_ratios_from_eval_result(pc, res)
134 diag = res.get("diagnostics") if isinstance(res.get("diagnostics"), dict) else {}
135 R = diag.get("momentum_ratio_R")
136 Rv = float(R) if R is not None and np.isfinite(R) else float("nan")
137 Fo = float(res.get("F", float("nan")))
138 MRo = float(res.get("MR", float("nan")))
139 r_o = float(ro) if ro is not None and np.isfinite(ro) else float("nan")
140 r_f = float(rf) if rf is not None and np.isfinite(rf) else float("nan")
141 out = {
142 "F": Fo,
143 "MR": MRo,
144 "Pc_MPa": pc / 1e6,
145 "r_O": r_o,
146 "r_F": r_f,
147 "R": Rv,
148 "P_O_psi_replay": float(P_O_psi),
149 "P_F_psi_replay": float(P_F_psi),
150 "iter": int(h.get("iteration", -1)),
151 "hist_objective": float(h.get("objective", float("nan"))),
152 }
153 out["rank_key"] = _prioritized_sort_key(out)
154 return out
155
156
157def main() -> None:
158 ap = argparse.ArgumentParser(description=__doc__)
159 ap.add_argument("--config", type=str, default=str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
160 ap.add_argument("--layer1-max-iterations", type=int, default=500)
161 ap.add_argument("--layer1-cma-restarts", type=int, default=2)
162 ap.add_argument("--p-o-min-psi", type=float, default=580.0)
163 ap.add_argument("--p-o-max-psi", type=float, default=700.0)
164 ap.add_argument("--p-f-min-psi", type=float, default=350.0)
165 ap.add_argument("--p-f-max-psi", type=float, default=500.0)
166 ap.add_argument("--max-lox-pressure-psi", type=float, default=700.0)
167 ap.add_argument("--max-fuel-pressure-psi", type=float, default=500.0)
168 ap.add_argument("--throat-min-mm2", type=float, default=2520.0)
169 ap.add_argument("--throat-max-mm2", type=float, default=2600.0)
170 ap.add_argument("--history-pick", type=int, default=80)
171 ap.add_argument("--output-csv", type=str, default=str(ROOT / "output/layer1_region_b_coop_ranked.csv"))
172 args = ap.parse_args()
173
174 cfg_path = Path(args.config)
175 if not cfg_path.is_absolute():
176 cfg_path = ROOT / cfg_path
177
178 cfg = load_config(str(cfg_path))
179 mid_mm2 = 0.5 * (float(args.throat_min_mm2) + float(args.throat_max_mm2))
180 _apply_throat_mm2(cfg, mid_mm2)
181 if cfg.chamber_geometry is not None:
182 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
183
184 ig0 = cfg.injector.geometry
185 ig0.oxidizer.n_elements = 68
186 ig0.oxidizer.d_jet = 1.424e-3
187 ig0.oxidizer.spacing = 5.901e-3
188 ig0.oxidizer.impingement_angle = 63.15
189 ig0.fuel.n_elements = 32
190 ig0.fuel.d_jet = 1.392e-3
191 ig0.fuel.spacing = 4.118e-3
192 ig0.fuel.impingement_angle = 75.05
193
194 req = cfg.design_requirements.model_dump()
195 fp = _strip_none_fp(dict(req.get("frozen_parameters") or {}))
196 fp.pop("A_throat_mm2", None)
197 fp.pop("P_O_start_psi", None)
198 fp.pop("P_F_start_psi", None)
199 if cfg.chamber_geometry is not None:
200 fp["expansion_ratio"] = float(cfg.chamber_geometry.expansion_ratio)
201 fp["Lstar_mm"] = float(cfg.chamber_geometry.Lstar) * 1000.0
202 req["frozen_parameters"] = fp
203
204 req["layer1_A_throat_mm2_min"] = float(args.throat_min_mm2)
205 req["layer1_A_throat_mm2_max"] = float(args.throat_max_mm2)
206 req["layer1_P_O_start_psi_min"] = float(args.p_o_min_psi)
207 req["layer1_P_O_start_psi_max"] = float(args.p_o_max_psi)
208 req["layer1_P_F_start_psi_min"] = float(args.p_f_min_psi)
209 req["layer1_P_F_start_psi_max"] = float(args.p_f_max_psi)
210 req["max_fuel_tank_pressure_psi"] = float(args.max_fuel_pressure_psi)
211 req["max_lox_tank_pressure_psi"] = float(args.max_lox_pressure_psi)
212
213 req["injector_dp_ratio_O_min"] = 0.15
214 req["injector_dp_ratio_O_max"] = 0.40
215 req["injector_dp_ratio_F_min"] = 0.40
216 req["injector_dp_ratio_F_max"] = 0.50
217
218 req["impinging_momentum_R_min"] = 0.93
219 req["impinging_momentum_R_max"] = 1.07
220
221 req["W_MOM"] = 1400.0
222 req["W_geom_ao_af_momentum"] = 6500.0
223 req["W_DP"] = 320.0
224 req["W_DP_O"] = 720.0
225 req["W_DP_F"] = 420.0
226
227 req["target_thrust"] = F_TARGET
228 req["optimal_of_ratio"] = MR_TARGET
229
230 req["require_stable_state"] = False
231 req["min_stability_score"] = min(float(req.get("min_stability_score", 0.75)), 0.58)
232 req["min_stability_margin"] = min(float(req.get("min_stability_margin", 1.2)), 1.05)
233 req["layer1_infeasibility_gate_eps"] = 2.0e-3
234
235 pcfg = {
236 "mode": "optimizer_controlled",
237 "max_lox_pressure_psi": float(args.max_lox_pressure_psi),
238 "max_fuel_pressure_psi": float(args.max_fuel_pressure_psi),
239 }
240
241 print(
242 "Region B Layer‑1 (co‑opt pressures) — "
243 "At [{:.0f}, {:.0f}] mm² | P_O∈[{:.0f},{:.0f}] psi | P_F∈[{:.0f},{:.0f}] psi".format(
244 args.throat_min_mm2,
245 args.throat_max_mm2,
246 args.p_o_min_psi,
247 args.p_o_max_psi,
248 args.p_f_min_psi,
249 args.p_f_max_psi,
250 )
251 )
252 print(
253 "layer1_infeasibility_gate_eps={} | max_iter={} | cma_restarts={}".format(
254 req["layer1_infeasibility_gate_eps"],
255 int(args.layer1_max_iterations),
256 int(args.layer1_cma_restarts),
257 )
258 )
259 print()
260
261 opt_cfg, results = run_layer1_optimization(
262 copy.deepcopy(cfg),
263 PintleEngineRunner(copy.deepcopy(cfg)),
264 req,
265 target_burn_time=float(req.get("target_burn_time", 6.0)),
266 tolerances={"thrust": 0.10, "apogee": 0.15},
267 pressure_config=pcfg,
268 layer1_smoke=False,
269 layer1_max_iterations=int(args.layer1_max_iterations),
270 layer1_cma_restarts=int(args.layer1_cma_restarts),
271 )
272
273 perf = results.get("performance") or {}
274 hist: List[Dict[str, Any]] = list(results.get("iteration_history") or [])
275
276 lt = getattr(opt_cfg.lox_tank, "initial_pressure_psi", None)
277 ft = getattr(opt_cfg.fuel_tank, "initial_pressure_psi", None)
278 print("\n=== Layer‑1 reported optimum ===")
279 print(f"Tanks psi: P_O={lt} P_F={ft}")
280 print(f"Thrust={perf.get('F')} N MR={perf.get('MR')} R={perf.get('momentum_ratio_R')}")
281 print(f"ΔP_O/Pc={perf.get('injector_dp_ratio_O')} ΔP_F/Pc={perf.get('injector_dp_ratio_F')}")
282 ig = opt_cfg.injector.geometry
283 print(
284 f"Geometry LOX n={ig.oxidizer.n_elements} d={ig.oxidizer.d_jet*1000:.3f}mm "
285 f"sp={ig.oxidizer.spacing*1000:.3f}mm θ={ig.oxidizer.impingement_angle:.1f}° | "
286 f"F n={ig.fuel.n_elements} d={ig.fuel.d_jet*1000:.3f}mm "
287 f"sp={ig.fuel.spacing*1000:.3f}mm θ={ig.fuel.impingement_angle:.1f}°"
288 )
289
290 sorted_hist = sorted(hist, key=lambda z: float(z.get("objective", 1e30)))
291 picked: List[Dict[str, Any]] = []
292 seen_sig = set()
293 for h in sorted_hist:
294 if not h.get("eval_success"):
295 continue
296 if h.get("thrust") is None or not np.isfinite(float(h["thrust"])):
297 continue
298 sig = _hist_signature(h)
299 if sig in seen_sig:
300 continue
301 seen_sig.add(sig)
302 picked.append(h)
303 if len(picked) >= int(args.history_pick):
304 break
305
306 cfg_eval_base = load_config(str(cfg_path))
307
308 oc = opt_cfg.chamber_geometry
309 assert oc is not None
310 poh = float(lt) if lt is not None and np.isfinite(float(lt)) else 640.0
311 pfh = float(ft) if ft is not None and np.isfinite(float(ft)) else 400.0
312 og = opt_cfg.injector.geometry
313 h_final = {
314 "A_throat": float(oc.A_throat),
315 "n_elements_O": int(og.oxidizer.n_elements),
316 "d_jet_O": float(og.oxidizer.d_jet),
317 "impingement_angle_O": float(og.oxidizer.impingement_angle),
318 "spacing_O": float(og.oxidizer.spacing),
319 "n_elements_F": int(og.fuel.n_elements),
320 "d_jet_F": float(og.fuel.d_jet),
321 "impingement_angle_F": float(og.fuel.impingement_angle),
322 "spacing_F": float(og.fuel.spacing),
323 "P_O_start_psi": poh,
324 "P_F_start_psi": pfh,
325 "iteration": -1,
326 "objective": float(results.get("convergence_info", {}).get("final_change", float("nan"))),
327 "eval_success": True,
328 }
329
330 rows_out: List[Dict[str, Any]] = []
331
332 def _row_from_hist(hh: Dict[str, Any], *, src: str) -> Optional[Dict[str, Any]]:
333 po = float(hh["P_O_start_psi"])
334 pf = float(hh["P_F_start_psi"])
335 ev = _evaluate_candidate(cfg_eval_base, hh, P_O_psi=po, P_F_psi=pf)
336 if ev is None or ev.get("error"):
337 return None
338 rk = ev["rank_key"]
339 return {
340 "source": src,
341 "rank_thr_e": rk[0],
342 "rank_mr_e": rk[1],
343 "rank_R_e": rk[2],
344 "rank_fuel_dp_hinge": rk[3],
345 "rank_lox_dp_ratio": rk[4],
346 "F_N": float(ev["F"]),
347 "MR": float(ev["MR"]),
348 "Pc_MPa": float(ev["Pc_MPa"]),
349 "dP_O_over_Pc": float(ev["r_O"]),
350 "dP_F_over_Pc": float(ev["r_F"]),
351 "R": float(ev["R"]),
352 "P_O_psi": po,
353 "P_F_psi": pf,
354 "A_throat_mm2": float(hh["A_throat"]) * 1e6,
355 "n_O": int(hh["n_elements_O"]),
356 "n_F": int(hh["n_elements_F"]),
357 "d_jet_O_mm": float(hh["d_jet_O"]) * 1000,
358 "d_jet_F_mm": float(hh["d_jet_F"]) * 1000,
359 "spacing_O_mm": float(hh["spacing_O"]) * 1000,
360 "spacing_F_mm": float(hh["spacing_F"]) * 1000,
361 "theta_O_deg": float(hh["impingement_angle_O"]),
362 "theta_F_deg": float(hh["impingement_angle_F"]),
363 "hist_iter": int(ev["iter"]),
364 "hist_objective": float(ev["hist_objective"]),
365 }
366
367 rf = _row_from_hist(h_final, src="layer1_best")
368 if rf:
369 rows_out.append(rf)
370
371 for h in picked:
372 row = _row_from_hist(h, src="history")
373 if row:
374 rows_out.append(row)
375
376 rows_out.sort(key=lambda r: (r["rank_thr_e"], r["rank_mr_e"], r["rank_R_e"], r["rank_fuel_dp_hinge"], r["rank_lox_dp_ratio"]))
377
378 def _row_key(r: Dict[str, Any]) -> Tuple[Any, ...]:
379 return (
380 round(float(r["A_throat_mm2"]), 3),
381 int(r["n_O"]),
382 int(r["n_F"]),
383 round(float(r["d_jet_O_mm"]), 4),
384 round(float(r["d_jet_F_mm"]), 4),
385 round(float(r["spacing_O_mm"]), 3),
386 round(float(r["spacing_F_mm"]), 3),
387 round(float(r["theta_O_deg"]), 2),
388 round(float(r["theta_F_deg"]), 2),
389 round(float(r["P_O_psi"]), 4),
390 round(float(r["P_F_psi"]), 4),
391 r.get("source", ""),
392 )
393
394 dedup: Dict[Tuple[Any, ...], Dict[str, Any]] = {}
395 for r in rows_out:
396 k = _row_key(r)
397 if k not in dedup:
398 dedup[k] = r
399 else:
400 old = dedup[k]
401 old_key = (old["rank_thr_e"], old["rank_mr_e"], old["rank_R_e"], old["rank_fuel_dp_hinge"], old["rank_lox_dp_ratio"])
402 new_key = (r["rank_thr_e"], r["rank_mr_e"], r["rank_R_e"], r["rank_fuel_dp_hinge"], r["rank_lox_dp_ratio"])
403 if new_key < old_key:
404 dedup[k] = r
405 rows_out = sorted(
406 dedup.values(),
407 key=lambda r: (r["rank_thr_e"], r["rank_mr_e"], r["rank_R_e"], r["rank_fuel_dp_hinge"], r["rank_lox_dp_ratio"]),
408 )
409
410 out_path = Path(args.output_csv)
411 out_path.parent.mkdir(parents=True, exist_ok=True)
412 if rows_out:
413 fieldnames = list(rows_out[0].keys())
414 with open(out_path, "w", newline="", encoding="utf-8") as f:
415 w = csv.DictWriter(f, fieldnames=fieldnames)
416 w.writeheader()
417 w.writerows(rows_out)
418
419 print("\n=== Top candidates (lexicographic rank key) ===")
420 print("Order: thrust→MR→R→fuel ΔP band→min LOX ΔP/Pc | replay at stored P_O, P_F")
421 for i, r in enumerate(rows_out[:15], start=1):
422 print(
423 f"{i:2d} thr_e={r['rank_thr_e']:.4f} mr_e={r['rank_mr_e']:.4f} R_e={r['rank_R_e']:.4f} "
424 f"fuel_h={r['rank_fuel_dp_hinge']:.5f} dP_O={r['dP_O_over_Pc']:.3f} "
425 f"P_O={r['P_O_psi']:.1f} P_F={r['P_F_psi']:.1f} F={r['F_N']:.0f} MR={r['MR']:.3f} R={r['R']:.3f} "
426 f"dP_F={r['dP_F_over_Pc']:.3f} At={r['A_throat_mm2']:.1f}"
427 )
428 print(
429 f" LOX n={r['n_O']} d={r['d_jet_O_mm']:.3f} sp={r['spacing_O_mm']:.3f} θ={r['theta_O_deg']:.1f}° | "
430 f"F n={r['n_F']} d={r['d_jet_F_mm']:.3f} sp={r['spacing_F_mm']:.3f} θ={r['theta_F_deg']:.1f}°"
431 )
432
433 print(f"\nCSV: {out_path}")
434
435
436if __name__ == "__main__":
437 main()
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
static emmc_status_t format(emmc_log_t *log)
Definition emmc_log.c:324
Optional[Dict[str, Any]] _evaluate_candidate(Any cfg_template, Dict[str, Any] h, *, float P_O_psi, float P_F_psi)
Tuple[float, float, float, float, float] _prioritized_sort_key(Dict[str, Any] ev)