20from typing
import Any, Dict, Optional, Tuple
133def _print_case(name: str, perf: Dict[str, Any], opt_cfg: Any, *, r_band: Tuple[float, float]) ->
None:
135 cdo, cdf =
_cds(perf)
136 ig = opt_cfg.injector.geometry
137 cg = getattr(opt_cfg,
"chamber_geometry",
None)
138 at = getattr(cg,
"A_throat",
None)
if cg
is not None else None
139 at_mm2 = float(at) * 1e6
if at
is not None else float(
"nan")
141 ae_o = perf.get(
"A_eff_O")
or (perf.get(
"diagnostics")
or {}).get(
"A_eff_O")
142 ae_f = perf.get(
"A_eff_F")
or (perf.get(
"diagnostics")
or {}).get(
"A_eff_F")
143 aeff_ratio = float(ae_o) / float(ae_f)
if ae_o
and ae_f
and float(ae_f) > 0
else float(
"nan")
145 print(f
"\n{'=' * 72}\n{name}\n{'=' * 72}")
146 print(f
"A_throat [mm²]: {at_mm2:.4f}")
147 print(f
"Thrust [N]: {_sf(perf.get('F')):.4g}")
148 print(f
"MR: {_sf(perf.get('MR')):.6g}")
149 print(f
"momentum_ratio_R: {_sf(perf.get('momentum_ratio_R')):.6g} (optimizer band {r_band})")
150 print(f
"ΔP_inj_O/Pc: {ro:.6g}")
151 print(f
"ΔP_inj_F/Pc: {rf:.6g}")
152 print(f
"Cd_O / Cd_F: {cdo:.6g} / {cdf:.6g}")
153 print(f
"A_geom_O/A_geom_F: {perf.get('geom_ao_af')}")
154 print(f
"A_eff_O/A_eff_F: {aeff_ratio:.6g}")
155 print(f
"P_O_psi / P_F_psi: {perf.get('P_O_start_psi')} / {perf.get('P_F_start_psi')}")
157 f
"Injector LOX: n={int(ig.oxidizer.n_elements)} d_jet={ig.oxidizer.d_jet*1000:.4f} mm "
158 f
"spacing={ig.oxidizer.spacing*1000:.4f} mm angle={ig.oxidizer.impingement_angle:.2f}°"
161 f
"Injector F: n={int(ig.fuel.n_elements)} d_jet={ig.fuel.d_jet*1000:.4f} mm "
162 f
"spacing={ig.fuel.spacing*1000:.4f} mm angle={ig.fuel.impingement_angle:.2f}°"
168 pa = runner_ref._get_ambient_pressure(
None)
169 print(
"\n" +
"=" * 72)
170 print(
"PART 1 (continued) — ±10% Cd_O / Cd_F diagnostic (evaluate only)")
172 print(f
"{'f_O':>6} {'f_F':>6} {'R':>8} {'dP_F/Pc':>10} {'MR':>8} {'F[N]':>10}")
174 for fo
in (0.9, 1.0, 1.1):
175 for ff
in (0.9, 1.0, 1.1):
178 diag = ev.get(
"diagnostics")
or {}
179 R = diag.get(
"momentum_ratio_R")
180 pc = float(ev.get(
"Pc", float(
"nan")))
181 dpf = diag.get(
"delta_p_injector_F")
182 rf = float(dpf) / pc
if dpf
is not None and pc > 0
else float(
"nan")
184 f
"{fo:6.2f} {ff:6.2f} {_sf(R):8.4f} {rf:10.4f} {_sf(ev.get('MR')):8.4f} "
185 f
"{_sf(ev.get('F')):10.2f}"
190 ap = argparse.ArgumentParser(description=__doc__)
191 ap.add_argument(
"--config", type=str, default=str(ROOT /
"configs/impinging_lox_ch4_8000N.yaml"))
192 ap.add_argument(
"--a-throat-center-mm2", type=float, default=2443.263433372886)
193 ap.add_argument(
"--throat-span-fraction", type=float, default=0.125)
194 ap.add_argument(
"--layer1-max-iterations", type=int, default=150)
195 ap.add_argument(
"--layer1-cma-restarts", type=int, default=1)
196 args = ap.parse_args()
198 cfg_path = Path(args.config)
199 center = float(args.a_throat_center_mm2)
200 frac = float(args.throat_span_fraction)
201 dp_band = (0.15, 0.40)
206 (
"CASE A — R band [0.85, 1.15]", 0.85, 1.15),
207 (
"CASE B — R band [0.75, 1.25]", 0.75, 1.25),
210 results_store: Dict[str, Any] = {}
212 print(
"\n" +
"=" * 72)
213 print(
"PART 2–3 — Layer-1 with relaxed R hinge")
216 for title, r_lo, r_hi
in scenarios:
217 cfg = load_config(str(cfg_path))
218 req = cfg.design_requirements.model_dump()
221 req[
"layer1_A_throat_mm2_min"] = center * (1.0 - frac)
222 req[
"layer1_A_throat_mm2_max"] = center * (1.0 + frac)
223 req[
"injector_dp_ratio_O_min"] = dp_band[0]
224 req[
"injector_dp_ratio_O_max"] = dp_band[1]
225 req[
"injector_dp_ratio_F_min"] = dp_band[0]
226 req[
"injector_dp_ratio_F_max"] = dp_band[1]
228 req[
"W_geom_ao_af_momentum"] = 2800.0
229 req[
"impinging_momentum_R_min"] = float(r_lo)
230 req[
"impinging_momentum_R_max"] = float(r_hi)
233 "mode":
"optimizer_controlled",
234 "max_lox_pressure_psi": float(req[
"max_lox_tank_pressure_psi"]),
235 "max_fuel_pressure_psi": float(req[
"max_fuel_tank_pressure_psi"]),
238 opt_cfg, results = run_layer1_optimization(
242 target_burn_time=float(req.get(
"target_burn_time", 6.0)),
243 tolerances={
"thrust": 0.10,
"apogee": 0.15},
244 pressure_config=pcfg,
246 layer1_max_iterations=int(args.layer1_max_iterations),
247 layer1_cma_restarts=int(args.layer1_cma_restarts),
249 perf = results.get(
"performance")
or {}
250 key =
"A" if r_lo >= 0.85
else "B"
251 results_store[key] = {
"perf": perf,
"opt_cfg": opt_cfg,
"r_band": (r_lo, r_hi)}
253 _print_case(title, perf, opt_cfg, r_band=(r_lo, r_hi))
255 joint =
_mission_ok(perf, r_lo=r_lo, r_hi=r_hi, dp_lo=dp_band[0], dp_hi=dp_band[1])
256 print(f
"\n** Mission gates (F±5%, MR±5%, R∈[{r_lo},{r_hi}], both ΔP∈{dp_band}): "
257 f
"{'YES' if joint else 'NO'} **")
259 print(
"\n" +
"=" * 72)
260 print(
"Cd table — best candidates (Cases A & B)")
262 print(f
"{'case':>6} {'Cd_O':>8} {'Cd_F':>8}")
264 for key, lab
in (
"A",
"A"), (
"B",
"B"):
265 if key
not in results_store:
267 perf = results_store[key][
"perf"]
268 cdo, cdf =
_cds(perf)
269 print(f
"{lab:>6} {cdo:8.4f} {cdf:8.4f}")
272 if "B" in results_store:
273 perf_b = results_store[
"B"][
"perf"]
274 cfg_b = results_store[
"B"][
"opt_cfg"]
275 po = float(perf_b[
"P_O_start_psi"]) * PSI_TO_PA
276 pf = float(perf_b[
"P_F_start_psi"]) * PSI_TO_PA
279 print(
"\n" +
"=" * 72)
280 print(
"PART 4 — Dominant constraint (heuristic)")
283 "If ΔP_inj_F/Pc ≫ 0.40 while LOX sits in-band: **(b) fuel injector pressure drop** / "
284 "tank–Pc split dominates.\n"
285 "If throat + mdot coupling prevents lowering fuel ΔP without breaking thrust/MR: **(c)**.\n"
286 "Cd asymmetry **(a)**: check sensitivity grid — strong motion of R/MR/ΔP_F with f_O,f_F supports "
287 "**audit-only** asymmetry.\n"
288 "AO/AF limits **(d)** / momentum **(e)**: W_geom + R hinge guide geometry; relaxed R reduces **(e)** "
289 "pressure on the optimizer.\n"
292 best_key =
"B" if "B" in results_store
else "A"
293 bc = results_store[best_key][
"opt_cfg"]
294 ig = bc.injector.geometry
296 "Best candidate geometry (prefer Case B if present — otherwise A):\n"
297 f
" LOX n={int(ig.oxidizer.n_elements)} d={ig.oxidizer.d_jet*1000:.4f} mm "
298 f
"sp={ig.oxidizer.spacing*1000:.3f} mm θ={ig.oxidizer.impingement_angle:.2f}°\n"
299 f
" F n={int(ig.fuel.n_elements)} d={ig.fuel.d_jet*1000:.4f} mm "
300 f
"sp={ig.fuel.spacing*1000:.3f} mm θ={ig.fuel.impingement_angle:.2f}°"