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"])
118 max_feed_fraction: float,
120 P_tank_psi = float(P_equal_psi)
121 p_to = P_tank_psi * PSI_TO_PA
122 p_tf = P_tank_psi * PSI_TO_PA
124 row: Dict[str, Any] = {
125 "P_equal_psi": P_tank_psi,
126 "P_tank_O_psi": P_tank_psi,
127 "P_tank_F_psi": P_tank_psi,
128 "source_iter": int(h.get(
"iteration", -2)),
129 "accepted_strict":
False,
132 "P_back_over_Pc":
None,
135 cfg = load_config(str(cfg_template))
139 res = runner.evaluate(p_to, p_tf, silent=
True)
140 except Exception
as e:
141 row[
"reject_reason"] = f
"eval_exception:{e}"
144 pc_pa = float(res.get(
"Pc", float(
"nan")))
145 if not np.isfinite(pc_pa)
or pc_pa <= 0:
146 row[
"reject_reason"] =
"invalid_pc"
149 ci = res.get(
"chamber_intrinsics")
150 if isinstance(ci, dict):
151 row[
"is_choked"] = ci.get(
"is_choked")
152 row[
"P_back_over_Pc"] = ci.get(
"actual_pressure_ratio")
153 if ci.get(
"is_choked")
is False:
154 rej.append(
"nozzle_unchoked(P_back/Pc>P_crit)")
155 if ci.get(
"is_choked")
is None:
156 rej.append(
"missing_is_choked_flag")
158 rej.append(
"missing_chamber_intrinsics")
159 row[
"is_choked"] =
None
163 d_inj_O =
_inj_diag(res,
"delta_p_injector_O")
164 d_inj_F =
_inj_diag(res,
"delta_p_injector_F")
165 d_fd_O =
_inj_diag(res,
"delta_p_feed_O")
166 d_fd_F =
_inj_diag(res,
"delta_p_feed_F")
168 if None in (P_inj_O, P_inj_F, d_inj_O, d_inj_F, d_fd_O, d_fd_F):
169 row[
"reject_reason"] =
"missing_pressure_diagnostics"
172 if P_inj_O <= pc_pa + PC_MARGIN_PA:
173 rej.append(
"P_inj_O_le_Pc")
174 if P_inj_F <= pc_pa + PC_MARGIN_PA:
175 rej.append(
"P_inj_F_le_Pc")
177 d_feed_geom_O = p_to - P_inj_O
178 d_feed_geom_F = p_tf - P_inj_F
179 if d_fd_O < -1e-3
or d_fd_F < -1e-3:
180 rej.append(
"diag_negative_delta_p_feed")
181 if d_feed_geom_O < -1e-3:
182 rej.append(
"negative_feed_allocation_O")
183 if d_feed_geom_F < -1e-3:
184 rej.append(
"negative_feed_allocation_F")
186 dp_tot_O = p_to - pc_pa
187 dp_tot_F = p_tf - pc_pa
188 f_feed_O = (d_fd_O / dp_tot_O)
if dp_tot_O > 1000.0
else float(
"nan")
189 f_feed_F = (d_fd_F / dp_tot_F)
if dp_tot_F > 1000.0
else float(
"nan")
191 if np.isfinite(f_feed_O)
and f_feed_O > max_feed_fraction:
192 rej.append(f
"feed_loss_fraction_O>{max_feed_fraction:.2f}")
193 if np.isfinite(f_feed_F)
and f_feed_F > max_feed_fraction:
194 rej.append(f
"feed_loss_fraction_F>{max_feed_fraction:.2f}")
196 budget_res_O = dp_tot_O - d_fd_O - d_inj_O
197 budget_res_F = dp_tot_F - d_fd_F - d_inj_F
199 ratio_o = (d_inj_O / pc_pa)
if pc_pa > 0
else float(
"nan")
200 ratio_f = (d_inj_F / pc_pa)
if pc_pa > 0
else float(
"nan")
201 ro, rf = injector_dp_ratios_from_eval_result(pc_pa, res)
203 diag = res.get(
"diagnostics")
if isinstance(res.get(
"diagnostics"), dict)
else {}
204 R = diag.get(
"momentum_ratio_R")
205 Rv = float(R)
if R
is not None and np.isfinite(R)
else float(
"nan")
206 Fo = float(res.get(
"F", float(
"nan")))
207 MRo = float(res.get(
"MR", float(
"nan")))
209 ig = cfg.injector.geometry
211 f
"LOX n={ig.oxidizer.n_elements} d={ig.oxidizer.d_jet*1000:.4f}mm "
212 f
"sp={ig.oxidizer.spacing*1000:.4f}mm θ={ig.oxidizer.impingement_angle:.2f}° | "
213 f
"F n={ig.fuel.n_elements} d={ig.fuel.d_jet*1000:.4f}mm "
214 f
"sp={ig.fuel.spacing*1000:.4f}mm θ={ig.fuel.impingement_angle:.2f}°"
217 tgt_ok_o = dp_inj_pc_lo <= ratio_o <= dp_inj_pc_hi
if np.isfinite(ratio_o)
else False
218 tgt_ok_f = dp_inj_pc_lo <= ratio_f <= dp_inj_pc_hi
if np.isfinite(ratio_f)
else False
219 mr_ok = abs(MRo / MR_TARGET - 1.0) <= 0.06
if np.isfinite(MRo)
else False
220 r_ok = abs(Rv - 1.0) <= 0.10
if np.isfinite(Rv)
else False
227 "Pc_psi": pc_pa * PA_TO_PSI,
229 "P_inj_O_psi": P_inj_O * PA_TO_PSI,
230 "P_inj_F_psi": P_inj_F * PA_TO_PSI,
231 "delta_p_inj_O_psi": d_inj_O * PA_TO_PSI,
232 "delta_p_inj_F_psi": d_inj_F * PA_TO_PSI,
233 "delta_p_feed_O_psi": d_fd_O * PA_TO_PSI,
234 "delta_p_feed_F_psi": d_fd_F * PA_TO_PSI,
235 "dP_inj_over_Pc_O": ratio_o,
236 "dP_inj_over_Pc_F": ratio_f,
237 "dP_inj_over_Pc_solver_O": float(ro)
if ro
is not None and np.isfinite(ro)
else None,
238 "dP_inj_over_Pc_solver_F": float(rf)
if rf
is not None and np.isfinite(rf)
else None,
239 "feed_loss_fraction_O": f_feed_O,
240 "feed_loss_fraction_F": f_feed_F,
241 "budget_residual_O_Pa": budget_res_O,
242 "budget_residual_F_Pa": budget_res_F,
243 "A_throat_mm2": float(h[
"A_throat"]) * 1e6,
244 "n_O": int(h[
"n_elements_O"]),
245 "n_F": int(h[
"n_elements_F"]),
246 "d_jet_O_mm": float(h[
"d_jet_O"]) * 1000,
247 "d_jet_F_mm": float(h[
"d_jet_F"]) * 1000,
248 "spacing_O_mm": float(h[
"spacing_O"]) * 1000,
249 "spacing_F_mm": float(h[
"spacing_F"]) * 1000,
250 "theta_O_deg": float(h[
"impingement_angle_O"]),
251 "theta_F_deg": float(h[
"impingement_angle_F"]),
252 "goal_dp_band_O": tgt_ok_o,
253 "goal_dp_band_F": tgt_ok_f,
254 "goal_mr_3p5_tol6pct": mr_ok,
255 "goal_R_unity_tol10pct": r_ok,
256 "goal_thrust_8k_tol10pct": abs(Fo / F_TARGET - 1.0) <= 0.10
if np.isfinite(Fo)
else False,
261 row[
"accepted_strict"] = len(rej) == 0
262 row[
"reject_reason"] =
";".join(rej)
if rej
else "ok"
264 mr_e = abs(MRo / MR_TARGET - 1.0)
if np.isfinite(MRo)
else 1e9
265 r_e = abs(Rv - 1.0)
if np.isfinite(Rv)
else 1e9
267 max(0.0, dp_inj_pc_lo - ratio_o, ratio_o - dp_inj_pc_hi) ** 2
268 +
max(0.0, dp_inj_pc_lo - ratio_f, ratio_f - dp_inj_pc_hi) ** 2
269 if (np.isfinite(ratio_o)
and np.isfinite(ratio_f))
272 thrust_e = abs(Fo / F_TARGET - 1.0)
if np.isfinite(Fo)
else 1e9
275 float(f_feed_O) - max_feed_fraction
if np.isfinite(f_feed_O)
else 0.0,
276 float(f_feed_F) - max_feed_fraction
if np.isfinite(f_feed_F)
else 0.0,
278 row[
"_sort_key"] = (thrust_e, mr_e, r_e, f_err, feed_pen)
287 throat_lo_mm2: float,
288 throat_hi_mm2: float,
289 layer1_max_iterations: int,
290 layer1_cma_restarts: int,
293 max_pressure_cap_margin_psi: float,
294 max_feed_fraction: float,
296) -> List[Dict[str, Any]]:
297 pe = float(P_equal_psi)
298 max_lox_cap = pe + float(max_pressure_cap_margin_psi)
299 max_fuel_cap = pe + float(max_pressure_cap_margin_psi)
301 cfg = load_config(str(cfg_path))
302 mid_mm2 = 0.5 * (float(throat_lo_mm2) + float(throat_hi_mm2))
304 if cfg.chamber_geometry
is not None:
305 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
307 ig0 = cfg.injector.geometry
308 ig0.oxidizer.n_elements = 68
309 ig0.oxidizer.d_jet = 1.424e-3
310 ig0.oxidizer.spacing = 5.901e-3
311 ig0.oxidizer.impingement_angle = 63.15
312 ig0.fuel.n_elements = 32
313 ig0.fuel.d_jet = 1.392e-3
314 ig0.fuel.spacing = 4.118e-3
315 ig0.fuel.impingement_angle = 75.05
317 req = cfg.design_requirements.model_dump()
319 fp.pop(
"A_throat_mm2",
None)
320 fp.pop(
"P_O_start_psi",
None)
321 fp.pop(
"P_F_start_psi",
None)
322 fp[
"P_O_start_psi"] = pe
323 fp[
"P_F_start_psi"] = pe
324 if cfg.chamber_geometry
is not None:
325 fp[
"expansion_ratio"] = float(cfg.chamber_geometry.expansion_ratio)
326 fp[
"Lstar_mm"] = float(cfg.chamber_geometry.Lstar) * 1000.0
327 req[
"frozen_parameters"] = fp
329 req[
"layer1_A_throat_mm2_min"] = float(throat_lo_mm2)
330 req[
"layer1_A_throat_mm2_max"] = float(throat_hi_mm2)
331 req[
"max_lox_tank_pressure_psi"] =
max(float(req.get(
"max_lox_tank_pressure_psi", 0.0)), max_lox_cap)
332 req[
"max_fuel_tank_pressure_psi"] =
max(float(req.get(
"max_fuel_tank_pressure_psi", 0.0)), max_fuel_cap)
334 req[
"injector_dp_ratio_O_min"] = dp_lo
335 req[
"injector_dp_ratio_O_max"] = dp_hi
336 req[
"injector_dp_ratio_F_min"] = dp_lo
337 req[
"injector_dp_ratio_F_max"] = dp_hi
339 req[
"impinging_momentum_R_min"] = 0.93
340 req[
"impinging_momentum_R_max"] = 1.07
342 req[
"W_MOM"] = 1600.0
343 req[
"W_geom_ao_af_momentum"] = 6500.0
345 req[
"W_DP_O"] = 900.0
346 req[
"W_DP_F"] = 900.0
348 req[
"target_thrust"] = F_TARGET
349 req[
"optimal_of_ratio"] = MR_TARGET
351 req[
"require_stable_state"] =
False
352 req[
"min_stability_score"] =
min(float(req.get(
"min_stability_score", 0.75)), 0.58)
353 req[
"min_stability_margin"] =
min(float(req.get(
"min_stability_margin", 1.2)), 1.05)
354 req[
"layer1_infeasibility_gate_eps"] = 2.0e-3
357 "mode":
"optimizer_controlled",
358 "max_lox_pressure_psi": float(req[
"max_lox_tank_pressure_psi"]),
359 "max_fuel_pressure_psi": float(req[
"max_fuel_tank_pressure_psi"]),
362 print(f
"\n--- P_equal = {pe:.1f} psi (Layer‑1 geometry+throat) ---")
363 _opt_cfg, results = run_layer1_optimization(
367 target_burn_time=float(req.get(
"target_burn_time", 6.0)),
368 tolerances={
"thrust": 0.10,
"apogee": 0.15},
369 pressure_config=pcfg,
371 layer1_max_iterations=int(layer1_max_iterations),
372 layer1_cma_restarts=int(layer1_cma_restarts),
375 hist = list(results.get(
"iteration_history")
or [])
376 oc = _opt_cfg.chamber_geometry
377 og = _opt_cfg.injector.geometry
378 assert oc
is not None and og
is not None
381 "A_throat": float(oc.A_throat),
382 "n_elements_O": int(og.oxidizer.n_elements),
383 "d_jet_O": float(og.oxidizer.d_jet),
384 "impingement_angle_O": float(og.oxidizer.impingement_angle),
385 "spacing_O": float(og.oxidizer.spacing),
386 "n_elements_F": int(og.fuel.n_elements),
387 "d_jet_F": float(og.fuel.d_jet),
388 "impingement_angle_F": float(og.fuel.impingement_angle),
389 "spacing_F": float(og.fuel.spacing),
391 "eval_success":
True,
392 "objective": float(results.get(
"convergence_info", {}).get(
"final_change", float(
"nan"))),
393 "thrust": (results.get(
"performance")
or {}).get(
"F"),
397 sorted_hist = sorted(hist, key=
lambda z: float(z.get(
"objective", 1e30)))
398 picked: List[Dict[str, Any]] = []
400 for h
in sorted_hist:
401 if not h.get(
"eval_success"):
403 if h.get(
"thrust")
is not None:
404 thrust_v = float(h[
"thrust"])
405 if not np.isfinite(thrust_v):
412 if len(picked) >= int(history_pick):
415 rows_out: List[Dict[str, Any]] = []
423 max_feed_fraction=max_feed_fraction,
427 accepted = [r
for r
in rows_out
if r[
"accepted_strict"]]
428 good = sorted([r
for r
in accepted
if isinstance(r.get(
"_sort_key"), tuple)], key=
lambda z: z[
"_sort_key"])
429 print(f
" Replay: {len(rows_out)} geometries | strictly accepted: {len(accepted)}")
433 f
" Best strictly-accepted sort: F={b['F_N']:.1f} N MR={b['MR']:.3f} R={b['R']:.3f} "
434 f
"Pc={b['Pc_psi']:.1f} psi dP/Pc O={b['dP_inj_over_Pc_O']:.3f} F={b['dP_inj_over_Pc_F']:.3f} "
435 f
"f_feed O={b['feed_loss_fraction_O']:.3f} F={b['feed_loss_fraction_F']:.3f} "
436 f
"choked={b.get('is_choked')}"
440 nf =
lambda r: (0
if r[
"accepted_strict"]
else len(r.get(
"reject_reason",
"").
split(
";")))
441 probe = sorted(rows_out, key=nf)[0]
442 print(f
" No strict accepts; example row: rejected={probe.get('reject_reason')}")
448 ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
449 ap.add_argument(
"--config", type=str, default=str(ROOT /
"configs/impinging_lox_ch4_8000N.yaml"))
454 default=[700.0, 800.0, 900.0],
455 help=
"Equal tank stagnation pressures [psi] to test (same for LOX and fuel)",
457 ap.add_argument(
"--throat-min-mm2", type=float, default=2520.0)
458 ap.add_argument(
"--throat-max-mm2", type=float, default=2600.0)
459 ap.add_argument(
"--layer1-max-iterations", type=int, default=280)
460 ap.add_argument(
"--layer1-cma-restarts", type=int, default=2)
461 ap.add_argument(
"--dp-inj-over-pc-min", type=float, default=0.15)
462 ap.add_argument(
"--dp-inj-over-pc-max", type=float, default=0.40)
463 ap.add_argument(
"--max-feed-fraction", type=float, default=0.60, help=
"Reject if f_feed>O or F exceeds this")
464 ap.add_argument(
"--history-pick", type=int, default=48)
465 ap.add_argument(
"--max-pressure-cap-margin-psi", type=float, default=100.0)
466 ap.add_argument(
"--output-csv", type=str, default=str(ROOT /
"output/study_equal_tank_pressure_feed_allocation.csv"))
467 args = ap.parse_args()
469 cfg_path = Path(args.config)
470 if not cfg_path.is_absolute():
471 cfg_path = ROOT / cfg_path
473 dp_lo = float(args.dp_inj_over_pc_min)
474 dp_hi = float(args.dp_inj_over_pc_max)
475 all_rows: List[Dict[str, Any]] = []
477 print(
"Equal tank pressure feed-allocation study")
478 print(f
" Levels [psi]: {args.pressure_psi}")
479 print(f
" Throat [{args.throat_min_mm2:.0f}, {args.throat_max_mm2:.0f}] mm²")
480 print(f
" ΔP_inj/Pc goal band [{dp_lo:.2f}, {dp_hi:.2f}] | max feed frac {args.max_feed_fraction:.2f}")
481 print(
" Strict reject: P_inj≤Pc | negative ΔP_feed | unchoked | f_feed too high")
483 for pe
in args.pressure_psi:
486 P_equal_psi=float(pe),
487 throat_lo_mm2=float(args.throat_min_mm2),
488 throat_hi_mm2=float(args.throat_max_mm2),
489 layer1_max_iterations=int(args.layer1_max_iterations),
490 layer1_cma_restarts=int(args.layer1_cma_restarts),
493 max_pressure_cap_margin_psi=float(args.max_pressure_cap_margin_psi),
494 max_feed_fraction=float(args.max_feed_fraction),
495 history_pick=int(args.history_pick),
497 all_rows.extend(rows)
499 out_path = Path(args.output_csv)
500 out_path.parent.mkdir(parents=
True, exist_ok=
True)
514 "delta_p_feed_O_psi",
515 "delta_p_feed_F_psi",
518 "feed_loss_fraction_O",
519 "feed_loss_fraction_F",
522 "goal_mr_3p5_tol6pct",
523 "goal_R_unity_tol10pct",
524 "goal_thrust_8k_tol10pct",
535 "budget_residual_O_Pa",
536 "budget_residual_F_Pa",
539 extras = sorted(k
for k
in all_rows[0].keys()
if k
not in preferred
and not str(k).startswith(
"_"))
540 fieldnames = [k
for k
in preferred
if k
in all_rows[0]] + extras
541 with open(out_path,
"w", newline=
"", encoding=
"utf-8")
as f:
542 w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction=
"ignore")
545 w.writerow({k: r.get(k)
for k
in fieldnames})
547 na = sum(1
for r
in all_rows
if r.get(
"accepted_strict"))
548 print(f
"\nWrote {out_path} (total rows={len(all_rows)}, strictly accepted={na})")
List[Dict[str, Any]] _run_one_pressure_level(*, Path cfg_path, float P_equal_psi, float throat_lo_mm2, float throat_hi_mm2, int layer1_max_iterations, int layer1_cma_restarts, float dp_lo, float dp_hi, float max_pressure_cap_margin_psi, float max_feed_fraction, int history_pick)