STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
run_layer1_region_b_injector_geometry.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Region B Layer‑1: optimize impinging injector geometry with frozen tanks + throat band.
3
4Targets from Pareto Region B exploration:
5 - ``layer1_A_throat_mm2_min/max`` ≈ 2520–2600 mm²
6 - Frozen tank stagnation pressures ~644 psi LOX, ~409 psi fuel (mid of 407–411)
7 - ``max_fuel_tank_pressure_psi`` / pressure_config fuel cap ≤ 600 psi
8
9Optimizes impinging jet parameters (n_elements, d_jet, spacing, impingement angles) and throat
10within the band; L*, D_outer, expansion_ratio pinned from YAML frozen_parameters.
11
12Post-processes ``iteration_history``: re-evaluates diverse near-best candidates and ranks by a
13composite of mission error, ΔP/Pc band hinges (LOX [0.15,0.40], fuel [0.40,0.50]), and R hinge
14around [0.93, 1.07].
15
16Does **not** modify Cd or injector physics equations.
17
18Example::
19
20 python scripts/run_layer1_region_b_injector_geometry.py \\
21 --layer1-max-iterations 175 --layer1-cma-restarts 2 \\
22 --output-csv output/layer1_region_b_ranked.csv
23"""
24
25from __future__ import annotations
26
27import argparse
28import copy
29import csv
30import math
31import sys
32from pathlib import Path
33from typing import Any, Dict, List, Optional, Tuple
34
35import numpy as np
36
37ROOT = Path(__file__).resolve().parents[1]
38sys.path.insert(0, str(ROOT))
39
40from engine.core.runner import PintleEngineRunner # noqa: E402
41from engine.optimizer.injector_dp_penalty import ( # noqa: E402
42 injector_dp_ratios_from_eval_result,
43 stream_injector_dp_band_hinge_squared,
44)
45from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization # noqa: E402
46from engine.pipeline.io import load_config # noqa: E402
47
48PSI_TO_PA = 6894.76
49F_TARGET = 8000.0
50MR_TARGET = 3.5
51
52
53def _strip_none_fp(fp: Dict[str, Any]) -> Dict[str, Any]:
54 return {k: v for k, v in fp.items() if v is not None}
55
56
57def _apply_throat_mm2(cfg: Any, mm2: float) -> None:
58 cg = cfg.chamber_geometry
59 if cg is None:
60 raise ValueError("chamber_geometry required")
61 At = float(mm2) * 1.0e-6
62 eps = float(cg.expansion_ratio)
63 Ae = eps * At
64 cg.A_throat = At
65 cg.A_exit = Ae
66 cg.exit_diameter = 2.0 * math.sqrt(max(Ae, 1e-30) / math.pi)
67
68
69def _hist_signature(h: Dict[str, Any]) -> Tuple[Any, ...]:
70 return (
71 round(float(h["A_throat"]), 14),
72 int(h["n_elements_O"]),
73 int(h["n_elements_F"]),
74 round(float(h["d_jet_O"]), 8),
75 round(float(h["d_jet_F"]), 8),
76 round(float(h["spacing_O"]), 8),
77 round(float(h["spacing_F"]), 8),
78 round(float(h["impingement_angle_O"]), 4),
79 round(float(h["impingement_angle_F"]), 4),
80 )
81
82
83def _apply_hist_to_cfg(cfg: Any, h: Dict[str, Any]) -> None:
84 _apply_throat_mm2(cfg, float(h["A_throat"]) * 1e6)
85 ig = cfg.injector.geometry
86 ig.oxidizer.n_elements = int(h["n_elements_O"])
87 ig.oxidizer.d_jet = float(h["d_jet_O"])
88 ig.oxidizer.impingement_angle = float(h["impingement_angle_O"])
89 ig.oxidizer.spacing = float(h["spacing_O"])
90 ig.fuel.n_elements = int(h["n_elements_F"])
91 ig.fuel.d_jet = float(h["d_jet_F"])
92 ig.fuel.impingement_angle = float(h["impingement_angle_F"])
93 ig.fuel.spacing = float(h["spacing_F"])
94
95
97 cfg_template: Any,
98 h: Dict[str, Any],
99 *,
100 P_O_psi: float,
101 P_F_psi: float,
102) -> Optional[Dict[str, Any]]:
103 cfg = copy.deepcopy(cfg_template)
104 _apply_hist_to_cfg(cfg, h)
105 try:
106 runner = PintleEngineRunner(cfg)
107 res = runner.evaluate(float(P_O_psi) * PSI_TO_PA, float(P_F_psi) * PSI_TO_PA, silent=True)
108 except Exception as e:
109 return {"error": str(e)}
110 pc = float(res.get("Pc", float("nan")))
111 if not math.isfinite(pc) or pc <= 0:
112 return None
113 ro, rf = injector_dp_ratios_from_eval_result(pc, res)
114 diag = res.get("diagnostics") if isinstance(res.get("diagnostics"), dict) else {}
115 R = diag.get("momentum_ratio_R")
116 Rv = float(R) if R is not None and np.isfinite(R) else float("nan")
117 Fo = float(res.get("F", float("nan")))
118 MRo = float(res.get("MR", float("nan")))
119 mission_err = (
120 math.hypot(Fo / F_TARGET - 1.0, MRo / MR_TARGET - 1.0)
121 if math.isfinite(Fo) and math.isfinite(MRo)
122 else float("nan")
123 )
124 r_o = float(ro) if ro is not None and np.isfinite(ro) else float("nan")
125 r_f = float(rf) if rf is not None and np.isfinite(rf) else float("nan")
126 dp_bal = (
127 stream_injector_dp_band_hinge_squared(r_o, 0.15, 0.40)
128 + stream_injector_dp_band_hinge_squared(r_f, 0.40, 0.50)
129 )
130 r_cost = stream_injector_dp_band_hinge_squared(Rv, 0.93, 1.07)
131 rank_score = mission_err + 0.45 * dp_bal + 0.35 * r_cost
132 return {
133 "F": Fo,
134 "MR": MRo,
135 "Pc_MPa": pc / 1e6,
136 "r_O": r_o,
137 "r_F": r_f,
138 "R": Rv,
139 "mission_err": mission_err,
140 "dp_bal": dp_bal,
141 "r_cost": r_cost,
142 "rank_score": rank_score,
143 "iter": int(h.get("iteration", -1)),
144 "hist_objective": float(h.get("objective", float("nan"))),
145 }
146
147
148def main() -> None:
149 ap = argparse.ArgumentParser(description=__doc__)
150 ap.add_argument("--config", type=str, default=str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
151 ap.add_argument("--layer1-max-iterations", type=int, default=220)
152 ap.add_argument("--layer1-cma-restarts", type=int, default=2)
153 ap.add_argument("--p-o-psi", type=float, default=644.0)
154 ap.add_argument("--p-f-psi", type=float, default=409.0)
155 ap.add_argument("--throat-min-mm2", type=float, default=2520.0)
156 ap.add_argument("--throat-max-mm2", type=float, default=2600.0)
157 ap.add_argument("--fuel-cap-psi", type=float, default=600.0)
158 ap.add_argument("--history-pick", type=int, default=55, help="Max unique hist rows to re-evaluate")
159 ap.add_argument("--output-csv", type=str, default=str(ROOT / "output/layer1_region_b_ranked.csv"))
160 args = ap.parse_args()
161
162 cfg_path = Path(args.config)
163 if not cfg_path.is_absolute():
164 cfg_path = ROOT / cfg_path
165
166 cfg = load_config(str(cfg_path))
167 mid_mm2 = 0.5 * (float(args.throat_min_mm2) + float(args.throat_max_mm2))
168 _apply_throat_mm2(cfg, mid_mm2)
169 if cfg.chamber_geometry is not None:
170 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
171
172 # Warm-start jets near a prior impinging LOX/CH4 optimum (8000 N family, Case B–class).
173 # Layer 1 recomputes chamber/nozzle from DOFs; ε_nom may clip at ``max_nozzle_exit``.
174 ig0 = cfg.injector.geometry
175 ig0.oxidizer.n_elements = 68
176 ig0.oxidizer.d_jet = 1.424e-3
177 ig0.oxidizer.spacing = 5.901e-3
178 ig0.oxidizer.impingement_angle = 63.15
179 ig0.fuel.n_elements = 32
180 ig0.fuel.d_jet = 1.392e-3
181 ig0.fuel.spacing = 4.118e-3
182 ig0.fuel.impingement_angle = 75.05
183
184 req = cfg.design_requirements.model_dump()
185 fp = _strip_none_fp(dict(req.get("frozen_parameters") or {}))
186 fp.pop("A_throat_mm2", None)
187 fp.pop("P_O_start_psi", None)
188 fp.pop("P_F_start_psi", None)
189 fp["P_O_start_psi"] = float(args.p_o_psi)
190 fp["P_F_start_psi"] = float(args.p_f_psi)
191 if cfg.chamber_geometry is not None:
192 fp["expansion_ratio"] = float(cfg.chamber_geometry.expansion_ratio)
193 fp["Lstar_mm"] = float(cfg.chamber_geometry.Lstar) * 1000.0
194 req["frozen_parameters"] = fp
195
196 req["layer1_A_throat_mm2_min"] = float(args.throat_min_mm2)
197 req["layer1_A_throat_mm2_max"] = float(args.throat_max_mm2)
198 req["max_fuel_tank_pressure_psi"] = float(args.fuel_cap_psi)
199
200 req["injector_dp_ratio_O_min"] = 0.15
201 req["injector_dp_ratio_O_max"] = 0.40
202 req["injector_dp_ratio_F_min"] = 0.40
203 req["injector_dp_ratio_F_max"] = 0.50
204
205 req["impinging_momentum_R_min"] = 0.93
206 req["impinging_momentum_R_max"] = 1.07
207
208 req["W_MOM"] = 1400.0
209 req["W_geom_ao_af_momentum"] = 6500.0
210 req["W_DP"] = 320.0
211 req["W_DP_O"] = 720.0
212 req["W_DP_F"] = 420.0
213
214 req["target_thrust"] = F_TARGET
215 req["optimal_of_ratio"] = MR_TARGET
216
217 # Region-B sweep: stability gates can pin the objective at BASE_INFEAS + W_INFEAS·score,
218 # masking thrust/O‑F trade-offs. Relax slightly so ΔP/R/MR terms steer geometry.
219 req["require_stable_state"] = False
220 req["min_stability_score"] = min(float(req.get("min_stability_score", 0.75)), 0.58)
221 req["min_stability_margin"] = min(float(req.get("min_stability_margin", 1.2)), 1.05)
222 # Let thrust/O‑F/ΔP penalties apply when only tiny stability/packing residuals remain (>0 but ≪1).
223 req["layer1_infeasibility_gate_eps"] = 2.0e-3
224
225 pcfg = {
226 "mode": "optimizer_controlled",
227 "max_lox_pressure_psi": float(req.get("max_lox_tank_pressure_psi", 700.0)),
228 "max_fuel_pressure_psi": float(args.fuel_cap_psi),
229 }
230
231 print("Region B Layer‑1 — throat band [{:.0f}, {:.0f}] mm²".format(args.throat_min_mm2, args.throat_max_mm2))
232 print(f"Frozen tanks: P_O={args.p_o_psi:.1f} psi, P_F={args.p_f_psi:.1f} psi")
233 print(f"Fuel cap={args.fuel_cap_psi:.0f} psi | ΔP bands O[0.15,0.40] F[0.40,0.50] | R hinge [0.93,1.07]")
234 print()
235
236 opt_cfg, results = run_layer1_optimization(
237 copy.deepcopy(cfg),
238 PintleEngineRunner(copy.deepcopy(cfg)),
239 req,
240 target_burn_time=float(req.get("target_burn_time", 6.0)),
241 tolerances={"thrust": 0.10, "apogee": 0.15},
242 pressure_config=pcfg,
243 layer1_smoke=False,
244 layer1_max_iterations=int(args.layer1_max_iterations),
245 layer1_cma_restarts=int(args.layer1_cma_restarts),
246 )
247
248 perf = results.get("performance") or {}
249 hist: List[Dict[str, Any]] = list(results.get("iteration_history") or [])
250
251 print("\n=== Layer‑1 reported optimum ===")
252 print(f"Thrust={perf.get('F')} N MR={perf.get('MR')} R={perf.get('momentum_ratio_R')}")
253 print(f"ΔP_O/Pc={perf.get('injector_dp_ratio_O')} ΔP_F/Pc={perf.get('injector_dp_ratio_F')}")
254 ig = opt_cfg.injector.geometry
255 print(
256 f"Geometry LOX n={ig.oxidizer.n_elements} d={ig.oxidizer.d_jet*1000:.3f}mm "
257 f"sp={ig.oxidizer.spacing*1000:.3f}mm θ={ig.oxidizer.impingement_angle:.1f}° | "
258 f"F n={ig.fuel.n_elements} d={ig.fuel.d_jet*1000:.3f}mm "
259 f"sp={ig.fuel.spacing*1000:.3f}mm θ={ig.fuel.impingement_angle:.1f}°"
260 )
261
262 sorted_hist = sorted(hist, key=lambda z: float(z.get("objective", 1e30)))
263 picked: List[Dict[str, Any]] = []
264 seen_sig = set()
265 for h in sorted_hist:
266 if not h.get("eval_success"):
267 continue
268 if h.get("thrust") is None or not np.isfinite(float(h["thrust"])):
269 continue
270 sig = _hist_signature(h)
271 if sig in seen_sig:
272 continue
273 seen_sig.add(sig)
274 picked.append(h)
275 if len(picked) >= int(args.history_pick):
276 break
277
278 cfg_eval_base = load_config(str(cfg_path))
279 og = opt_cfg.injector.geometry
280 oc = opt_cfg.chamber_geometry
281 assert oc is not None
282 h_final = {
283 "A_throat": float(oc.A_throat),
284 "n_elements_O": int(og.oxidizer.n_elements),
285 "d_jet_O": float(og.oxidizer.d_jet),
286 "impingement_angle_O": float(og.oxidizer.impingement_angle),
287 "spacing_O": float(og.oxidizer.spacing),
288 "n_elements_F": int(og.fuel.n_elements),
289 "d_jet_F": float(og.fuel.d_jet),
290 "impingement_angle_F": float(og.fuel.impingement_angle),
291 "spacing_F": float(og.fuel.spacing),
292 "iteration": -1,
293 "objective": float(results.get("convergence_info", {}).get("final_change", float("nan"))),
294 "eval_success": True,
295 }
296
297 rows_out: List[Dict[str, Any]] = []
298
299 def _row_from_hist(h: Dict[str, Any], *, src: str) -> Optional[Dict[str, Any]]:
300 ev = _evaluate_candidate(cfg_eval_base, h, P_O_psi=float(args.p_o_psi), P_F_psi=float(args.p_f_psi))
301 if ev is None or ev.get("error"):
302 return None
303 return {
304 "source": src,
305 "rank_score": float(ev["rank_score"]),
306 "mission_err": float(ev["mission_err"]),
307 "dp_bal": float(ev["dp_bal"]),
308 "r_cost": float(ev["r_cost"]),
309 "F_N": float(ev["F"]),
310 "MR": float(ev["MR"]),
311 "Pc_MPa": float(ev["Pc_MPa"]),
312 "dP_O_over_Pc": float(ev["r_O"]),
313 "dP_F_over_Pc": float(ev["r_F"]),
314 "R": float(ev["R"]),
315 "A_throat_mm2": float(h["A_throat"]) * 1e6,
316 "n_O": int(h["n_elements_O"]),
317 "n_F": int(h["n_elements_F"]),
318 "d_jet_O_mm": float(h["d_jet_O"]) * 1000,
319 "d_jet_F_mm": float(h["d_jet_F"]) * 1000,
320 "spacing_O_mm": float(h["spacing_O"]) * 1000,
321 "spacing_F_mm": float(h["spacing_F"]) * 1000,
322 "theta_O_deg": float(h["impingement_angle_O"]),
323 "theta_F_deg": float(h["impingement_angle_F"]),
324 "hist_iter": int(ev["iter"]),
325 "hist_objective": float(ev["hist_objective"]),
326 }
327
328 rf = _row_from_hist(h_final, src="layer1_best")
329 if rf:
330 rows_out.append(rf)
331
332 for h in picked:
333 row = _row_from_hist(h, src="history")
334 if row:
335 rows_out.append(row)
336
337 rows_out.sort(key=lambda r: r["rank_score"])
338
339 def _row_key(r: Dict[str, Any]) -> Tuple[Any, ...]:
340 return (
341 round(float(r["A_throat_mm2"]), 3),
342 int(r["n_O"]),
343 int(r["n_F"]),
344 round(float(r["d_jet_O_mm"]), 4),
345 round(float(r["d_jet_F_mm"]), 4),
346 round(float(r["spacing_O_mm"]), 3),
347 round(float(r["spacing_F_mm"]), 3),
348 round(float(r["theta_O_deg"]), 2),
349 round(float(r["theta_F_deg"]), 2),
350 r.get("source", ""),
351 )
352
353 dedup: Dict[Tuple[Any, ...], Dict[str, Any]] = {}
354 for r in rows_out:
355 k = _row_key(r)
356 if k not in dedup or float(r["rank_score"]) < float(dedup[k]["rank_score"]):
357 dedup[k] = r
358 rows_out = sorted(dedup.values(), key=lambda r: float(r["rank_score"]))
359
360 out_path = Path(args.output_csv)
361 out_path.parent.mkdir(parents=True, exist_ok=True)
362 if rows_out:
363 fieldnames = list(rows_out[0].keys())
364 with open(out_path, "w", newline="", encoding="utf-8") as f:
365 w = csv.DictWriter(f, fieldnames=fieldnames)
366 w.writeheader()
367 w.writerows(rows_out)
368
369 print("\n=== Top candidates (composite rank_score = mission_err + 0.45·dp_bal + 0.35·r_cost) ===")
370 print("(Lower rank_score is better; tanks replayed at {:.1f} / {:.1f} psi)".format(args.p_o_psi, args.p_f_psi))
371 for i, r in enumerate(rows_out[:12], start=1):
372 print(
373 f"{i:2d} score={r['rank_score']:.5f} m_err={r['mission_err']:.4f} "
374 f"dP_O={r['dP_O_over_Pc']:.3f} dP_F={r['dP_F_over_Pc']:.3f} R={r['R']:.3f} "
375 f"F={r['F_N']:.0f} MR={r['MR']:.3f} At={r['A_throat_mm2']:.1f} mm²"
376 )
377 print(
378 f" LOX n={r['n_O']} d={r['d_jet_O_mm']:.3f} sp={r['spacing_O_mm']:.3f} θ={r['theta_O_deg']:.1f}° | "
379 f"F n={r['n_F']} d={r['d_jet_F_mm']:.3f} sp={r['spacing_F_mm']:.3f} θ={r['theta_F_deg']:.1f}°"
380 )
381
382 print(f"\nCSV: {out_path}")
383
384
385if __name__ == "__main__":
386 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)