29from typing
import Any, Dict, List, Optional, Tuple
98 diag = perf.get(
"diagnostics")
or {}
99 pc = float(perf.get(
"Pc", float(
"nan")))
100 ro, rf =
_dp_ratios({
"Pc": pc,
"diagnostics": diag})
102 ig = opt_cfg.injector.geometry
103 n_o = int(ig.oxidizer.n_elements)
104 n_f = int(ig.fuel.n_elements)
105 d_o_mm = float(ig.oxidizer.d_jet) * 1000.0
106 d_f_mm = float(ig.fuel.d_jet) * 1000.0
107 s_o_mm = float(ig.oxidizer.spacing) * 1000.0
108 s_f_mm = float(ig.fuel.spacing) * 1000.0
110 a_go = perf.get(
"A_geom_O")
or diag.get(
"A_geom_O")
111 a_gf = perf.get(
"A_geom_F")
or diag.get(
"A_geom_F")
112 a_eo = perf.get(
"A_eff_O")
or diag.get(
"A_eff_O")
113 a_ef = perf.get(
"A_eff_F")
or diag.get(
"A_eff_F")
115 print(f
"\n{'=' * 80}\n{title}\n{'=' * 80}")
116 print(f
"Thrust F [N]: {perf.get('F')}")
117 print(f
"MR: {perf.get('MR')}")
118 print(f
"momentum_ratio_R: {perf.get('momentum_ratio_R')}")
119 print(f
"ΔP_inj_O / Pc: {ro}")
120 print(f
"ΔP_inj_F / Pc: {rf}")
121 print(f
"P_O_start_psi: {perf.get('P_O_start_psi')}")
122 print(f
"P_F_start_psi: {perf.get('P_F_start_psi')}")
123 print(f
"A_geom_O / A_geom_F: {perf.get('geom_ao_af')}")
124 print(f
"A_geom_O [m²]: {a_go}")
125 print(f
"A_geom_F [m²]: {a_gf}")
127 float(a_eo) / float(a_ef)
128 if a_eo
is not None and a_ef
is not None and float(a_ef) > 0
131 print(f
"A_eff_O / A_eff_F: {ao_af_eff}")
132 print(f
"expected_AO_AF_for_R≈1: {perf.get('expected_ao_af_for_R1')}")
134 f
"Injector: n_O={n_o} d_O={d_o_mm:.4f} mm spacing_O={s_o_mm:.4f} mm imp_O={ig.oxidizer.impingement_angle:.2f}°"
137 f
" n_F={n_f} d_F={d_f_mm:.4f} mm spacing_F={s_f_mm:.4f} mm imp_F={ig.fuel.impingement_angle:.2f}°"
142 opt_cfg: PintleEngineConfig,
150 f_band_o: Tuple[float, float],
152) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
153 geom = opt_cfg.injector.geometry
156 n_o =
max(1, int(ox.n_elements))
157 n_f =
max(1, int(fu.n_elements))
158 d_f = float(fu.d_jet)
161 pa = runner_base._get_ambient_pressure(
None)
163 ratios = np.linspace(ratio_min, ratio_max, steps)
164 rows: List[Dict[str, Any]] = []
165 print(f
"\n{'=' * 80}\nAO/AF sweep (fixed tank P, vary d_O)\n{'=' * 80}")
168 cfg_i = copy.deepcopy(opt_cfg)
169 g = cfg_i.injector.geometry
170 scale = math.sqrt(
max(1e-30, float(r_tgt) * float(n_f) / float(n_o)))
171 g.oxidizer.d_jet = d_f * scale
174 ev = runner.evaluate(po_pa, pf_pa, P_ambient=pa, silent=silent)
175 except Exception
as e:
176 rows.append({
"AoAf_tgt": r_tgt,
"error": str(e)})
179 diag = ev.get(
"diagnostics")
or {}
180 R = diag.get(
"momentum_ratio_R")
182 F = float(ev.get(
"F", float(
"nan")))
183 MR = float(ev.get(
"MR", float(
"nan")))
184 ok =
_feasible(F, MR, float(R)
if R
is not None else None, ro, rf, f_band_o=f_band_o, rf_cap=rf_cap)
187 "AoAf_tgt": float(r_tgt),
190 "R": float(R)
if R
is not None else float(
"nan"),
197 hdr = f
"{'AoAf':>8} {'F':>10} {'MR':>8} {'R':>8} {'dPO':>8} {'dPF':>8} {'ok':>5}"
199 print(
"-" * len(hdr))
202 print(f
"{row['AoAf_tgt']:8.4f} ERROR {row['error']}")
205 f
"{row['AoAf_tgt']:8.4f} {row['F']:10.2f} {row['MR']:8.4f} {row['R']:8.4f} "
206 f
"{row['dP_O_Pc'] if row['dP_O_Pc'] is not None else float('nan'):8.4f} "
207 f
"{row['dP_F_Pc'] if row['dP_F_Pc'] is not None else float('nan'):8.4f} "
208 f
"{str(row['feasible']):>5}"
211 good = [r
for r
in rows
if r.get(
"feasible")]
216 opt_cfg: PintleEngineConfig,
222 print(f
"\n{'=' * 80}\nCd sensitivity (×Cd_inf & Cd_min per stream; in-memory copy only)\n{'=' * 80}")
223 factors = [0.9, 1.0, 1.1]
225 pa = runner_ref._get_ambient_pressure(
None)
227 print(f
"{'f_O':>6} {'f_F':>6} {'R':>8} {'dPF/Pc':>10} {'MR':>8} {'F[N]':>10}")
234 ev = runner.evaluate(po_pa, pf_pa, P_ambient=pa, silent=silent)
235 except Exception
as e:
236 print(f
"{fo:6.2f} {ff:6.2f} ERROR {e}")
238 diag = ev.get(
"diagnostics")
or {}
239 R = diag.get(
"momentum_ratio_R")
242 f
"{fo:6.2f} {ff:6.2f} {float(R) if R is not None else float('nan'):8.4f} "
243 f
"{rf if rf is not None else float('nan'):10.4f} {float(ev.get('MR', float('nan'))):8.4f} "
244 f
"{float(ev.get('F', float('nan'))):10.2f}"
249 ap = argparse.ArgumentParser(description=__doc__)
250 ap.add_argument(
"--config", type=str, default=str(ROOT /
"configs/impinging_lox_ch4_8000N.yaml"))
251 ap.add_argument(
"--layer1-max-iterations", type=int, default=150)
252 ap.add_argument(
"--layer1-cma-restarts", type=int, default=1)
253 ap.add_argument(
"--max-fuel-pressure-psi-band-cap", type=float, default=900.0)
254 ap.add_argument(
"--injector-dp-f-max", type=float, default=1.50)
255 ap.add_argument(
"--w-mom", type=float, default=300.0)
256 ap.add_argument(
"--w-geom-ao-af", type=float, default=2800.0)
257 ap.add_argument(
"--aoaf-min", type=float, default=1.8)
258 ap.add_argument(
"--aoaf-max", type=float, default=2.5)
259 ap.add_argument(
"--aoaf-steps", type=int, default=22)
260 ap.add_argument(
"--silent-eval", action=
"store_true")
261 args = ap.parse_args()
263 cfg = load_config(args.config)
264 req = cfg.design_requirements.model_dump()
265 req[
"W_MOM"] = float(args.w_mom)
266 req[
"W_geom_ao_af_momentum"] = float(args.w_geom_ao_af)
267 req[
"injector_dp_ratio_F_max"] = float(args.injector_dp_f_max)
270 "mode":
"optimizer_controlled",
271 "max_lox_pressure_psi": float(req[
"max_lox_tank_pressure_psi"]),
273 "max_fuel_pressure_psi": float(args.max_fuel_pressure_psi_band_cap),
276 f_band_o = (float(req[
"injector_dp_ratio_O_min"]), float(req[
"injector_dp_ratio_O_max"]))
277 rf_cap = float(args.injector_dp_f_max)
280 "\nExploration overrides:\n"
281 f
" pressure_config[max_fuel_pressure_psi]={pcfg['max_fuel_pressure_psi']} "
282 f
"→ fuel P search [{pcfg['max_fuel_pressure_psi'] * 0.65:.1f}, {pcfg['max_fuel_pressure_psi'] * 0.85:.1f}] psi\n"
283 f
" injector_dp_ratio_F_max={rf_cap}\n"
284 f
" W_MOM={req['W_MOM']} W_geom_ao_af_momentum={req['W_geom_ao_af_momentum']}\n"
285 f
" Layer-1 iterations={args.layer1_max_iterations} restarts={args.layer1_cma_restarts}\n"
288 opt_cfg, results = run_layer1_optimization(
292 target_burn_time=float(req.get(
"target_burn_time", 6.0)),
293 tolerances={
"thrust": 0.10,
"apogee": 0.15},
294 pressure_config=pcfg,
296 layer1_max_iterations=int(args.layer1_max_iterations),
297 layer1_cma_restarts=int(args.layer1_cma_restarts),
299 perf = results.get(
"performance")
or {}
302 po_pa = float(perf[
"P_O_start_psi"]) * PSI_TO_PA
303 pf_pa = float(perf[
"P_F_start_psi"]) * PSI_TO_PA
309 ratio_min=float(args.aoaf_min),
310 ratio_max=float(args.aoaf_max),
311 steps=int(args.aoaf_steps),
312 silent=args.silent_eval,
317 _cd_sweep(opt_cfg, po_pa, pf_pa, silent=args.silent_eval)
319 print(f
"\n{'=' * 80}\nCONCLUSION / BEST CANDIDATES\n{'=' * 80}")
321 def _score(evdict: Dict[str, Any]) -> float:
322 F = float(evdict.get(
"F", 0))
323 MR = float(evdict.get(
"MR", 0))
324 R = float(evdict.get(
"R", 1.0))
325 ro = evdict.get(
"dP_O_Pc")
326 rf = evdict.get(
"dP_F_Pc")
328 pen += ((F - 8000) / 8000) ** 2
329 pen += ((MR - 3.5) / 3.5) ** 2
330 pen += (R - 1.0) ** 2
332 pen +=
max(0.0, 0.20 - ro) ** 2 +
max(0.0, ro - 0.35) ** 2
334 pen +=
max(0.0, rf - rf_cap) ** 2
338 best =
min(good, key=_score)
339 print(f
"AO/AF sweep: {len(good)} feasible point(s). Best composite row:\n {best}")
341 print(
"AO/AF sweep: **no** point satisfied all tightened feasibility filters.")
344 "\nInterpretation checklist:\n"
345 " (a) Cd asymmetry — compare Cd grid: if R and ΔP_F move strongly with f_O,f_F, Cd coupling matters.\n"
346 " (b) Fuel tank pressure lower bound — if lowering max_fuel_pressure_psi cap (narrower high-P bound)\n"
347 " shifts ΔP_F/Pc downward while holding MR/thrust, bound was limiting.\n"
348 " (c) Throat-fixed mdot — frozen A_throat ties mdot at Pc; cannot independently satisfy all jets without\n"
349 " compatible injector areas and ΔP split.\n"
350 " (d) AO/AF limits — if sweep never enters feasible set, geometry-pressure coupling dominates.\n"
351 " (e) Momentum coupling — R responds to √(ρ_O/ρ_F)·(v_O/v_F)² with MR tying mdots; fixing throat couples\n"
352 " mdot_sum leaving ΔP and areas to reconcile MR and R.\n"
355 print(
"\nBest geometry under exploration: **Layer-1 optimum above** (frozen throat + relaxed penalties).\n")
bool _feasible(float F, float MR, Optional[float] R, Optional[float] ro, Optional[float] rf, *, Tuple[float, float] f_band_o, float rf_cap)
Tuple[List[Dict[str, Any]], List[Dict[str, Any]]] _ao_af_sweep(PintleEngineConfig opt_cfg, float po_pa, float pf_pa, *, float ratio_min, float ratio_max, int steps, bool silent, Tuple[float, float] f_band_o, float rf_cap)