102) -> Optional[Dict[str, Any]]:
103 cfg = copy.deepcopy(cfg_template)
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:
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")))
120 math.hypot(Fo / F_TARGET - 1.0, MRo / MR_TARGET - 1.0)
121 if math.isfinite(Fo)
and math.isfinite(MRo)
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")
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)
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
139 "mission_err": mission_err,
142 "rank_score": rank_score,
143 "iter": int(h.get(
"iteration", -1)),
144 "hist_objective": float(h.get(
"objective", float(
"nan"))),
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()
162 cfg_path = Path(args.config)
163 if not cfg_path.is_absolute():
164 cfg_path = ROOT / cfg_path
166 cfg = load_config(str(cfg_path))
167 mid_mm2 = 0.5 * (float(args.throat_min_mm2) + float(args.throat_max_mm2))
169 if cfg.chamber_geometry
is not None:
170 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
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
184 req = cfg.design_requirements.model_dump()
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
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)
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
205 req[
"impinging_momentum_R_min"] = 0.93
206 req[
"impinging_momentum_R_max"] = 1.07
208 req[
"W_MOM"] = 1400.0
209 req[
"W_geom_ao_af_momentum"] = 6500.0
211 req[
"W_DP_O"] = 720.0
212 req[
"W_DP_F"] = 420.0
214 req[
"target_thrust"] = F_TARGET
215 req[
"optimal_of_ratio"] = MR_TARGET
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)
223 req[
"layer1_infeasibility_gate_eps"] = 2.0e-3
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),
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]")
236 opt_cfg, results = run_layer1_optimization(
240 target_burn_time=float(req.get(
"target_burn_time", 6.0)),
241 tolerances={
"thrust": 0.10,
"apogee": 0.15},
242 pressure_config=pcfg,
244 layer1_max_iterations=int(args.layer1_max_iterations),
245 layer1_cma_restarts=int(args.layer1_cma_restarts),
248 perf = results.get(
"performance")
or {}
249 hist: List[Dict[str, Any]] = list(results.get(
"iteration_history")
or [])
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
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}°"
262 sorted_hist = sorted(hist, key=
lambda z: float(z.get(
"objective", 1e30)))
263 picked: List[Dict[str, Any]] = []
265 for h
in sorted_hist:
266 if not h.get(
"eval_success"):
268 if h.get(
"thrust")
is None or not np.isfinite(float(h[
"thrust"])):
275 if len(picked) >= int(args.history_pick):
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
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),
293 "objective": float(results.get(
"convergence_info", {}).get(
"final_change", float(
"nan"))),
294 "eval_success":
True,
297 rows_out: List[Dict[str, Any]] = []
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"):
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"]),
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"]),
328 rf = _row_from_hist(h_final, src=
"layer1_best")
333 row = _row_from_hist(h, src=
"history")
337 rows_out.sort(key=
lambda r: r[
"rank_score"])
339 def _row_key(r: Dict[str, Any]) -> Tuple[Any, ...]:
341 round(float(r[
"A_throat_mm2"]), 3),
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),
353 dedup: Dict[Tuple[Any, ...], Dict[str, Any]] = {}
356 if k
not in dedup
or float(r[
"rank_score"]) < float(dedup[k][
"rank_score"]):
358 rows_out = sorted(dedup.values(), key=
lambda r: float(r[
"rank_score"]))
360 out_path = Path(args.output_csv)
361 out_path.parent.mkdir(parents=
True, exist_ok=
True)
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)
367 w.writerows(rows_out)
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):
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²"
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}°"
382 print(f
"\nCSV: {out_path}")