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()
174 cfg_path = Path(args.config)
175 if not cfg_path.is_absolute():
176 cfg_path = ROOT / cfg_path
178 cfg = load_config(str(cfg_path))
179 mid_mm2 = 0.5 * (float(args.throat_min_mm2) + float(args.throat_max_mm2))
181 if cfg.chamber_geometry
is not None:
182 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
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
194 req = cfg.design_requirements.model_dump()
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
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)
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
218 req[
"impinging_momentum_R_min"] = 0.93
219 req[
"impinging_momentum_R_max"] = 1.07
221 req[
"W_MOM"] = 1400.0
222 req[
"W_geom_ao_af_momentum"] = 6500.0
224 req[
"W_DP_O"] = 720.0
225 req[
"W_DP_F"] = 420.0
227 req[
"target_thrust"] = F_TARGET
228 req[
"optimal_of_ratio"] = MR_TARGET
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
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),
242 "Region B Layer‑1 (co‑opt pressures) — "
243 "At [{:.0f}, {:.0f}] mm² | P_O∈[{:.0f},{:.0f}] psi | P_F∈[{:.0f},{:.0f}] psi".
format(
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),
261 opt_cfg, results = run_layer1_optimization(
265 target_burn_time=float(req.get(
"target_burn_time", 6.0)),
266 tolerances={
"thrust": 0.10,
"apogee": 0.15},
267 pressure_config=pcfg,
269 layer1_max_iterations=int(args.layer1_max_iterations),
270 layer1_cma_restarts=int(args.layer1_cma_restarts),
273 perf = results.get(
"performance")
or {}
274 hist: List[Dict[str, Any]] = list(results.get(
"iteration_history")
or [])
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
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}°"
290 sorted_hist = sorted(hist, key=
lambda z: float(z.get(
"objective", 1e30)))
291 picked: List[Dict[str, Any]] = []
293 for h
in sorted_hist:
294 if not h.get(
"eval_success"):
296 if h.get(
"thrust")
is None or not np.isfinite(float(h[
"thrust"])):
303 if len(picked) >= int(args.history_pick):
306 cfg_eval_base = load_config(str(cfg_path))
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
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,
326 "objective": float(results.get(
"convergence_info", {}).get(
"final_change", float(
"nan"))),
327 "eval_success":
True,
330 rows_out: List[Dict[str, Any]] = []
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"])
336 if ev
is None or ev.get(
"error"):
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"]),
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"]),
367 rf = _row_from_hist(h_final, src=
"layer1_best")
372 row = _row_from_hist(h, src=
"history")
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"]))
378 def _row_key(r: Dict[str, Any]) -> Tuple[Any, ...]:
380 round(float(r[
"A_throat_mm2"]), 3),
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),
394 dedup: Dict[Tuple[Any, ...], Dict[str, Any]] = {}
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:
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"]),
410 out_path = Path(args.output_csv)
411 out_path.parent.mkdir(parents=
True, exist_ok=
True)
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)
417 w.writerows(rows_out)
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):
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}"
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}°"
433 print(f
"\nCSV: {out_path}")