STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
recover_8000n_local_refine_rank10.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Match rank-#10 transcript metrics via bounded global search (SciPy DE).
3
4Requires chamber_geometry.expansion_ratio == A_exit / A_throat (see nozzle.calculate_thrust).
5When A_throat is varied here, A_exit is set to epsilon_fixed × A_throat with epsilon_fixed
6taken once from the seed YAML after that consistency fix.
7"""
8
9from __future__ import annotations
10
11import copy
12import logging
13import math
14import sys
15from pathlib import Path
16
17import numpy as np
18import yaml
19from scipy.optimize import least_squares, minimize
20
21ROOT = Path(__file__).resolve().parents[1]
22sys.path.insert(0, str(ROOT))
23
24from engine.core.runner import PintleEngineRunner
25from engine.optimizer.injector_dp_penalty import injector_dp_ratios_from_eval_result
26from engine.pipeline.io import load_config
27
28PSI_TO_PA = 6894.757293168
29
30logging.getLogger("engine.core.injectors.impinging").setLevel(logging.ERROR)
31
32F_T = 8139.56
33MR_T = 3.5085
34R_T = 1.0150
35DPF_T = 0.4493
36N_EL = 14
37
38C_D_O_MM = 3.314
39C_D_F_MM = 2.368
40C_TH_O = 30.79
41C_TH_F = 54.77
42C_PO = 759.07
43C_PF = 747.25
44C_AT = 0.00133574
45C_S_O_MM = 6.2
46C_S_F_MM = 4.2
47
48
50 cg = cfg.chamber_geometry
51 assert cg is not None and cg.A_throat is not None and cg.A_exit is not None and float(cg.A_throat) > 0
52 eps_geo = float(cg.A_exit) / float(cg.A_throat)
53 cg.expansion_ratio = eps_geo
54 if cfg.combustion is not None and getattr(cfg.combustion, "cea", None) is not None:
55 cfg.combustion.cea.expansion_ratio = eps_geo
56 return float(eps_geo)
57
58
59def _apply_throat_keeps_eps(cfg, A_throat: float, eps_fixed: float):
60 cg = cfg.chamber_geometry
61 cg.A_throat = float(A_throat)
62 cg.expansion_ratio = float(eps_fixed)
63 cg.A_exit = float(A_throat) * float(eps_fixed)
64 de = math.sqrt(max(0.0, 4.0 * float(cg.A_exit) / math.pi))
65 cg.exit_diameter = de
66 if cfg.combustion is not None and getattr(cfg.combustion, "cea", None) is not None:
67 cfg.combustion.cea.expansion_ratio = float(eps_fixed)
68 nz = getattr(cfg, "nozzle", None)
69 if nz is not None:
70 nz.A_throat = cg.A_throat
71 nz.A_exit = cg.A_exit
72 if hasattr(nz, "expansion_ratio"):
73 setattr(nz, "expansion_ratio", float(eps_fixed))
74 if hasattr(nz, "exit_diameter"):
75 setattr(nz, "exit_diameter", de)
76
77
78def _cost(F: float, MR: float, R: float, dpf: float) -> float:
79 if not all(map(math.isfinite, (F, MR, R, dpf))):
80 return 1e12
81 return (
82 ((F / F_T - 1.0) ** 2) * 2.0
83 + ((MR / MR_T - 1.0) ** 2) * 400.0
84 + ((R - R_T) ** 2) * 2500.0
85 + ((dpf - DPF_T) ** 2) * 12000.0
86 )
87
88
89def _vec_model(x: np.ndarray, tmpl, eps_fixed: float):
90 cfg = copy.deepcopy(tmpl)
91 _apply_throat_keeps_eps(cfg, float(x[8]), eps_fixed)
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
100
101 runner = PintleEngineRunner(cfg)
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")
106
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
115
116
117def main() -> None:
118 tmpl = load_config(ROOT / "configs" / "impinging_lox_ch4_8000N.yaml")
119 eps_fixed = _ensure_geom_eps_aligned(tmpl)
120 out_path = ROOT / "configs" / "impinging_lox_ch4_8000N_optimal.yaml"
121
122 n_eval = {"n": 0}
123 track: dict[str, object] = {"best": 1e30, "best_x": None, "meta": {}}
124
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
127
128 cfg = copy.deepcopy(tmpl)
129 _apply_throat_keeps_eps(cfg, float(at), eps_fixed)
130
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
139
140 try:
141 runner = PintleEngineRunner(cfg)
142 res = runner.evaluate(float(po) * PSI_TO_PA, float(pf) * PSI_TO_PA, silent=True)
143 except Exception:
144 return 1e12
145
146 pc = float(res.get("Pc", float("nan")))
147 if not math.isfinite(pc) or pc <= 0:
148 return 1e12
149
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")
157
158 n_eval["n"] += 1
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']}")
166 return c
167
168 bounds = np.array(
169 [
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],
179 ]
180 )
181
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])
183
184 scipy_bounds = [(float(lo), float(hi)) for lo, hi in bounds]
185
186 res = minimize(
187 objective,
188 x0,
189 method="L-BFGS-B",
190 bounds=scipy_bounds,
191 options={
192 "maxfun": 220,
193 "ftol": 1e-7,
194 },
195 )
196
197 bx = track.get("best_x")
198 if bx is None:
199 raise SystemExit("Optimizer produced no feasible evaluations.")
200
201 xv = np.asarray(bx, dtype=float)
202 best_c = float(track["best"])
203
204 # Pass 2: vector residual polishing (prioritize thrust + MR once ΔP_F is in-band).
205 def residuals(x_flat: np.ndarray) -> np.ndarray:
206 try:
207 _c, F, MR, R, dpf = _vec_model(np.asarray(x_flat, dtype=float), tmpl, eps_fixed)
208 except Exception:
209 return np.array([50.0, 50.0, 50.0, 50.0], dtype=float)
210
211 sigma_f = 35.0
212 sigma_mr = 0.04
213 sigma_r = 0.012
214 sigma_dp = 0.012
215
216 rf = np.array(
217 [
218 (F - F_T) / sigma_f,
219 (MR - MR_T) / sigma_mr,
220 (R - R_T) / sigma_r,
221 (dpf - DPF_T) / sigma_dp,
222 ],
223 dtype=float,
224 )
225 residuals.last_cfg = _c # type: ignore[attr-defined]
226 return rf
227
228 lo = bounds[:, 0]
229 hi = bounds[:, 1]
230 xv_ls = np.clip(xv, lo, hi)
231 ls_res = least_squares(
232 residuals,
233 xv_ls,
234 bounds=(lo, hi),
235 method="trf",
236 max_nfev=260,
237 xtol=1e-11,
238 ftol=1e-11,
239 gtol=1e-11,
240 verbose=0,
241 )
242 xv_final = np.clip(np.asarray(ls_res.x, dtype=float), lo, hi)
243
244 cfg_ls = getattr(residuals, "last_cfg", None)
245 if cfg_ls is not None and hasattr(ls_res, "cost"):
246 meta_ls = {}
247 try:
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)
253
254 xv = xv_final
255
256 def build_cfg(vec: np.ndarray):
257 cfg = copy.deepcopy(tmpl)
258 _apply_throat_keeps_eps(cfg, float(vec[8]), eps_fixed)
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
267 return cfg
268
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")
275
276 print("L-BFGS-B success=", res.success, "message=", res.message)
277 print("best cost during search=", float(best_c))
278 print(
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}"
281 )
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)
283
284 out_path.write_text(yaml.safe_dump(cfg_out.model_dump(mode="json"), sort_keys=False))
285 print(f"wrote {out_path.resolve()}")
286
287
288if __name__ == "__main__":
289 main()
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
float _cost(float F, float MR, float R, float dpf)
_apply_throat_keeps_eps(cfg, float A_throat, float eps_fixed)
_vec_model(np.ndarray x, tmpl, float eps_fixed)