STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
audit_gemini_proposals.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Audit Gemini-style proposals without changing repo defaults.
4
51) High tank-pressure sweep (evaluate-only): geometry fixed near mission; sweep P_O,P_F up to ~1150 psi.
62) Layer 1 W_MOM sensitivity (75 / 300 / 1000 / 3000), smoke CMA, shared short budget.
73) Layer 1 with n_elements_O/F frozen (25 / 63), other impinging DOFs + pressures free.
8
9Run from repo root:
10 PYTHONPATH=. python scripts/audit_gemini_proposals.py
11 PYTHONPATH=. python scripts/audit_gemini_proposals.py --layer1-iters 8 # faster
12
13Uses configs/impinging_lox_ch4_8000N.yaml (deepcopy only).
14"""
15
16from __future__ import annotations
17
18import argparse
19import copy
20import math
21import sys
22from pathlib import Path
23from typing import Any, Dict, Optional, Tuple
24
25import numpy as np
26
27ROOT = Path(__file__).resolve().parents[1]
28if str(ROOT) not in sys.path:
29 sys.path.insert(0, str(ROOT))
30
31from engine.core.runner import PintleEngineRunner
32from engine.optimizer.injector_dp_penalty import injector_dp_ratios_from_eval_result
33from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization
34from engine.pipeline.io import load_config
35
36PSI_TO_PA = 6894.757293168361
37TARGET_F = 8000.0
38TARGET_MR = 3.5
39
40
41def ambient_pa(cfg) -> float:
42 """The engine solver's site ambient (engine.core.runner, the 1976 standard atmosphere)."""
43 from engine.core.runner import compute_ambient_pressure_from_elevation
44
45 el = float(getattr(cfg.environment, "elevation", 0.0) or 0.0)
46 return float(compute_ambient_pressure_from_elevation(el))
47
48
49def mission_metrics(result: Dict[str, Any]) -> Tuple[float, float, float, float, float, Optional[float]]:
50 """Returns F, MR, R_mom, r_dp_o, r_dp_f, Pc."""
51 F = float(result.get("F", float("nan")))
52 MR = float(result.get("MR", float("nan")))
53 diag = result.get("diagnostics") or {}
54 Rm = diag.get("momentum_ratio_R")
55 R_mom = float(Rm) if Rm is not None and np.isfinite(float(Rm)) else float("nan")
56 pc = float(result.get("Pc", float("nan")))
57 ro, rf = injector_dp_ratios_from_eval_result(pc, result)
58 return F, MR, R_mom, float(ro or np.nan), float(rf or np.nan), pc
59
60
62 F: float,
63 MR: float,
64 R_mom: float,
65 r_o: float,
66 r_f: float,
67 *,
68 thrust_band: float = 0.05,
69 mr_band: float = 0.05,
70 r_band: float = 0.10,
71 dp_lo: float = 0.20,
72 dp_hi: float = 0.35,
73) -> bool:
74 if not np.isfinite(F) or TARGET_F <= 0:
75 return False
76 if abs(F / TARGET_F - 1.0) > thrust_band:
77 return False
78 if not np.isfinite(MR) or TARGET_MR <= 0 or abs(MR / TARGET_MR - 1.0) > mr_band:
79 return False
80 if not np.isfinite(R_mom) or abs(R_mom - 1.0) > r_band:
81 return False
82 if not np.isfinite(r_o) or not np.isfinite(r_f):
83 return False
84 if not (dp_lo <= r_o <= dp_hi and dp_lo <= r_f <= dp_hi):
85 return False
86 return True
87
88
89def fuel_geometry_for_ratio(a_o: float, ratio_ao_af: float, n_f: int) -> float:
90 a_f = a_o / ratio_ao_af
91 return float(2.0 * math.sqrt(max(a_f, 1e-12) / (math.pi * max(n_f, 1))))
92
93
94def task1_high_pressure_sweep(cfg_base, p_lo: float, p_hi: float, n_steps: int) -> None:
95 print("\n" + "=" * 70)
96 print("TASK 1 — Tank pressure sweep (evaluate-only; ceilings not binding here)")
97 print(" Nominal YAML max LOX ~700 psi caps *optimizer* bounds, not physics evaluate().")
98 print(" Sweeping P_O,P_F directly up to high psi tests whether ΔP/Pc can enter [0.20,0.35]")
99 print(" while staying near F≈8000, MR≈3.5, R≈1 (same frozen throat geometry).")
100 print("=" * 70)
101
102 cfg = copy.deepcopy(cfg_base)
103 pa = ambient_pa(cfg)
104
105 n_o, d_o = 25, 0.00244
106 n_f = 63
107 ao_af = TARGET_MR / math.sqrt(cfg.fluids["oxidizer"].density / cfg.fluids["fuel"].density)
108 a_o = n_o * math.pi * (d_o / 2.0) ** 2
109 d_f = fuel_geometry_for_ratio(a_o, ao_af, n_f)
110
111 cfg.injector.geometry.oxidizer.n_elements = n_o
112 cfg.injector.geometry.oxidizer.d_jet = d_o
113 cfg.injector.geometry.fuel.n_elements = n_f
114 cfg.injector.geometry.fuel.d_jet = d_f
115
116 po_list = np.linspace(p_lo, p_hi, int(n_steps))
117 pf_list = np.linspace(p_lo, p_hi, int(n_steps))
118
119 hits_full = []
120 mission_band = [] # thrust/MR/R only (ΔP unconstrained)
121
122 runner = PintleEngineRunner(cfg)
123 n_ok = 0
124 for po in po_list:
125 for pf in pf_list:
126 try:
127 res = runner.evaluate(float(po) * PSI_TO_PA, float(pf) * PSI_TO_PA, P_ambient=pa, silent=True)
128 n_ok += 1
129 except Exception:
130 continue
131 F, MR, Rm, ro, rf, _pc = mission_metrics(res)
132 if full_mission_ok(F, MR, Rm, ro, rf):
133 hits_full.append((po, pf, F, MR, Rm, ro, rf))
134
135 thrust_ok = np.isfinite(F) and abs(F / TARGET_F - 1.0) <= 0.05
136 mr_ok = np.isfinite(MR) and abs(MR / TARGET_MR - 1.0) <= 0.05
137 r_ok = np.isfinite(Rm) and abs(Rm - 1.0) <= 0.10
138 if thrust_ok and mr_ok and r_ok and np.isfinite(rf) and np.isfinite(ro):
139 mission_band.append((rf, ro, po, pf, F, MR, Rm))
140
141 print(f"Grid {len(po_list)}×{len(pf_list)} = {len(po_list)*len(pf_list)} points, successful evaluates: {n_ok}")
142 print(
143 "Full-mission hits (|F|,|MR|,|R| gates + both ΔP/Pc ∈ [0.20,0.35]): "
144 f"{len(hits_full)}"
145 )
146 if hits_full:
147 for row in hits_full[:15]:
148 print(" ", row)
149
150 print(
151 f"\nAmong points with |F-8000|/8000≤5%, |MR-3.5|/3.5≤5%, |R-1|≤10% (ΔP unconstrained): "
152 f"N={len(mission_band)}"
153 )
154 if mission_band:
155 rfs = [t[0] for t in mission_band]
156 ros = [t[1] for t in mission_band]
157 in_dp_band = sum(1 for t in mission_band if 0.20 <= t[0] <= 0.35 and 0.20 <= t[1] <= 0.35)
158 print(f" ΔP_F/Pc min/med/max: {min(rfs):.4f} / {float(np.median(rfs)):.4f} / {max(rfs):.4f}")
159 print(f" ΔP_O/Pc min/med/max: {min(ros):.4f} / {float(np.median(ros)):.4f} / {max(ros):.4f}")
160 print(f" Count with BOTH streams in [0.20, 0.35]: {in_dp_band}/{len(mission_band)}")
161 mission_band.sort(key=lambda t: t[0])
162 rf, ro, po, pf, F, MR, Rm = mission_band[0]
163 print(
164 " Lowest fuel ΔP/Pc within mission kinematic band: "
165 f"ΔP_F/Pc={rf:.4f}, ΔP_O/Pc={ro:.4f}, P_O={po:.1f} psi, P_F={pf:.1f} psi, "
166 f"F={F:.1f}, MR={MR:.4f}, R={Rm:.4f}"
167 )
168
169
171 cfg.injector.geometry.oxidizer.n_elements = 25
172 cfg.injector.geometry.oxidizer.d_jet = 0.00244
173 cfg.injector.geometry.oxidizer.impingement_angle = 45.0
174 cfg.injector.geometry.oxidizer.spacing = 0.008
175 cfg.injector.geometry.fuel.n_elements = 63
176 cfg.injector.geometry.fuel.d_jet = 0.00105
177 cfg.injector.geometry.fuel.impingement_angle = 45.0
178 cfg.injector.geometry.fuel.spacing = 0.008
179
180
181def build_pressure_config(max_lox: float, max_fuel: float) -> Dict[str, Any]:
182 return {
183 "mode": "optimizer_controlled",
184 "max_lox_pressure_psi": float(max_lox),
185 "max_fuel_pressure_psi": float(max_fuel),
186 }
187
188
189def summarize_layer1(tag: str, results: Dict[str, Any]) -> None:
190 perf = results.get("performance") or {}
191 pc = float(perf.get("Pc", float("nan")))
192 F = float(perf.get("F", float("nan")))
193 MR = float(perf.get("MR", float("nan")))
194 Rm = perf.get("momentum_ratio_R")
195 R_mom = float(Rm) if Rm is not None and np.isfinite(float(Rm)) else float("nan")
196 ro = perf.get("injector_dp_ratio_O")
197 rf = perf.get("injector_dp_ratio_F")
198 if ro is None or rf is None or (not np.isfinite(float(ro))) or (not np.isfinite(float(rf))):
199 _ro, _rf = injector_dp_ratios_from_eval_result(pc, perf)
200 ro, rf = _ro, _rf
201 ro_f = float(ro) if ro is not None and np.isfinite(float(ro)) else float("nan")
202 rf_f = float(rf) if rf is not None and np.isfinite(float(rf)) else float("nan")
203
204 ok = full_mission_ok(F, MR, R_mom, ro_f, rf_f)
205
206 print(f"\n[{tag}]")
207 print(
208 f" F={F:.2f} N MR={MR:.4f} R={R_mom:.4f} "
209 f"ΔP_O/Pc={ro_f} ΔP_F/Pc={rf_f} full-mission-feasible={ok}"
210 )
211 print(f" P_O_opt_psi≈{perf.get('P_O_start_psi')} P_F_opt_psi≈{perf.get('P_F_start_psi')}")
212 failure_reasons = perf.get("failure_reasons") or []
213 if failure_reasons:
214 print(f" failure_reasons: {failure_reasons[:5]}")
215
216
218 *,
219 tag: str,
220 cfg_seed,
221 requirements: Dict[str, Any],
222 pressure_config: Dict[str, Any],
223 layer1_iters: int,
224) -> None:
225 cfg = copy.deepcopy(cfg_seed)
226
227 runner = PintleEngineRunner(copy.deepcopy(cfg))
228 _, results = run_layer1_optimization(
229 config_obj=cfg,
230 runner=runner,
231 requirements=requirements,
232 target_burn_time=float(requirements.get("target_burn_time", 6.0)),
233 tolerances={"thrust": 0.10, "apogee": 0.15},
234 pressure_config=pressure_config,
235 layer1_smoke=True,
236 layer1_max_iterations=int(layer1_iters),
237 layer1_cma_restarts=1,
238 )
239 summarize_layer1(tag, results)
240
241
242def main() -> None:
243 ap = argparse.ArgumentParser()
244 ap.add_argument("--layer1-iters", type=int, default=12, help="CMA generations per Layer-1 run (smoke)")
245 ap.add_argument("--skip-layer1", action="store_true")
246 args = ap.parse_args()
247
248 cfg_base = load_config(str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
249
250 # -------- Task 1 --------
251 task1_high_pressure_sweep(cfg_base, p_lo=450.0, p_hi=1150.0, n_steps=17)
252
253 if args.skip_layer1:
254 print("\n(--skip-layer1: W_MOM and frozen-n experiments not run)")
255 return
256
257 # Shared: raise *optimizer* tank bounds to 1200 psi (both) for these experiments only.
258 max_p = 1200.0
259 pressure_hi = build_pressure_config(max_p, max_p)
260
261 print("\n" + "=" * 70)
262 print("TASK 2 — W_MOM sensitivity (short smoke Layer 1)")
263 print(f" Tank caps in pressure_config: LOX/Fuel max = {max_p:.0f} psi")
264 print(f" layer1_max_iterations = {args.layer1_iters}, restarts = 1")
265 print("=" * 70)
266
267 for w_mom in (75.0, 300.0, 1000.0, 3000.0):
268 cfg_seed = copy.deepcopy(cfg_base)
270 req = cfg_seed.design_requirements.model_dump()
271 req["W_MOM"] = float(w_mom)
272 req["max_lox_tank_pressure_psi"] = max_p
273 req["max_fuel_tank_pressure_psi"] = max_p
275 tag=f"W_MOM={w_mom:g}",
276 cfg_seed=cfg_seed,
277 requirements=req,
278 pressure_config=pressure_hi,
279 layer1_iters=args.layer1_iters,
280 )
281
282 print("\n" + "=" * 70)
283 print("TASK 3 — Frozen n_elements_O=25, n_elements_F=63 (other DOFs free)")
284 print("=" * 70)
285 cfg_seed = copy.deepcopy(cfg_base)
287 req = cfg_seed.design_requirements.model_dump()
288 req["max_lox_tank_pressure_psi"] = max_p
289 req["max_fuel_tank_pressure_psi"] = max_p
290 fp = dict(req.get("frozen_parameters") or {})
291 fp["n_elements_O"] = 25
292 fp["n_elements_F"] = 63
293 req["frozen_parameters"] = fp
295 tag="frozen_n_only",
296 cfg_seed=cfg_seed,
297 requirements=req,
298 pressure_config=pressure_hi,
299 layer1_iters=args.layer1_iters,
300 )
301
302 print("\n" + "=" * 70)
303 print("TASK 4 — Interpretation printed inline per run (full-mission feasible?)")
304 print(" Full-mission gate: |F-8000|/8000≤5%, |MR-3.5|/3.5≤5%, |R-1|≤10%,")
305 print(" both ΔP_inj/Pc ∈ [0.20, 0.35].")
306 print("=" * 70)
307
308
309if __name__ == "__main__":
310 main()
#define max(a, b)
Definition Arduino.h:43
Dict[str, Any] build_pressure_config(float max_lox, float max_fuel)
None task1_high_pressure_sweep(cfg_base, float p_lo, float p_hi, int n_steps)
None run_layer1_case(*, str tag, cfg_seed, Dict[str, Any] requirements, Dict[str, Any] pressure_config, int layer1_iters)
None summarize_layer1(str tag, Dict[str, Any] results)
float fuel_geometry_for_ratio(float a_o, float ratio_ao_af, int n_f)
bool full_mission_ok(float F, float MR, float R_mom, float r_o, float r_f, *, float thrust_band=0.05, float mr_band=0.05, float r_band=0.10, float dp_lo=0.20, float dp_hi=0.35)
Tuple[float, float, float, float, float, Optional[float]] mission_metrics(Dict[str, Any] result)