83def _apply_cd_caps(cfg: Any, cd_o_inf: float, cd_f_inf: float) -> Tuple[float, float, float, float]:
84 ox = cfg.discharge[
"oxidizer"]
85 fu = cfg.discharge[
"fuel"]
86 cd_o_min = float(np.clip(cd_o_inf * _RATIO_CD_MIN_INF_O, 0.05, cd_o_inf - 1e-6))
87 cd_f_min = float(np.clip(cd_f_inf * _RATIO_CD_MIN_INF_F, 0.05, cd_f_inf - 1e-6))
88 cfg.discharge[
"oxidizer"] = ox.model_copy(update={
"Cd_inf": float(cd_o_inf),
"Cd_min": cd_o_min})
89 cfg.discharge[
"fuel"] = fu.model_copy(update={
"Cd_inf": float(cd_f_inf),
"Cd_min": cd_f_min})
90 return cd_o_inf, cd_o_min, cd_f_inf, cd_f_min
104) -> Tuple[Any, Dict[str, Any], Dict[str, Any]]:
105 cfg = load_config(str(base_cfg_path))
106 mid_mm2 = 0.5 * (float(throat_lo_mm2) + float(throat_hi_mm2))
108 if cfg.chamber_geometry
is not None:
109 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
111 ig0 = cfg.injector.geometry
112 ig0.oxidizer.n_elements = 68
113 ig0.oxidizer.d_jet = 1.424e-3
114 ig0.oxidizer.spacing = 5.901e-3
115 ig0.oxidizer.impingement_angle = 63.15
116 ig0.fuel.n_elements = 32
117 ig0.fuel.d_jet = 1.392e-3
118 ig0.fuel.spacing = 4.118e-3
119 ig0.fuel.impingement_angle = 75.05
121 req = cfg.design_requirements.model_dump()
123 fp.pop(
"A_throat_mm2",
None)
124 fp.pop(
"P_O_start_psi",
None)
125 fp.pop(
"P_F_start_psi",
None)
126 if cfg.chamber_geometry
is not None:
127 fp[
"expansion_ratio"] = float(cfg.chamber_geometry.expansion_ratio)
128 fp[
"Lstar_mm"] = float(cfg.chamber_geometry.Lstar) * 1000.0
129 req[
"frozen_parameters"] = fp
131 req[
"layer1_A_throat_mm2_min"] = float(throat_lo_mm2)
132 req[
"layer1_A_throat_mm2_max"] = float(throat_hi_mm2)
133 req[
"layer1_P_O_start_psi_min"] = float(p_o_lo)
134 req[
"layer1_P_O_start_psi_max"] = float(p_o_hi)
135 req[
"layer1_P_F_start_psi_min"] = float(p_f_lo)
136 req[
"layer1_P_F_start_psi_max"] = float(p_f_hi)
137 req[
"max_lox_tank_pressure_psi"] = float(max_lox_psi)
138 req[
"max_fuel_tank_pressure_psi"] = float(max_fuel_psi)
140 req[
"injector_dp_ratio_O_min"] = 0.15
141 req[
"injector_dp_ratio_O_max"] = 0.40
142 req[
"injector_dp_ratio_F_min"] = 0.15
143 req[
"injector_dp_ratio_F_max"] = 0.40
145 req[
"impinging_momentum_R_min"] = 0.93
146 req[
"impinging_momentum_R_max"] = 1.07
148 req[
"W_MOM"] = 1600.0
149 req[
"W_geom_ao_af_momentum"] = 6500.0
151 req[
"W_DP_O"] = 820.0
152 req[
"W_DP_F"] = 820.0
154 req[
"target_thrust"] = F_TARGET
155 req[
"optimal_of_ratio"] = MR_TARGET
157 req[
"require_stable_state"] =
False
158 req[
"min_stability_score"] =
min(float(req.get(
"min_stability_score", 0.75)), 0.58)
159 req[
"min_stability_margin"] =
min(float(req.get(
"min_stability_margin", 1.2)), 1.05)
160 req[
"layer1_infeasibility_gate_eps"] = 2.0e-3
163 "mode":
"optimizer_controlled",
164 "max_lox_pressure_psi": float(max_lox_psi),
165 "max_fuel_pressure_psi": float(max_fuel_psi),
167 return cfg, req, pcfg
171 ap = argparse.ArgumentParser(description=__doc__)
172 ap.add_argument(
"--config", type=str, default=str(ROOT /
"configs/impinging_lox_ch4_8000N.yaml"))
173 ap.add_argument(
"--layer1-max-iterations", type=int, default=400)
174 ap.add_argument(
"--layer1-cma-restarts", type=int, default=2)
178 choices=(
"region_b",
"pct125"),
180 help=
"region_b=[2520,2600] mm²; pct125=±12.5%% around YAML nominal A_throat_mm2",
182 ap.add_argument(
"--p-o-min-psi", type=float, default=580.0)
183 ap.add_argument(
"--p-o-max-psi", type=float, default=700.0)
184 ap.add_argument(
"--p-f-min-psi", type=float, default=350.0)
185 ap.add_argument(
"--p-f-max-psi", type=float, default=500.0)
186 ap.add_argument(
"--max-lox-pressure-psi", type=float, default=700.0)
187 ap.add_argument(
"--max-fuel-pressure-psi", type=float, default=500.0)
188 ap.add_argument(
"--output-csv", type=str, default=str(ROOT /
"output/sensitivity_cd_layer1_summary.csv"))
189 args = ap.parse_args()
191 cfg_path = Path(args.config)
192 if not cfg_path.is_absolute():
193 cfg_path = ROOT / cfg_path
195 probe = load_config(str(cfg_path))
196 if args.athroat_mode ==
"region_b":
197 throat_lo, throat_hi = 2520.0, 2600.0
199 at0 = float(probe.chamber_geometry.A_throat) * 1e6
200 throat_lo, throat_hi = at0 * 0.875, at0 * 1.125
201 layer1_logger_info =
"A_throat band ±12.5% from nominal {:.1f} mm² → [{:.1f}, {:.1f}]".
format(
202 at0, throat_lo, throat_hi
204 print(layer1_logger_info)
208 throat_lo_mm2=throat_lo,
209 throat_hi_mm2=throat_hi,
210 p_o_lo=args.p_o_min_psi,
211 p_o_hi=args.p_o_max_psi,
212 p_f_lo=args.p_f_min_psi,
213 p_f_hi=args.p_f_max_psi,
214 max_lox_psi=args.max_lox_pressure_psi,
215 max_fuel_psi=args.max_fuel_pressure_psi,
218 print(
"Cd assumption sensitivity — Layer 1")
220 " Throat mm² [{:.0f}, {:.0f}] | P_O [{:.0f},{:.0f}] | P_F [{:.0f},{:.0f}] | "
221 "ΔP/Pc [0.15,0.40] both | R [0.93,1.07]".
format(
222 throat_lo, throat_hi, args.p_o_min_psi, args.p_o_max_psi, args.p_f_min_psi, args.p_f_max_psi
225 print(f
" max_iter={args.layer1_max_iterations} cma_restarts={args.layer1_cma_restarts}\n")
227 rows: List[Dict[str, Any]] = []
229 for case_id, cd_o_assume, cd_f_assume
in CASES:
230 cfg = copy.deepcopy(cfg0)
231 cdi_o, cdm_o, cdi_f, cdm_f =
_apply_cd_caps(cfg, cd_o_assume, cd_f_assume)
232 opt_cfg, results = run_layer1_optimization(
236 target_burn_time=float(req.get(
"target_burn_time", 6.0)),
237 tolerances={
"thrust": 0.10,
"apogee": 0.15},
238 pressure_config=copy.deepcopy(pcfg),
240 layer1_max_iterations=int(args.layer1_max_iterations),
241 layer1_cma_restarts=int(args.layer1_cma_restarts),
243 perf = results.get(
"performance")
or {}
244 diag = perf.get(
"diagnostics")
if isinstance(perf.get(
"diagnostics"), dict)
else {}
246 lt = getattr(opt_cfg.lox_tank,
"initial_pressure_psi",
None)
247 ft = getattr(opt_cfg.fuel_tank,
"initial_pressure_psi",
None)
248 poc = getattr(opt_cfg.chamber_geometry,
"A_throat",
None)
249 at_mm2 = float(poc) * 1e6
if poc
is not None and np.isfinite(float(poc))
else float(
"nan")
251 ig = opt_cfg.injector.geometry
252 cd_o_act = perf.get(
"Cd_O", diag.get(
"Cd_O"))
253 cd_f_act = perf.get(
"Cd_F", diag.get(
"Cd_F"))
256 f
"LOX n={ig.oxidizer.n_elements} d={ig.oxidizer.d_jet*1000:.3f}mm "
257 f
"sp={ig.oxidizer.spacing*1000:.3f}mm θ={ig.oxidizer.impingement_angle:.1f}° | "
258 f
"F n={ig.fuel.n_elements} d={ig.fuel.d_jet*1000:.3f}mm "
259 f
"sp={ig.fuel.spacing*1000:.3f}mm θ={ig.fuel.impingement_angle:.1f}°"
264 "Cd_O_assumed_inf": cdi_o,
265 "Cd_O_assumed_min": cdm_o,
266 "Cd_F_assumed_inf": cdi_f,
267 "Cd_F_assumed_min": cdm_f,
268 "Cd_O_actual": cd_o_act,
269 "Cd_F_actual": cd_f_act,
270 "F_N": perf.get(
"F"),
271 "MR": perf.get(
"MR"),
272 "R": perf.get(
"momentum_ratio_R"),
273 "dP_O_over_Pc": perf.get(
"injector_dp_ratio_O"),
274 "dP_F_over_Pc": perf.get(
"injector_dp_ratio_F"),
275 "P_O_tank_psi": float(lt)
if lt
is not None and np.isfinite(float(lt))
else None,
276 "P_F_tank_psi": float(ft)
if ft
is not None and np.isfinite(float(ft))
else None,
277 "A_throat_mm2": at_mm2,
278 "injector_geometry": geo,
279 "layer1_objective_approx": results.get(
"convergence_info", {}).get(
"final_change"),
283 print(f
"=== Case {case_id} (Cd_inf O={cdi_o}, F={cdi_f}; Cd_min O={cdm_o:.4f}, F={cdm_f:.4f}) ===")
284 print(f
" actual Cd_O={cd_o_act} Cd_F={cd_f_act}")
285 print(f
" F={perf.get('F')} N MR={perf.get('MR')} R={perf.get('momentum_ratio_R')}")
286 print(f
" ΔP_O/Pc={perf.get('injector_dp_ratio_O')} ΔP_F/Pc={perf.get('injector_dp_ratio_F')}")
287 print(f
" tanks P_O={lt} psi P_F={ft} psi At={at_mm2:.2f} mm²")
290 outp = Path(args.output_csv)
291 outp.parent.mkdir(parents=
True, exist_ok=
True)
293 fieldnames = list(rows[0].keys())
294 with open(outp,
"w", newline=
"", encoding=
"utf-8")
as f:
295 w = csv.DictWriter(f, fieldnames=fieldnames)
298 print(f
"Wrote {outp}")
Tuple[Any, Dict[str, Any], Dict[str, Any]] _build_req_and_cfg(Path base_cfg_path, *, float throat_lo_mm2, float throat_hi_mm2, float p_o_lo, float p_o_hi, float p_f_lo, float p_f_hi, float max_lox_psi, float max_fuel_psi)