STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
run_impinging_feasibility_exploration.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Exploration run: relaxed fuel ΔP band + reduced fuel tank pressure *upper* bound via pressure_config.
3
4Layer-1 uses (see layer1_static_optimization):
5
6 P_F bounds = [max_fuel_pressure_psi * 0.65, max_fuel_pressure_psi * 0.85]
7
8Setting pressure_config[\"max_fuel_pressure_psi\"] = 900 gives ~585–765 psi search band.
9
10Also overrides requirements for this session:
11
12 injector_dp_ratio_F_max = 1.50 (oxidizer band unchanged from YAML)
13 W_MOM = 300
14 W_geom_ao_af_momentum = 2800
15
16Then: AO/AF evaluate sweep [1.8, 2.5] at optimized tank pressures,
17 Cd sensitivity grid (0.9/1/1.1)× on Cd_inf and Cd_min per propellant (in-memory copies only).
18
19Does not modify YAML or committed Cd defaults.
20"""
21
22from __future__ import annotations
23
24import argparse
25import copy
26import math
27import sys
28from pathlib import Path
29from typing import Any, Dict, List, Optional, Tuple
30
31import numpy as np
32
33ROOT = Path(__file__).resolve().parents[1]
34sys.path.insert(0, str(ROOT))
35
36PSI_TO_PA = 6894.76
37
38from engine.core.runner import PintleEngineRunner # noqa: E402
39from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization # noqa: E402
40from engine.pipeline.config_schemas import DischargeConfig, PintleEngineConfig # noqa: E402
41from engine.pipeline.io import load_config # noqa: E402
42
43
44def _dp_ratios(ev: Dict[str, Any]) -> Tuple[Optional[float], Optional[float]]:
45 pc = float(ev.get("Pc", float("nan")))
46 if not (np.isfinite(pc) and pc > 0):
47 return None, None
48 diag = ev.get("diagnostics") or {}
49 dpo = diag.get("delta_p_injector_O")
50 dpf = diag.get("delta_p_injector_F")
51 ro = float(dpo) / pc if dpo is not None and np.isfinite(dpo) else None
52 rf = float(dpf) / pc if dpf is not None and np.isfinite(dpf) else None
53 return ro, rf
54
55
57 F: float,
58 MR: float,
59 R: Optional[float],
60 ro: Optional[float],
61 rf: Optional[float],
62 *,
63 f_band_o: Tuple[float, float],
64 rf_cap: float,
65) -> bool:
66 if not np.isfinite(F) or not np.isfinite(MR):
67 return False
68 if abs(F - 8000.0) / 8000.0 > 0.05:
69 return False
70 if abs(MR - 3.5) / 3.5 > 0.05:
71 return False
72 if R is None or not np.isfinite(R) or abs(R - 1.0) > 0.10:
73 return False
74 if ro is None or rf is None:
75 return False
76 if ro < f_band_o[0] or ro > f_band_o[1]:
77 return False
78 if rf > rf_cap:
79 return False
80 return True
81
82
83def _scale_discharge_inplace(dc: DischargeConfig, factor: float) -> None:
84 dc.Cd_inf = float(np.clip(dc.Cd_inf * factor, 1e-6, 1.0))
85 dc.Cd_min = float(min(dc.Cd_min * factor, dc.Cd_inf))
86
87
88def _apply_cd_factors(cfg: PintleEngineConfig, fo: float, ff: float) -> PintleEngineConfig:
89 c = copy.deepcopy(cfg)
90 ox = c.discharge["oxidizer"]
91 fu = c.discharge["fuel"]
94 return c
95
96
97def _print_layer1_metrics(title: str, opt_cfg: PintleEngineConfig, perf: Dict[str, Any]) -> None:
98 diag = perf.get("diagnostics") or {}
99 pc = float(perf.get("Pc", float("nan")))
100 ro, rf = _dp_ratios({"Pc": pc, "diagnostics": diag})
101
102 ig = opt_cfg.injector.geometry
103 n_o = int(ig.oxidizer.n_elements)
104 n_f = int(ig.fuel.n_elements)
105 d_o_mm = float(ig.oxidizer.d_jet) * 1000.0
106 d_f_mm = float(ig.fuel.d_jet) * 1000.0
107 s_o_mm = float(ig.oxidizer.spacing) * 1000.0
108 s_f_mm = float(ig.fuel.spacing) * 1000.0
109
110 a_go = perf.get("A_geom_O") or diag.get("A_geom_O")
111 a_gf = perf.get("A_geom_F") or diag.get("A_geom_F")
112 a_eo = perf.get("A_eff_O") or diag.get("A_eff_O")
113 a_ef = perf.get("A_eff_F") or diag.get("A_eff_F")
114
115 print(f"\n{'=' * 80}\n{title}\n{'=' * 80}")
116 print(f"Thrust F [N]: {perf.get('F')}")
117 print(f"MR: {perf.get('MR')}")
118 print(f"momentum_ratio_R: {perf.get('momentum_ratio_R')}")
119 print(f"ΔP_inj_O / Pc: {ro}")
120 print(f"ΔP_inj_F / Pc: {rf}")
121 print(f"P_O_start_psi: {perf.get('P_O_start_psi')}")
122 print(f"P_F_start_psi: {perf.get('P_F_start_psi')}")
123 print(f"A_geom_O / A_geom_F: {perf.get('geom_ao_af')}")
124 print(f"A_geom_O [m²]: {a_go}")
125 print(f"A_geom_F [m²]: {a_gf}")
126 ao_af_eff = (
127 float(a_eo) / float(a_ef)
128 if a_eo is not None and a_ef is not None and float(a_ef) > 0
129 else float("nan")
130 )
131 print(f"A_eff_O / A_eff_F: {ao_af_eff}")
132 print(f"expected_AO_AF_for_R≈1: {perf.get('expected_ao_af_for_R1')}")
133 print(
134 f"Injector: n_O={n_o} d_O={d_o_mm:.4f} mm spacing_O={s_o_mm:.4f} mm imp_O={ig.oxidizer.impingement_angle:.2f}°"
135 )
136 print(
137 f" n_F={n_f} d_F={d_f_mm:.4f} mm spacing_F={s_f_mm:.4f} mm imp_F={ig.fuel.impingement_angle:.2f}°"
138 )
139
140
142 opt_cfg: PintleEngineConfig,
143 po_pa: float,
144 pf_pa: float,
145 *,
146 ratio_min: float,
147 ratio_max: float,
148 steps: int,
149 silent: bool,
150 f_band_o: Tuple[float, float],
151 rf_cap: float,
152) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
153 geom = opt_cfg.injector.geometry
154 ox = geom.oxidizer
155 fu = geom.fuel
156 n_o = max(1, int(ox.n_elements))
157 n_f = max(1, int(fu.n_elements))
158 d_f = float(fu.d_jet)
159
160 runner_base = PintleEngineRunner(copy.deepcopy(opt_cfg))
161 pa = runner_base._get_ambient_pressure(None)
162
163 ratios = np.linspace(ratio_min, ratio_max, steps)
164 rows: List[Dict[str, Any]] = []
165 print(f"\n{'=' * 80}\nAO/AF sweep (fixed tank P, vary d_O)\n{'=' * 80}")
166
167 for r_tgt in ratios:
168 cfg_i = copy.deepcopy(opt_cfg)
169 g = cfg_i.injector.geometry
170 scale = math.sqrt(max(1e-30, float(r_tgt) * float(n_f) / float(n_o)))
171 g.oxidizer.d_jet = d_f * scale
172 runner = PintleEngineRunner(cfg_i)
173 try:
174 ev = runner.evaluate(po_pa, pf_pa, P_ambient=pa, silent=silent)
175 except Exception as e:
176 rows.append({"AoAf_tgt": r_tgt, "error": str(e)})
177 continue
178
179 diag = ev.get("diagnostics") or {}
180 R = diag.get("momentum_ratio_R")
181 ro, rf = _dp_ratios(ev)
182 F = float(ev.get("F", float("nan")))
183 MR = float(ev.get("MR", float("nan")))
184 ok = _feasible(F, MR, float(R) if R is not None else None, ro, rf, f_band_o=f_band_o, rf_cap=rf_cap)
185 rows.append(
186 {
187 "AoAf_tgt": float(r_tgt),
188 "F": F,
189 "MR": MR,
190 "R": float(R) if R is not None else float("nan"),
191 "dP_O_Pc": ro,
192 "dP_F_Pc": rf,
193 "feasible": ok,
194 }
195 )
196
197 hdr = f"{'AoAf':>8} {'F':>10} {'MR':>8} {'R':>8} {'dPO':>8} {'dPF':>8} {'ok':>5}"
198 print(hdr)
199 print("-" * len(hdr))
200 for row in rows:
201 if "error" in row:
202 print(f"{row['AoAf_tgt']:8.4f} ERROR {row['error']}")
203 continue
204 print(
205 f"{row['AoAf_tgt']:8.4f} {row['F']:10.2f} {row['MR']:8.4f} {row['R']:8.4f} "
206 f"{row['dP_O_Pc'] if row['dP_O_Pc'] is not None else float('nan'):8.4f} "
207 f"{row['dP_F_Pc'] if row['dP_F_Pc'] is not None else float('nan'):8.4f} "
208 f"{str(row['feasible']):>5}"
209 )
210
211 good = [r for r in rows if r.get("feasible")]
212 return rows, good
213
214
216 opt_cfg: PintleEngineConfig,
217 po_pa: float,
218 pf_pa: float,
219 *,
220 silent: bool,
221) -> None:
222 print(f"\n{'=' * 80}\nCd sensitivity (×Cd_inf & Cd_min per stream; in-memory copy only)\n{'=' * 80}")
223 factors = [0.9, 1.0, 1.1]
224 runner_ref = PintleEngineRunner(copy.deepcopy(opt_cfg))
225 pa = runner_ref._get_ambient_pressure(None)
226
227 print(f"{'f_O':>6} {'f_F':>6} {'R':>8} {'dPF/Pc':>10} {'MR':>8} {'F[N]':>10}")
228 print("-" * 52)
229 for fo in factors:
230 for ff in factors:
231 cfg_cd = _apply_cd_factors(opt_cfg, fo, ff)
232 runner = PintleEngineRunner(cfg_cd)
233 try:
234 ev = runner.evaluate(po_pa, pf_pa, P_ambient=pa, silent=silent)
235 except Exception as e:
236 print(f"{fo:6.2f} {ff:6.2f} ERROR {e}")
237 continue
238 diag = ev.get("diagnostics") or {}
239 R = diag.get("momentum_ratio_R")
240 _, rf = _dp_ratios(ev)
241 print(
242 f"{fo:6.2f} {ff:6.2f} {float(R) if R is not None else float('nan'):8.4f} "
243 f"{rf if rf is not None else float('nan'):10.4f} {float(ev.get('MR', float('nan'))):8.4f} "
244 f"{float(ev.get('F', float('nan'))):10.2f}"
245 )
246
247
248def main() -> None:
249 ap = argparse.ArgumentParser(description=__doc__)
250 ap.add_argument("--config", type=str, default=str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
251 ap.add_argument("--layer1-max-iterations", type=int, default=150)
252 ap.add_argument("--layer1-cma-restarts", type=int, default=1)
253 ap.add_argument("--max-fuel-pressure-psi-band-cap", type=float, default=900.0)
254 ap.add_argument("--injector-dp-f-max", type=float, default=1.50)
255 ap.add_argument("--w-mom", type=float, default=300.0)
256 ap.add_argument("--w-geom-ao-af", type=float, default=2800.0)
257 ap.add_argument("--aoaf-min", type=float, default=1.8)
258 ap.add_argument("--aoaf-max", type=float, default=2.5)
259 ap.add_argument("--aoaf-steps", type=int, default=22)
260 ap.add_argument("--silent-eval", action="store_true")
261 args = ap.parse_args()
262
263 cfg = load_config(args.config)
264 req = cfg.design_requirements.model_dump()
265 req["W_MOM"] = float(args.w_mom)
266 req["W_geom_ao_af_momentum"] = float(args.w_geom_ao_af)
267 req["injector_dp_ratio_F_max"] = float(args.injector_dp_f_max)
268
269 pcfg = {
270 "mode": "optimizer_controlled",
271 "max_lox_pressure_psi": float(req["max_lox_tank_pressure_psi"]),
272 # User-requested exploration band: 0.65–0.85 × this cap ≈ 585–765 psi when cap = 900
273 "max_fuel_pressure_psi": float(args.max_fuel_pressure_psi_band_cap),
274 }
275
276 f_band_o = (float(req["injector_dp_ratio_O_min"]), float(req["injector_dp_ratio_O_max"]))
277 rf_cap = float(args.injector_dp_f_max)
278
279 print(
280 "\nExploration overrides:\n"
281 f" pressure_config[max_fuel_pressure_psi]={pcfg['max_fuel_pressure_psi']} "
282 f"→ fuel P search [{pcfg['max_fuel_pressure_psi'] * 0.65:.1f}, {pcfg['max_fuel_pressure_psi'] * 0.85:.1f}] psi\n"
283 f" injector_dp_ratio_F_max={rf_cap}\n"
284 f" W_MOM={req['W_MOM']} W_geom_ao_af_momentum={req['W_geom_ao_af_momentum']}\n"
285 f" Layer-1 iterations={args.layer1_max_iterations} restarts={args.layer1_cma_restarts}\n"
286 )
287
288 opt_cfg, results = run_layer1_optimization(
289 copy.deepcopy(cfg),
290 PintleEngineRunner(copy.deepcopy(cfg)),
291 req,
292 target_burn_time=float(req.get("target_burn_time", 6.0)),
293 tolerances={"thrust": 0.10, "apogee": 0.15},
294 pressure_config=pcfg,
295 layer1_smoke=False,
296 layer1_max_iterations=int(args.layer1_max_iterations),
297 layer1_cma_restarts=int(args.layer1_cma_restarts),
298 )
299 perf = results.get("performance") or {}
300 _print_layer1_metrics("Layer-1 result (exploration overrides)", opt_cfg, perf)
301
302 po_pa = float(perf["P_O_start_psi"]) * PSI_TO_PA
303 pf_pa = float(perf["P_F_start_psi"]) * PSI_TO_PA
304
305 rows, good = _ao_af_sweep(
306 opt_cfg,
307 po_pa,
308 pf_pa,
309 ratio_min=float(args.aoaf_min),
310 ratio_max=float(args.aoaf_max),
311 steps=int(args.aoaf_steps),
312 silent=args.silent_eval,
313 f_band_o=f_band_o,
314 rf_cap=rf_cap,
315 )
316
317 _cd_sweep(opt_cfg, po_pa, pf_pa, silent=args.silent_eval)
318
319 print(f"\n{'=' * 80}\nCONCLUSION / BEST CANDIDATES\n{'=' * 80}")
320
321 def _score(evdict: Dict[str, Any]) -> float:
322 F = float(evdict.get("F", 0))
323 MR = float(evdict.get("MR", 0))
324 R = float(evdict.get("R", 1.0))
325 ro = evdict.get("dP_O_Pc")
326 rf = evdict.get("dP_F_Pc")
327 pen = 0.0
328 pen += ((F - 8000) / 8000) ** 2
329 pen += ((MR - 3.5) / 3.5) ** 2
330 pen += (R - 1.0) ** 2
331 if ro is not None:
332 pen += max(0.0, 0.20 - ro) ** 2 + max(0.0, ro - 0.35) ** 2
333 if rf is not None:
334 pen += max(0.0, rf - rf_cap) ** 2
335 return pen
336
337 if good:
338 best = min(good, key=_score)
339 print(f"AO/AF sweep: {len(good)} feasible point(s). Best composite row:\n {best}")
340 else:
341 print("AO/AF sweep: **no** point satisfied all tightened feasibility filters.")
342
343 print(
344 "\nInterpretation checklist:\n"
345 " (a) Cd asymmetry — compare Cd grid: if R and ΔP_F move strongly with f_O,f_F, Cd coupling matters.\n"
346 " (b) Fuel tank pressure lower bound — if lowering max_fuel_pressure_psi cap (narrower high-P bound)\n"
347 " shifts ΔP_F/Pc downward while holding MR/thrust, bound was limiting.\n"
348 " (c) Throat-fixed mdot — frozen A_throat ties mdot at Pc; cannot independently satisfy all jets without\n"
349 " compatible injector areas and ΔP split.\n"
350 " (d) AO/AF limits — if sweep never enters feasible set, geometry-pressure coupling dominates.\n"
351 " (e) Momentum coupling — R responds to √(ρ_O/ρ_F)·(v_O/v_F)² with MR tying mdots; fixing throat couples\n"
352 " mdot_sum leaving ΔP and areas to reconcile MR and R.\n"
353 )
354
355 print("\nBest geometry under exploration: **Layer-1 optimum above** (frozen throat + relaxed penalties).\n")
356
357
358if __name__ == "__main__":
359 main()
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
None _cd_sweep(PintleEngineConfig opt_cfg, float po_pa, float pf_pa, *, bool silent)
bool _feasible(float F, float MR, Optional[float] R, Optional[float] ro, Optional[float] rf, *, Tuple[float, float] f_band_o, float rf_cap)
Tuple[Optional[float], Optional[float]] _dp_ratios(Dict[str, Any] ev)
None _scale_discharge_inplace(DischargeConfig dc, float factor)
None _print_layer1_metrics(str title, PintleEngineConfig opt_cfg, Dict[str, Any] perf)
Tuple[List[Dict[str, Any]], List[Dict[str, Any]]] _ao_af_sweep(PintleEngineConfig opt_cfg, float po_pa, float pf_pa, *, float ratio_min, float ratio_max, int steps, bool silent, Tuple[float, float] f_band_o, float rf_cap)
PintleEngineConfig _apply_cd_factors(PintleEngineConfig cfg, float fo, float ff)