STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
run_layer1_relaxed_momentum_cd_audit.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Part 2–3: free-throat Layer-1 with relaxed momentum_ratio_R bands + metrics/Cd reporting.
3
4Requirements keys (optimizer-only; no injector physics edits)::
5
6 impinging_momentum_R_min / impinging_momentum_R_max — ratio-space deadband for W_MOM hinge
7
8Runs Case A [0.85, 1.15] and Case B [0.75, 1.25], then ±10%% Cd scaling diagnostic on Case B optimum.
9
10Also prints Part 1 Cd model summary from YAML ``discharge`` + ``engine.core.discharge.cd_from_re``.
11"""
12
13from __future__ import annotations
14
15import argparse
16import copy
17import math
18import sys
19from pathlib import Path
20from typing import Any, Dict, Optional, Tuple
21
22import numpy as np
23
24ROOT = Path(__file__).resolve().parents[1]
25sys.path.insert(0, str(ROOT))
26
27PSI_TO_PA = 6894.76
28
29from engine.core.runner import PintleEngineRunner # noqa: E402
30from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization # noqa: E402
31from engine.pipeline.config_schemas import DischargeConfig # noqa: E402
32from engine.pipeline.io import load_config # noqa: E402
33
34
35def _clean_frozen(req: Dict[str, Any]) -> None:
36 fp = req.get("frozen_parameters")
37 if not isinstance(fp, dict):
38 fp = {}
39 else:
40 fp = {k: v for k, v in fp.items() if v is not None}
41 fp.pop("A_throat_mm2", None)
42 req["frozen_parameters"] = fp
43
44
45def _sf(x: Any) -> float:
46 try:
47 if x is None:
48 return float("nan")
49 v = float(x)
50 return v if math.isfinite(v) else float("nan")
51 except (TypeError, ValueError):
52 return float("nan")
53
54
55def _dp_ratios(perf: Dict[str, Any]) -> Tuple[float, float]:
56 ro = perf.get("injector_dp_ratio_O")
57 rf = perf.get("injector_dp_ratio_F")
58 if ro is not None and rf is not None:
59 return float(ro), float(rf)
60 diag = perf.get("diagnostics") or {}
61 pc = float(perf.get("Pc", float("nan")))
62 dpo = diag.get("delta_p_injector_O")
63 dpf = diag.get("delta_p_injector_F")
64 ro_v = float(dpo) / pc if dpo is not None and pc > 0 else float("nan")
65 rf_v = float(dpf) / pc if dpf is not None and pc > 0 else float("nan")
66 return ro_v, rf_v
67
68
69def _cds(perf: Dict[str, Any]) -> Tuple[float, float]:
70 diag = perf.get("diagnostics") or {}
71 co = perf.get("Cd_O", diag.get("Cd_O"))
72 cf = perf.get("Cd_F", diag.get("Cd_F"))
73 return _sf(co), _sf(cf)
74
75
76def _scale_dc(dc: DischargeConfig, factor: float) -> None:
77 dc.Cd_inf = float(np.clip(dc.Cd_inf * factor, 1e-6, 1.0))
78 dc.Cd_min = float(min(dc.Cd_min * factor, dc.Cd_inf))
79
80
81def _cd_scaled_cfg(base_cfg: Any, fo: float, ff: float) -> Any:
82 c = copy.deepcopy(base_cfg)
83 _scale_dc(c.discharge["oxidizer"], fo)
84 _scale_dc(c.discharge["fuel"], ff)
85 return c
86
87
89 perf: Dict[str, Any],
90 *,
91 r_lo: float,
92 r_hi: float,
93 dp_lo: float,
94 dp_hi: float,
95) -> bool:
96 F = _sf(perf.get("F"))
97 MR = _sf(perf.get("MR"))
98 R = _sf(perf.get("momentum_ratio_R"))
99 ro, rf = _dp_ratios(perf)
100 ok_f = abs(F - 8000.0) / 8000.0 <= 0.05
101 ok_mr = abs(MR - 3.5) / 3.5 <= 0.05
102 ok_r = r_lo <= R <= r_hi
103 ok_dp = (
104 dp_lo <= ro <= dp_hi
105 and dp_lo <= rf <= dp_hi
106 and math.isfinite(ro)
107 and math.isfinite(rf)
108 )
109 return bool(ok_f and ok_mr and ok_r and ok_dp)
110
111
112def _print_cd_audit(cfg_path: Path) -> None:
113 cfg = load_config(str(cfg_path))
114 ox = cfg.discharge["oxidizer"]
115 fu = cfg.discharge["fuel"]
116 print("\n" + "=" * 72)
117 print("PART 1 — Discharge coefficient model (audit)")
118 print("=" * 72)
119 print(
120 "Solver uses ``engine.core.discharge.cd_from_re``:\n"
121 " Cd(Re) = Cd_inf - a_Re / sqrt(Re) (Re > 0; else Cd_min)\n"
122 " then optional pressure/temperature multipliers if enabled;\n"
123 " finally clip to [Cd_min, Cd_inf].\n"
124 )
125 print(f"YAML defaults — oxidizer: Cd_inf={ox.Cd_inf} Cd_min={ox.Cd_min} a_Re={ox.a_Re}")
126 print(f"YAML defaults — fuel: Cd_inf={fu.Cd_inf} Cd_min={fu.Cd_min} a_Re={fu.a_Re}")
127 print(
128 "\nLOX vs CH4 Cd differ because ``discharge`` entries differ (Cd_inf/Cd_min/a_Re/Re path);\n"
129 "the **same function** is applied per stream with its own ``DischargeConfig``.\n"
130 )
131
132
133def _print_case(name: str, perf: Dict[str, Any], opt_cfg: Any, *, r_band: Tuple[float, float]) -> None:
134 ro, rf = _dp_ratios(perf)
135 cdo, cdf = _cds(perf)
136 ig = opt_cfg.injector.geometry
137 cg = getattr(opt_cfg, "chamber_geometry", None)
138 at = getattr(cg, "A_throat", None) if cg is not None else None
139 at_mm2 = float(at) * 1e6 if at is not None else float("nan")
140
141 ae_o = perf.get("A_eff_O") or (perf.get("diagnostics") or {}).get("A_eff_O")
142 ae_f = perf.get("A_eff_F") or (perf.get("diagnostics") or {}).get("A_eff_F")
143 aeff_ratio = float(ae_o) / float(ae_f) if ae_o and ae_f and float(ae_f) > 0 else float("nan")
144
145 print(f"\n{'=' * 72}\n{name}\n{'=' * 72}")
146 print(f"A_throat [mm²]: {at_mm2:.4f}")
147 print(f"Thrust [N]: {_sf(perf.get('F')):.4g}")
148 print(f"MR: {_sf(perf.get('MR')):.6g}")
149 print(f"momentum_ratio_R: {_sf(perf.get('momentum_ratio_R')):.6g} (optimizer band {r_band})")
150 print(f"ΔP_inj_O/Pc: {ro:.6g}")
151 print(f"ΔP_inj_F/Pc: {rf:.6g}")
152 print(f"Cd_O / Cd_F: {cdo:.6g} / {cdf:.6g}")
153 print(f"A_geom_O/A_geom_F: {perf.get('geom_ao_af')}")
154 print(f"A_eff_O/A_eff_F: {aeff_ratio:.6g}")
155 print(f"P_O_psi / P_F_psi: {perf.get('P_O_start_psi')} / {perf.get('P_F_start_psi')}")
156 print(
157 f"Injector LOX: n={int(ig.oxidizer.n_elements)} d_jet={ig.oxidizer.d_jet*1000:.4f} mm "
158 f"spacing={ig.oxidizer.spacing*1000:.4f} mm angle={ig.oxidizer.impingement_angle:.2f}°"
159 )
160 print(
161 f"Injector F: n={int(ig.fuel.n_elements)} d_jet={ig.fuel.d_jet*1000:.4f} mm "
162 f"spacing={ig.fuel.spacing*1000:.4f} mm angle={ig.fuel.impingement_angle:.2f}°"
163 )
164
165
166def _cd_sensitivity_grid(opt_cfg: Any, po_pa: float, pf_pa: float) -> None:
167 runner_ref = PintleEngineRunner(copy.deepcopy(opt_cfg))
168 pa = runner_ref._get_ambient_pressure(None)
169 print("\n" + "=" * 72)
170 print("PART 1 (continued) — ±10% Cd_O / Cd_F diagnostic (evaluate only)")
171 print("=" * 72)
172 print(f"{'f_O':>6} {'f_F':>6} {'R':>8} {'dP_F/Pc':>10} {'MR':>8} {'F[N]':>10}")
173 print("-" * 52)
174 for fo in (0.9, 1.0, 1.1):
175 for ff in (0.9, 1.0, 1.1):
176 rcfg = _cd_scaled_cfg(opt_cfg, fo, ff)
177 ev = PintleEngineRunner(rcfg).evaluate(po_pa, pf_pa, P_ambient=pa, silent=True)
178 diag = ev.get("diagnostics") or {}
179 R = diag.get("momentum_ratio_R")
180 pc = float(ev.get("Pc", float("nan")))
181 dpf = diag.get("delta_p_injector_F")
182 rf = float(dpf) / pc if dpf is not None and pc > 0 else float("nan")
183 print(
184 f"{fo:6.2f} {ff:6.2f} {_sf(R):8.4f} {rf:10.4f} {_sf(ev.get('MR')):8.4f} "
185 f"{_sf(ev.get('F')):10.2f}"
186 )
187
188
189def main() -> None:
190 ap = argparse.ArgumentParser(description=__doc__)
191 ap.add_argument("--config", type=str, default=str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
192 ap.add_argument("--a-throat-center-mm2", type=float, default=2443.263433372886)
193 ap.add_argument("--throat-span-fraction", type=float, default=0.125)
194 ap.add_argument("--layer1-max-iterations", type=int, default=150)
195 ap.add_argument("--layer1-cma-restarts", type=int, default=1)
196 args = ap.parse_args()
197
198 cfg_path = Path(args.config)
199 center = float(args.a_throat_center_mm2)
200 frac = float(args.throat_span_fraction)
201 dp_band = (0.15, 0.40)
202
203 _print_cd_audit(cfg_path)
204
205 scenarios = [
206 ("CASE A — R band [0.85, 1.15]", 0.85, 1.15),
207 ("CASE B — R band [0.75, 1.25]", 0.75, 1.25),
208 ]
209
210 results_store: Dict[str, Any] = {}
211
212 print("\n" + "=" * 72)
213 print("PART 2–3 — Layer-1 with relaxed R hinge")
214 print("=" * 72)
215
216 for title, r_lo, r_hi in scenarios:
217 cfg = load_config(str(cfg_path))
218 req = cfg.design_requirements.model_dump()
219 _clean_frozen(req)
220
221 req["layer1_A_throat_mm2_min"] = center * (1.0 - frac)
222 req["layer1_A_throat_mm2_max"] = center * (1.0 + frac)
223 req["injector_dp_ratio_O_min"] = dp_band[0]
224 req["injector_dp_ratio_O_max"] = dp_band[1]
225 req["injector_dp_ratio_F_min"] = dp_band[0]
226 req["injector_dp_ratio_F_max"] = dp_band[1]
227 req["W_MOM"] = 300.0
228 req["W_geom_ao_af_momentum"] = 2800.0
229 req["impinging_momentum_R_min"] = float(r_lo)
230 req["impinging_momentum_R_max"] = float(r_hi)
231
232 pcfg = {
233 "mode": "optimizer_controlled",
234 "max_lox_pressure_psi": float(req["max_lox_tank_pressure_psi"]),
235 "max_fuel_pressure_psi": float(req["max_fuel_tank_pressure_psi"]),
236 }
237
238 opt_cfg, results = run_layer1_optimization(
239 copy.deepcopy(cfg),
240 PintleEngineRunner(copy.deepcopy(cfg)),
241 req,
242 target_burn_time=float(req.get("target_burn_time", 6.0)),
243 tolerances={"thrust": 0.10, "apogee": 0.15},
244 pressure_config=pcfg,
245 layer1_smoke=False,
246 layer1_max_iterations=int(args.layer1_max_iterations),
247 layer1_cma_restarts=int(args.layer1_cma_restarts),
248 )
249 perf = results.get("performance") or {}
250 key = "A" if r_lo >= 0.85 else "B"
251 results_store[key] = {"perf": perf, "opt_cfg": opt_cfg, "r_band": (r_lo, r_hi)}
252
253 _print_case(title, perf, opt_cfg, r_band=(r_lo, r_hi))
254
255 joint = _mission_ok(perf, r_lo=r_lo, r_hi=r_hi, dp_lo=dp_band[0], dp_hi=dp_band[1])
256 print(f"\n** Mission gates (F±5%, MR±5%, R∈[{r_lo},{r_hi}], both ΔP∈{dp_band}): "
257 f"{'YES' if joint else 'NO'} **")
258
259 print("\n" + "=" * 72)
260 print("Cd table — best candidates (Cases A & B)")
261 print("=" * 72)
262 print(f"{'case':>6} {'Cd_O':>8} {'Cd_F':>8}")
263 print("-" * 26)
264 for key, lab in ("A", "A"), ("B", "B"):
265 if key not in results_store:
266 continue
267 perf = results_store[key]["perf"]
268 cdo, cdf = _cds(perf)
269 print(f"{lab:>6} {cdo:8.4f} {cdf:8.4f}")
270
271 # Sensitivity on Case B optimum (typically more slack)
272 if "B" in results_store:
273 perf_b = results_store["B"]["perf"]
274 cfg_b = results_store["B"]["opt_cfg"]
275 po = float(perf_b["P_O_start_psi"]) * PSI_TO_PA
276 pf = float(perf_b["P_F_start_psi"]) * PSI_TO_PA
277 _cd_sensitivity_grid(cfg_b, po, pf)
278
279 print("\n" + "=" * 72)
280 print("PART 4 — Dominant constraint (heuristic)")
281 print("=" * 72)
282 print(
283 "If ΔP_inj_F/Pc ≫ 0.40 while LOX sits in-band: **(b) fuel injector pressure drop** / "
284 "tank–Pc split dominates.\n"
285 "If throat + mdot coupling prevents lowering fuel ΔP without breaking thrust/MR: **(c)**.\n"
286 "Cd asymmetry **(a)**: check sensitivity grid — strong motion of R/MR/ΔP_F with f_O,f_F supports "
287 "**audit-only** asymmetry.\n"
288 "AO/AF limits **(d)** / momentum **(e)**: W_geom + R hinge guide geometry; relaxed R reduces **(e)** "
289 "pressure on the optimizer.\n"
290 )
291
292 best_key = "B" if "B" in results_store else "A"
293 bc = results_store[best_key]["opt_cfg"]
294 ig = bc.injector.geometry
295 print(
296 "Best candidate geometry (prefer Case B if present — otherwise A):\n"
297 f" LOX n={int(ig.oxidizer.n_elements)} d={ig.oxidizer.d_jet*1000:.4f} mm "
298 f"sp={ig.oxidizer.spacing*1000:.3f} mm θ={ig.oxidizer.impingement_angle:.2f}°\n"
299 f" F n={int(ig.fuel.n_elements)} d={ig.fuel.d_jet*1000:.4f} mm "
300 f"sp={ig.fuel.spacing*1000:.3f} mm θ={ig.fuel.impingement_angle:.2f}°"
301 )
302
303
304if __name__ == "__main__":
305 main()
#define min(a, b)
Definition Arduino.h:42
None _cd_sensitivity_grid(Any opt_cfg, float po_pa, float pf_pa)
Tuple[float, float] _cds(Dict[str, Any] perf)
None _scale_dc(DischargeConfig dc, float factor)
None _print_case(str name, Dict[str, Any] perf, Any opt_cfg, *, Tuple[float, float] r_band)
bool _mission_ok(Dict[str, Any] perf, *, float r_lo, float r_hi, float dp_lo, float dp_hi)
Any _cd_scaled_cfg(Any base_cfg, float fo, float ff)
Tuple[float, float] _dp_ratios(Dict[str, Any] perf)