90 cfg = copy.deepcopy(tmpl)
92 cfg.injector.geometry.oxidizer.n_elements = N_EL
93 cfg.injector.geometry.fuel.n_elements = N_EL
94 cfg.injector.geometry.oxidizer.d_jet = float(x[0]) * 1e-3
95 cfg.injector.geometry.fuel.d_jet = float(x[1]) * 1e-3
96 cfg.injector.geometry.oxidizer.impingement_angle = float(x[2])
97 cfg.injector.geometry.fuel.impingement_angle = float(x[3])
98 cfg.injector.geometry.oxidizer.spacing = float(x[4]) * 1e-3
99 cfg.injector.geometry.fuel.spacing = float(x[5]) * 1e-3
102 res = runner.evaluate(float(x[6]) * PSI_TO_PA, float(x[7]) * PSI_TO_PA, silent=
True)
103 pc = float(res.get(
"Pc", float(
"nan")))
104 if not math.isfinite(pc)
or pc <= 0:
105 raise ValueError(
"bad pc")
107 F = float(res.get(
"F", float(
"nan")))
108 MR = float(res.get(
"MR", float(
"nan")))
109 diag = res.get(
"diagnostics")
if isinstance(res.get(
"diagnostics"), dict)
else {}
110 Ru = diag.get(
"momentum_ratio_R")
111 R = float(Ru)
if Ru
is not None else float(
"nan")
112 _ro, rf = injector_dp_ratios_from_eval_result(pc, res)
113 dpf = float(rf)
if rf
is not None and math.isfinite(float(rf))
else float(
"nan")
114 return cfg, F, MR, R, dpf
118 tmpl = load_config(ROOT /
"configs" /
"impinging_lox_ch4_8000N.yaml")
120 out_path = ROOT /
"configs" /
"impinging_lox_ch4_8000N_optimal.yaml"
123 track: dict[str, object] = {
"best": 1e30,
"best_x":
None,
"meta": {}}
125 def objective(x: np.ndarray) -> float:
126 d_o_mm, d_f_mm, th_o, th_f, s_o_mm, s_f_mm, po, pf, at = x
128 cfg = copy.deepcopy(tmpl)
131 cfg.injector.geometry.oxidizer.n_elements = N_EL
132 cfg.injector.geometry.fuel.n_elements = N_EL
133 cfg.injector.geometry.oxidizer.d_jet = float(d_o_mm) * 1e-3
134 cfg.injector.geometry.fuel.d_jet = float(d_f_mm) * 1e-3
135 cfg.injector.geometry.oxidizer.impingement_angle = float(th_o)
136 cfg.injector.geometry.fuel.impingement_angle = float(th_f)
137 cfg.injector.geometry.oxidizer.spacing = float(s_o_mm) * 1e-3
138 cfg.injector.geometry.fuel.spacing = float(s_f_mm) * 1e-3
142 res = runner.evaluate(float(po) * PSI_TO_PA, float(pf) * PSI_TO_PA, silent=
True)
146 pc = float(res.get(
"Pc", float(
"nan")))
147 if not math.isfinite(pc)
or pc <= 0:
150 F = float(res.get(
"F", float(
"nan")))
151 MR = float(res.get(
"MR", float(
"nan")))
152 diag = res.get(
"diagnostics")
if isinstance(res.get(
"diagnostics"), dict)
else {}
153 rv = diag.get(
"momentum_ratio_R")
154 R = float(rv)
if rv
is not None else float(
"nan")
155 _ro, rf = injector_dp_ratios_from_eval_result(pc, res)
156 dpf = float(rf)
if rf
is not None and math.isfinite(float(rf))
else float(
"nan")
159 c =
_cost(F, MR, R, dpf)
160 if c < float(track[
"best"]):
161 track[
"best"] = float(c)
162 track[
"best_x"] = np.array(x, dtype=float, copy=
True)
163 track[
"meta"] = {
"F": F,
"MR": MR,
"R": R,
"dPf": dpf}
164 if n_eval[
"n"] % 35 == 0:
165 print(f
" eval={n_eval['n']} best_cost={track['best']}")
170 [C_D_O_MM * 0.85, C_D_O_MM * 1.15],
171 [C_D_F_MM * 0.85, C_D_F_MM * 1.15],
172 [
max(15.0, C_TH_O * 0.88),
min(85.0, C_TH_O * 1.12)],
173 [
max(15.0, C_TH_F * 0.88),
min(90.0, C_TH_F * 1.12)],
174 [C_S_O_MM * 0.78, C_S_O_MM * 1.22],
175 [C_S_F_MM * 0.78, C_S_F_MM * 1.22],
176 [C_PO * 0.92, C_PO * 1.08],
177 [C_PF * 0.92, C_PF * 1.08],
178 [
max(tmpl.chamber_geometry.A_throat * 0.52, C_AT * 0.82), tmpl.chamber_geometry.A_throat * 0.88],
182 x0 = np.array([C_D_O_MM, C_D_F_MM, C_TH_O, C_TH_F, C_S_O_MM, C_S_F_MM, C_PO, C_PF, C_AT])
184 scipy_bounds = [(float(lo), float(hi))
for lo, hi
in bounds]
197 bx = track.get(
"best_x")
199 raise SystemExit(
"Optimizer produced no feasible evaluations.")
201 xv = np.asarray(bx, dtype=float)
202 best_c = float(track[
"best"])
205 def residuals(x_flat: np.ndarray) -> np.ndarray:
207 _c, F, MR, R, dpf =
_vec_model(np.asarray(x_flat, dtype=float), tmpl, eps_fixed)
209 return np.array([50.0, 50.0, 50.0, 50.0], dtype=float)
219 (MR - MR_T) / sigma_mr,
221 (dpf - DPF_T) / sigma_dp,
225 residuals.last_cfg = _c
230 xv_ls = np.clip(xv, lo, hi)
231 ls_res = least_squares(
242 xv_final = np.clip(np.asarray(ls_res.x, dtype=float), lo, hi)
244 cfg_ls = getattr(residuals,
"last_cfg",
None)
245 if cfg_ls
is not None and hasattr(ls_res,
"cost"):
248 _c3, Fs, MRs, Rs, DPfs =
_vec_model(xv_final, tmpl, eps_fixed)
249 meta_ls = {
"F": Fs,
"MR": MRs,
"R": Rs,
"dPf": DPfs}
250 print(f
"least_squares residual cost={float(ls_res.cost):.6g}", meta_ls)
251 except Exception
as e:
252 print(
"least_squares reeval failed:", e)
256 def build_cfg(vec: np.ndarray):
257 cfg = copy.deepcopy(tmpl)
259 cfg.injector.geometry.oxidizer.n_elements = N_EL
260 cfg.injector.geometry.fuel.n_elements = N_EL
261 cfg.injector.geometry.oxidizer.d_jet = float(vec[0]) * 1e-3
262 cfg.injector.geometry.fuel.d_jet = float(vec[1]) * 1e-3
263 cfg.injector.geometry.oxidizer.impingement_angle = float(vec[2])
264 cfg.injector.geometry.fuel.impingement_angle = float(vec[3])
265 cfg.injector.geometry.oxidizer.spacing = float(vec[4]) * 1e-3
266 cfg.injector.geometry.fuel.spacing = float(vec[5]) * 1e-3
269 cfg_out = build_cfg(xv)
270 rr =
PintleEngineRunner(cfg_out).evaluate(float(xv[6]) * PSI_TO_PA, float(xv[7]) * PSI_TO_PA, silent=
True)
271 pc_f = float(rr.get(
"Pc", float(
"nan")))
272 _ro, rf = injector_dp_ratios_from_eval_result(pc_f, rr)
273 diag_o = rr.get(
"diagnostics")
if isinstance(rr.get(
"diagnostics"), dict)
else {}
274 Ru = diag_o.get(
"momentum_ratio_R")
276 print(
"L-BFGS-B success=", res.success,
"message=", res.message)
277 print(
"best cost during search=", float(best_c))
279 f
"reeval best: F={rr.get('F')} MR={rr.get('MR')} R={Ru} "
280 f
"dP_O={_ro} dP_F={rf} n={N_EL}"
282 print(
"x (d_o_mm, d_f_mm, th_o, th_f, s_o_mm, s_f_mm, Po, Pf, A_throat):\n ", xv)
284 out_path.write_text(yaml.safe_dump(cfg_out.model_dump(mode=
"json"), sort_keys=
False))
285 print(f
"wrote {out_path.resolve()}")