54 o_band: Tuple[float, float],
56) -> Tuple[bool, bool, bool, bool, bool]:
57 F = float(perf.get(
"F", float(
"nan")))
58 MR = float(perf.get(
"MR", float(
"nan")))
59 R = perf.get(
"momentum_ratio_R")
60 R = float(R)
if R
is not None else float(
"nan")
62 ok_f = abs(F - 8000.0) / 8000.0 <= 0.05
if math.isfinite(F)
else False
63 ok_mr = abs(MR - 3.5) / 3.5 <= 0.05
if math.isfinite(MR)
else False
64 ok_r = abs(R - 1.0) <= 0.10
if math.isfinite(R)
else False
65 ok_o = o_band[0] <= ro <= o_band[1]
if math.isfinite(ro)
else False
66 ok_ff = rf <= f_cap
if math.isfinite(rf)
else False
67 return ok_f, ok_mr, ok_r, ok_o, ok_ff
83 o_band: Tuple[float, float],
85) -> Tuple[int, float, float]:
86 """Sort ascending: fewer gate failures; lower LOX stress metric."""
87 ok_f, ok_mr, ok_r, ok_o, ok_ff =
_gates(perf, o_band=o_band, f_cap=f_cap)
88 fails = sum(
not x
for x
in (ok_f, ok_mr, ok_r, ok_o, ok_ff))
92 if not math.isfinite(ro):
93 return fails, 1e9, 1e9
96 lox_primary = 1000.0 + (lo_o - ro)
97 lox_secondary = lo_o - ro
99 lox_primary = 500.0 + (ro - hi_o)
100 lox_secondary = ro - hi_o
102 lox_primary = hi_o - ro
103 lox_secondary = abs(ro - hi_o)
105 return fails, lox_primary, lox_secondary
109 ap = argparse.ArgumentParser(description=__doc__)
110 ap.add_argument(
"--config", type=str, default=str(ROOT /
"configs/impinging_lox_ch4_8000N.yaml"))
111 ap.add_argument(
"--a-throat-center-mm2", type=float, default=2443.263433372886)
112 ap.add_argument(
"--layer1-max-iterations", type=int, default=110)
113 ap.add_argument(
"--layer1-cma-restarts", type=int, default=1)
114 ap.add_argument(
"--w-dp-base", type=float, default=160.0)
115 args = ap.parse_args()
117 cfg_path = Path(args.config)
118 a_center = float(args.a_throat_center_mm2)
119 w_base = float(args.w_dp_base)
121 cases: List[Tuple[str, float, float, Optional[float], Optional[float]]] = [
122 (
"sym ±12.5%", 0.125, w_base,
None,
None),
123 (
"O-heavy ±12.5% ×1.5", 0.125, w_base, w_base * 1.5, w_base),
124 (
"O-heavy ±12.5% ×2", 0.125, w_base, w_base * 2.0, w_base),
125 (
"O-heavy ±15% ×1.5", 0.15, w_base, w_base * 1.5, w_base),
126 (
"O-heavy ±15% ×2", 0.15, w_base, w_base * 2.0, w_base),
127 (
"O-heavy ±15% ×2.25", 0.15, w_base, w_base * 2.25, w_base),
130 rows: List[Dict[str, Any]] = []
132 print(
"\n=== LOX-focused free-throat Layer-1 grid ===\n")
134 for label, throat_frac, w_dp, w_o, w_f
in cases:
135 cfg = load_config(str(cfg_path))
136 req = cfg.design_requirements.model_dump()
139 req[
"layer1_A_throat_mm2_min"] = a_center * (1.0 - throat_frac)
140 req[
"layer1_A_throat_mm2_max"] = a_center * (1.0 + throat_frac)
143 req[
"W_geom_ao_af_momentum"] = 2800.0
144 req[
"injector_dp_ratio_F_min"] = 0.5
145 req[
"injector_dp_ratio_F_max"] = 1.5
146 req[
"W_DP"] = float(w_dp)
148 req[
"W_DP_O"] = float(w_o)
150 req.pop(
"W_DP_O",
None)
152 req[
"W_DP_F"] = float(w_f)
154 req.pop(
"W_DP_F",
None)
157 "mode":
"optimizer_controlled",
158 "max_lox_pressure_psi": float(req[
"max_lox_tank_pressure_psi"]),
159 "max_fuel_pressure_psi": float(req[
"max_fuel_tank_pressure_psi"]),
162 print(f
"\n--- Case: {label} ---")
164 opt_cfg, results = run_layer1_optimization(
168 target_burn_time=float(req.get(
"target_burn_time", 6.0)),
169 tolerances={
"thrust": 0.10,
"apogee": 0.15},
170 pressure_config=pcfg,
172 layer1_max_iterations=int(args.layer1_max_iterations),
173 layer1_cma_restarts=int(args.layer1_cma_restarts),
175 perf = results.get(
"performance")
or {}
177 cg = getattr(opt_cfg,
"chamber_geometry",
None)
178 atm = getattr(cg,
"A_throat",
None)
if cg
is not None else None
179 a_mm2 = float(atm) * 1e6
if atm
is not None else float(
"nan")
181 ok_f, ok_mr, ok_r, ok_o, ok_ff =
_gates(
183 o_band=(float(req[
"injector_dp_ratio_O_min"]), float(req[
"injector_dp_ratio_O_max"])),
184 f_cap=float(req[
"injector_dp_ratio_F_max"]),
190 "throat_frac": throat_frac,
192 "W_DP_O": w_o
if w_o
is not None else w_dp,
193 "W_DP_F": w_f
if w_f
is not None else w_dp,
195 "MR": perf.get(
"MR"),
196 "R": perf.get(
"momentum_ratio_R"),
199 "A_throat_mm2": a_mm2,
200 "P_O_psi": perf.get(
"P_O_start_psi"),
201 "P_F_psi": perf.get(
"P_F_start_psi"),
212 o_band = (0.20, 0.35)
215 rows_sorted = sorted(rows, key=
lambda r:
_rank_key(r[
"perf"], o_band=o_band, f_cap=f_cap))
217 print(
"\n" +
"=" * 110)
218 print(
"RANKED CANDIDATES (best first)")
219 print(
"Gates: |F−8000|/8000≤5%; |MR−3.5|/3.5≤5%; |R−1|≤10%; LOX ΔP/Pc∈[0.20,0.35]; fuel ΔP/Pc≤1.50")
220 print(
"Secondary: minimize LOX violation / maximize LOX ΔP/Pc toward 0.35 inside band")
224 f
"{'rank':>4} {'case':<26} {'At_mm2':>9} {'F':>9} {'MR':>7} {'R':>7} "
225 f
"{'dP_O':>7} {'dP_F':>7} {'gF':>3}{'gMR':>4}{'gR':>4}{'gO':>4}{'gf':>4}"
228 print(
"-" * len(hdr))
230 for i, r
in enumerate(rows_sorted, start=1):
232 f
"{i:4d} {r['label']:<26} {r['A_throat_mm2']:9.1f} "
233 f
"{_sf(r['F']):9.2f} {_sf(r['MR']):7.4f} {_sf(r['R']):7.4f} "
234 f
"{_sf(r['dP_O']):7.4f} {_sf(r['dP_F']):7.4f} "
235 f
"{'Y' if r['ok_F'] else 'N':>3}"
236 f
"{'Y' if r['ok_MR'] else 'N':>4}"
237 f
"{'Y' if r['ok_R'] else 'N':>4}"
238 f
"{'Y' if r['ok_LOX'] else 'N':>4}"
239 f
"{'Y' if r['ok_FuelCap'] else 'N':>4}"
242 best = rows_sorted[0]
243 print(
"\nBest-ranked row:", best[
"label"])
244 ig = best[
"opt_cfg"].injector.geometry
246 f
" Injector n_O={int(ig.oxidizer.n_elements)} d_O={ig.oxidizer.d_jet*1000:.4f} mm "
247 f
"n_F={int(ig.fuel.n_elements)} d_F={ig.fuel.d_jet*1000:.4f} mm"