STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
sensitivity_mr_r_dp_coupling.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Sensitivity sweep: MR, momentum_ratio_R, ΔP/Pc vs AO/AF, tank pressures, Cd_O/Cd_F.
3
4Uses frozen chamber from configs/impinging_lox_ch4_8000N.yaml (no YAML edits).
5Filters samples near thrust ~8000 N and R ~ 1; reports MR and ΔP_O/Pc, ΔP_F/Pc.
6
7Run from repo root: PYTHONPATH=. python scripts/sensitivity_mr_r_dp_coupling.py
8"""
9
10from __future__ import annotations
11
12import copy
13import math
14import sys
15from dataclasses import dataclass
16from pathlib import Path
17
18import numpy as np
19
20ROOT = Path(__file__).resolve().parents[1]
21if str(ROOT) not in sys.path:
22 sys.path.insert(0, str(ROOT))
23
24from engine.core.runner import PintleEngineRunner
25from engine.pipeline.io import load_config
26
27PSI_TO_PA = 6894.757293168361
28TARGET_F = 8000.0
29TARGET_MR = 3.5
30
31
32def ambient_pa_from_config(cfg) -> float:
33 """The engine solver's site ambient (engine.core.runner, the 1976 standard atmosphere)."""
34 from engine.core.runner import compute_ambient_pressure_from_elevation
35
36 env = getattr(cfg, "environment", None)
37 el = float(getattr(env, "elevation", 0.0) or 0.0)
38 return float(compute_ambient_pressure_from_elevation(el))
39
40
41def apply_cd_multipliers(cfg, m_o: float, m_f: float) -> None:
42 """Scale Cd_inf and Cd_min for oxidizer/fuel discharge (clamp Cd_inf <= 1)."""
43 for side, mult in (("oxidizer", m_o), ("fuel", m_f)):
44 d = cfg.discharge[side]
45 ci = float(d.Cd_inf) * mult
46 cm = float(d.Cd_min) * mult
47 ci = min(0.999, max(cm + 1e-6, ci))
48 d.Cd_inf = ci
49 d.Cd_min = min(cm, d.Cd_inf * 0.99)
50
51
52def fuel_geometry_for_ratio(a_o: float, ratio_ao_af: float, n_f: int) -> float:
53 """Return fuel jet diameter [m] for A_F = A_O / ratio_ao_af with fixed n_F."""
54 a_f = a_o / ratio_ao_af
55 return float(2.0 * math.sqrt(max(a_f, 1e-12) / (math.pi * max(n_f, 1))))
56
57
58@dataclass
59class Row:
60 ao_af: float
61 p_o_psi: float
62 p_f_psi: float
63 cd_o_mult: float
64 cd_f_mult: float
65 F: float
66 MR: float
67 R_mom: float
68 pc: float
69 r_dp_o: float
70 r_dp_f: float
71 cd_o: float
72 cd_f: float
73
74
75def main() -> None:
76 cfg_base = load_config(str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
77 p_amb = ambient_pa_from_config(cfg_base)
78
79 rho_o = float(cfg_base.fluids["oxidizer"].density)
80 rho_f = float(cfg_base.fluids["fuel"].density)
81 sqrt_density_ratio = math.sqrt(rho_o / rho_f)
82 ao_af_for_mr35_at_R1 = TARGET_MR / sqrt_density_ratio
83
84 # Fixed oxidizer geometry (matches prior diagnostic scale)
85 n_o = 25
86 d_o = 0.00244
87 a_o = n_o * math.pi * (d_o / 2.0) ** 2
88 n_f = 63
89
90 # Grid size ~7*5*5*9 ≈ 1600 evaluates (~few minutes locally)
91 ao_af_list = np.linspace(1.65, 2.55, 7)
92 p_o_grid = np.linspace(400.0, 600.0, 5)
93 p_f_grid = np.linspace(500.0, 780.0, 5)
94 cd_mults = [0.9, 1.0, 1.1]
95
96 rows: list[Row] = []
97 n_eval = 0
98 n_fail = 0
99
100 for m_o in cd_mults:
101 for m_f in cd_mults:
102 for ao_af in ao_af_list:
103 d_f = fuel_geometry_for_ratio(a_o, float(ao_af), n_f)
104 if not (0.00035 <= d_f <= 0.0065):
105 continue
106 for p_o_psi in p_o_grid:
107 for p_f_psi in p_f_grid:
108 cfg = copy.deepcopy(cfg_base)
109 apply_cd_multipliers(cfg, m_o, m_f)
110 cfg.injector.geometry.oxidizer.n_elements = n_o
111 cfg.injector.geometry.oxidizer.d_jet = d_o
112 cfg.injector.geometry.fuel.n_elements = n_f
113 cfg.injector.geometry.fuel.d_jet = d_f
114
115 try:
116 runner = PintleEngineRunner(cfg)
117 res = runner.evaluate(
118 float(p_o_psi) * PSI_TO_PA,
119 float(p_f_psi) * PSI_TO_PA,
120 P_ambient=p_amb,
121 silent=True,
122 )
123 n_eval += 1
124 except Exception:
125 n_fail += 1
126 continue
127
128 F = float(res.get("F", float("nan")))
129 MR = float(res.get("MR", float("nan")))
130 pc = float(res.get("Pc", float("nan")))
131 diag = res.get("diagnostics") or {}
132 R_mom = float(diag.get("momentum_ratio_R", float("nan")))
133 dp_o = diag.get("delta_p_injector_O")
134 dp_f = diag.get("delta_p_injector_F")
135 cd_o = float(diag.get("Cd_O", float("nan")))
136 cd_f = float(diag.get("Cd_F", float("nan")))
137 if (
138 not np.isfinite(pc)
139 or pc <= 0
140 or dp_o is None
141 or dp_f is None
142 or not np.isfinite(F)
143 or not np.isfinite(MR)
144 ):
145 continue
146
147 rows.append(
148 Row(
149 ao_af=float(ao_af),
150 p_o_psi=float(p_o_psi),
151 p_f_psi=float(p_f_psi),
152 cd_o_mult=float(m_o),
153 cd_f_mult=float(m_f),
154 F=F,
155 MR=MR,
156 R_mom=R_mom,
157 pc=pc,
158 r_dp_o=float(dp_o) / pc,
159 r_dp_f=float(dp_f) / pc,
160 cd_o=cd_o,
161 cd_f=cd_f,
162 )
163 )
164
165 print("=== Sweep summary ===")
166 print(f"Ambient P_a used in evaluate: {p_amb:.2f} Pa ({p_amb/101325:.4f} atm)")
167 print(f"Fixed oxidizer: n_O={n_o}, d_O={d_o*1000:.3f} mm, A_O={a_o*1e6:.4f} mm²")
168 print(f"Fuel elements n_F={n_f}; d_F recomputed per AO/AF")
169 print(f"rho_O/rho_F = {rho_o/rho_f:.4f}, sqrt(rho_O/rho_F) = {sqrt_density_ratio:.4f}")
170 print(f"AO/AF needed for MR≈{TARGET_MR} if R=1 exactly (MR=R*sqrt(rho_O/rho_F)*AO/AF → AO/AF={TARGET_MR}/(R*sqrt…)): {ao_af_for_mr35_at_R1:.4f}")
171 print(f"Successful evaluates stored: {len(rows)}, exceptions: {n_fail}, total attempts: {n_eval}")
172
173 # Filters (tight → loose)
174 def filt(rel_F: float, band_R: float, mr_lo: float, mr_hi: float) -> list[Row]:
175 out = []
176 for r in rows:
177 if abs(r.F / TARGET_F - 1.0) > rel_F:
178 continue
179 if not np.isfinite(r.R_mom):
180 continue
181 if abs(r.R_mom - 1.0) > band_R:
182 continue
183 if r.MR < mr_lo or r.MR > mr_hi:
184 continue
185 out.append(r)
186 return out
187
188 scenarios = [
189 ("strict", 0.03, 0.06, 3.45, 3.55),
190 ("medium", 0.05, 0.08, 3.40, 3.60),
191 ("loose", 0.08, 0.10, 3.30, 3.70),
192 ]
193
194 for name, rf, br, ml, mh in scenarios:
195 sel = filt(rf, br, ml, mh)
196 print(f"\n--- Filter [{name}]: |F/F_tgt-1|<={rf}, |R-1|<={br}, MR∈[{ml},{mh}] → N={len(sel)} ---")
197 if not sel:
198 continue
199 rf_vals = [x.r_dp_f for x in sel]
200 ro_vals = [x.r_dp_o for x in sel]
201 print(f" ΔP_F/Pc: min={min(rf_vals):.4f} med={np.median(rf_vals):.4f} max={max(rf_vals):.4f}")
202 print(f" ΔP_O/Pc: min={min(ro_vals):.4f} med={np.median(ro_vals):.4f} max={max(ro_vals):.4f}")
203 print(f" MR: min={min(x.MR for x in sel):.4f} med={np.median([x.MR for x in sel]):.4f} max={max(x.MR for x in sel):.4f}")
204 below = sum(1 for x in sel if x.r_dp_f <= 0.35)
205 above = sum(1 for x in sel if x.r_dp_f > 0.35)
206 print(f" Fuel ΔP/Pc<=0.35: {below}/{len(sel)} ({100*below/len(sel):.1f}%), >0.35: {above}")
207 # Example lowest fuel dp
208 sel_sorted = sorted(sel, key=lambda x: x.r_dp_f)
209 ex = sel_sorted[0]
210 print(
211 f" Lowest ΔP_F/Pc sample: ΔP_F/Pc={ex.r_dp_f:.4f}, ΔP_O/Pc={ex.r_dp_o:.4f}, "
212 f"MR={ex.MR:.4f}, R={ex.R_mom:.4f}, F={ex.F:.1f}, AO/AF={ex.ao_af:.3f}, "
213 f"P_O={ex.p_o_psi:.0f} psi, P_F={ex.p_f_psi:.0f} psi, Cd×=({ex.cd_o_mult:.2f},{ex.cd_f_mult:.2f})"
214 )
215
216 # Broader: R~1 and F~8000 only (ignore MR band) — shows MR drift when AO/AF wrong
217 loose_rf = 0.06
218 loose_br = 0.08
219 sel2 = [r for r in rows if np.isfinite(r.R_mom) and abs(r.R_mom - 1.0) <= loose_br and abs(r.F / TARGET_F - 1.0) <= loose_rf]
220 print(f"\n--- MR distribution when only |F-8000|/8000<= {loose_rf} and |R-1|<={loose_br} (no MR filter): N={len(sel2)} ---")
221 if sel2:
222 mrs = [x.MR for x in sel2]
223 print(f" MR min/med/max: {min(mrs):.4f} / {np.median(mrs):.4f} / {max(mrs):.4f}")
224 pred = [sqrt_density_ratio * x.ao_af for x in sel2]
225 err = [abs(sel2[i].MR - pred[i]) for i in range(len(sel2))]
226 print(f" |MR - sqrt(rho_O/rho_F)*AO/AF| median: {np.median(err):.4f} (injector closure breaks pure analytic relation)")
227
228
229if __name__ == "__main__":
230 main()
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
None apply_cd_multipliers(cfg, float m_o, float m_f)
float fuel_geometry_for_ratio(float a_o, float ratio_ao_af, int n_f)