STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
sensitivity_cd_assumptions_layer1.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Cd **design-assumption** sensitivity (Layer‑1): vary ``Cd_inf`` / ``Cd_min`` only.
3
4Does **not** change injector equations (``engine/core/injectors/impinging.py`` unchanged).
5Does **not** commit new Cd defaults — cases override config in-memory before ``run_layer1_optimization``.
6
7Cases (assume ``Cd_*`` applies to oxidizer vs fuel discharge caps::
8
9 A baseline: Cd_O = 0.40, Cd_F = 0.30
10 B: Cd_O = 0.60, Cd_F = 0.60
11 C: Cd_O = 0.70, Cd_F = 0.70
12 D: Cd_O = 0.60, Cd_F = 0.70
13
14``Cd_min`` per stream preserves the oxidizer:fuel baseline **ratio**
15``Cd_min / Cd_inf`` from ``configs/impinging_lox_ch4_8000N.yaml`` (0.15/0.40 and 0.2/0.3).
16
17Layer‑1 targets (via requirements):
18
19- Thrust target 8000 N, MR target 3.5
20- Impinging R hinge [0.93, 1.07]
21- ΔP_inj/Pc hinge **both streams** [0.15, 0.40]
22- Tank pressures co‑optimized within CLI bands
23- Default throat band: Region B [2520, 2600] mm² ; optional ±12.5% around YAML ``A_throat``
24
25Outputs one summary CSV plus per-console case blocks.
26
27Example::
28
29 python scripts/sensitivity_cd_assumptions_layer1.py \\
30 --output-csv output/sensitivity_cd_layer1_summary.csv
31
32"""
33
34from __future__ import annotations
35
36import argparse
37import copy
38import csv
39import math
40import sys
41from pathlib import Path
42from typing import Any, Dict, List, Tuple
43
44import numpy as np
45
46ROOT = Path(__file__).resolve().parents[1]
47sys.path.insert(0, str(ROOT))
48
49from engine.core.runner import PintleEngineRunner # noqa: E402
50from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization # noqa: E402
51from engine.pipeline.io import load_config # noqa: E402
52
53F_TARGET = 8000.0
54MR_TARGET = 3.5
55# Baseline ratios from configs/impinging_lox_ch4_8000N.yaml discharge blocks
56_RATIO_CD_MIN_INF_O = 0.15 / 0.40
57_RATIO_CD_MIN_INF_F = 0.2 / 0.30
58
59CASES: List[Tuple[str, float, float]] = [
60 ("A_baseline", 0.40, 0.30),
61 ("B", 0.60, 0.60),
62 ("C", 0.70, 0.70),
63 ("D", 0.60, 0.70),
64]
65
66
67def _strip_none_fp(fp: Dict[str, Any]) -> Dict[str, Any]:
68 return {k: v for k, v in fp.items() if v is not None}
69
70
71def _apply_throat_mm2(cfg: Any, mm2: float) -> None:
72 cg = cfg.chamber_geometry
73 if cg is None:
74 raise ValueError("chamber_geometry required")
75 At = float(mm2) * 1.0e-6
76 eps = float(cg.expansion_ratio)
77 Ae = eps * At
78 cg.A_throat = At
79 cg.A_exit = Ae
80 cg.exit_diameter = 2.0 * math.sqrt(max(Ae, 1e-30) / math.pi)
81
82
83def _apply_cd_caps(cfg: Any, cd_o_inf: float, cd_f_inf: float) -> Tuple[float, float, float, float]:
84 ox = cfg.discharge["oxidizer"]
85 fu = cfg.discharge["fuel"]
86 cd_o_min = float(np.clip(cd_o_inf * _RATIO_CD_MIN_INF_O, 0.05, cd_o_inf - 1e-6))
87 cd_f_min = float(np.clip(cd_f_inf * _RATIO_CD_MIN_INF_F, 0.05, cd_f_inf - 1e-6))
88 cfg.discharge["oxidizer"] = ox.model_copy(update={"Cd_inf": float(cd_o_inf), "Cd_min": cd_o_min})
89 cfg.discharge["fuel"] = fu.model_copy(update={"Cd_inf": float(cd_f_inf), "Cd_min": cd_f_min})
90 return cd_o_inf, cd_o_min, cd_f_inf, cd_f_min
91
92
94 base_cfg_path: Path,
95 *,
96 throat_lo_mm2: float,
97 throat_hi_mm2: float,
98 p_o_lo: float,
99 p_o_hi: float,
100 p_f_lo: float,
101 p_f_hi: float,
102 max_lox_psi: float,
103 max_fuel_psi: float,
104) -> Tuple[Any, Dict[str, Any], Dict[str, Any]]:
105 cfg = load_config(str(base_cfg_path))
106 mid_mm2 = 0.5 * (float(throat_lo_mm2) + float(throat_hi_mm2))
107 _apply_throat_mm2(cfg, mid_mm2)
108 if cfg.chamber_geometry is not None:
109 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
110
111 ig0 = cfg.injector.geometry
112 ig0.oxidizer.n_elements = 68
113 ig0.oxidizer.d_jet = 1.424e-3
114 ig0.oxidizer.spacing = 5.901e-3
115 ig0.oxidizer.impingement_angle = 63.15
116 ig0.fuel.n_elements = 32
117 ig0.fuel.d_jet = 1.392e-3
118 ig0.fuel.spacing = 4.118e-3
119 ig0.fuel.impingement_angle = 75.05
120
121 req = cfg.design_requirements.model_dump()
122 fp = _strip_none_fp(dict(req.get("frozen_parameters") or {}))
123 fp.pop("A_throat_mm2", None)
124 fp.pop("P_O_start_psi", None)
125 fp.pop("P_F_start_psi", None)
126 if cfg.chamber_geometry is not None:
127 fp["expansion_ratio"] = float(cfg.chamber_geometry.expansion_ratio)
128 fp["Lstar_mm"] = float(cfg.chamber_geometry.Lstar) * 1000.0
129 req["frozen_parameters"] = fp
130
131 req["layer1_A_throat_mm2_min"] = float(throat_lo_mm2)
132 req["layer1_A_throat_mm2_max"] = float(throat_hi_mm2)
133 req["layer1_P_O_start_psi_min"] = float(p_o_lo)
134 req["layer1_P_O_start_psi_max"] = float(p_o_hi)
135 req["layer1_P_F_start_psi_min"] = float(p_f_lo)
136 req["layer1_P_F_start_psi_max"] = float(p_f_hi)
137 req["max_lox_tank_pressure_psi"] = float(max_lox_psi)
138 req["max_fuel_tank_pressure_psi"] = float(max_fuel_psi)
139
140 req["injector_dp_ratio_O_min"] = 0.15
141 req["injector_dp_ratio_O_max"] = 0.40
142 req["injector_dp_ratio_F_min"] = 0.15
143 req["injector_dp_ratio_F_max"] = 0.40
144
145 req["impinging_momentum_R_min"] = 0.93
146 req["impinging_momentum_R_max"] = 1.07
147
148 req["W_MOM"] = 1600.0
149 req["W_geom_ao_af_momentum"] = 6500.0
150 req["W_DP"] = 380.0
151 req["W_DP_O"] = 820.0
152 req["W_DP_F"] = 820.0
153
154 req["target_thrust"] = F_TARGET
155 req["optimal_of_ratio"] = MR_TARGET
156
157 req["require_stable_state"] = False
158 req["min_stability_score"] = min(float(req.get("min_stability_score", 0.75)), 0.58)
159 req["min_stability_margin"] = min(float(req.get("min_stability_margin", 1.2)), 1.05)
160 req["layer1_infeasibility_gate_eps"] = 2.0e-3
161
162 pcfg = {
163 "mode": "optimizer_controlled",
164 "max_lox_pressure_psi": float(max_lox_psi),
165 "max_fuel_pressure_psi": float(max_fuel_psi),
166 }
167 return cfg, req, pcfg
168
169
170def main() -> None:
171 ap = argparse.ArgumentParser(description=__doc__)
172 ap.add_argument("--config", type=str, default=str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
173 ap.add_argument("--layer1-max-iterations", type=int, default=400)
174 ap.add_argument("--layer1-cma-restarts", type=int, default=2)
175 ap.add_argument(
176 "--athroat-mode",
177 type=str,
178 choices=("region_b", "pct125"),
179 default="region_b",
180 help="region_b=[2520,2600] mm²; pct125=±12.5%% around YAML nominal A_throat_mm2",
181 )
182 ap.add_argument("--p-o-min-psi", type=float, default=580.0)
183 ap.add_argument("--p-o-max-psi", type=float, default=700.0)
184 ap.add_argument("--p-f-min-psi", type=float, default=350.0)
185 ap.add_argument("--p-f-max-psi", type=float, default=500.0)
186 ap.add_argument("--max-lox-pressure-psi", type=float, default=700.0)
187 ap.add_argument("--max-fuel-pressure-psi", type=float, default=500.0)
188 ap.add_argument("--output-csv", type=str, default=str(ROOT / "output/sensitivity_cd_layer1_summary.csv"))
189 args = ap.parse_args()
190
191 cfg_path = Path(args.config)
192 if not cfg_path.is_absolute():
193 cfg_path = ROOT / cfg_path
194
195 probe = load_config(str(cfg_path))
196 if args.athroat_mode == "region_b":
197 throat_lo, throat_hi = 2520.0, 2600.0
198 else:
199 at0 = float(probe.chamber_geometry.A_throat) * 1e6
200 throat_lo, throat_hi = at0 * 0.875, at0 * 1.125
201 layer1_logger_info = "A_throat band ±12.5% from nominal {:.1f} mm² → [{:.1f}, {:.1f}]".format(
202 at0, throat_lo, throat_hi
203 )
204 print(layer1_logger_info)
205
206 cfg0, req, pcfg = _build_req_and_cfg(
207 cfg_path,
208 throat_lo_mm2=throat_lo,
209 throat_hi_mm2=throat_hi,
210 p_o_lo=args.p_o_min_psi,
211 p_o_hi=args.p_o_max_psi,
212 p_f_lo=args.p_f_min_psi,
213 p_f_hi=args.p_f_max_psi,
214 max_lox_psi=args.max_lox_pressure_psi,
215 max_fuel_psi=args.max_fuel_pressure_psi,
216 )
217
218 print("Cd assumption sensitivity — Layer 1")
219 print(
220 " Throat mm² [{:.0f}, {:.0f}] | P_O [{:.0f},{:.0f}] | P_F [{:.0f},{:.0f}] | "
221 "ΔP/Pc [0.15,0.40] both | R [0.93,1.07]".format(
222 throat_lo, throat_hi, args.p_o_min_psi, args.p_o_max_psi, args.p_f_min_psi, args.p_f_max_psi
223 )
224 )
225 print(f" max_iter={args.layer1_max_iterations} cma_restarts={args.layer1_cma_restarts}\n")
226
227 rows: List[Dict[str, Any]] = []
228
229 for case_id, cd_o_assume, cd_f_assume in CASES:
230 cfg = copy.deepcopy(cfg0)
231 cdi_o, cdm_o, cdi_f, cdm_f = _apply_cd_caps(cfg, cd_o_assume, cd_f_assume)
232 opt_cfg, results = run_layer1_optimization(
233 copy.deepcopy(cfg),
234 PintleEngineRunner(copy.deepcopy(cfg)),
235 copy.deepcopy(req),
236 target_burn_time=float(req.get("target_burn_time", 6.0)),
237 tolerances={"thrust": 0.10, "apogee": 0.15},
238 pressure_config=copy.deepcopy(pcfg),
239 layer1_smoke=False,
240 layer1_max_iterations=int(args.layer1_max_iterations),
241 layer1_cma_restarts=int(args.layer1_cma_restarts),
242 )
243 perf = results.get("performance") or {}
244 diag = perf.get("diagnostics") if isinstance(perf.get("diagnostics"), dict) else {}
245
246 lt = getattr(opt_cfg.lox_tank, "initial_pressure_psi", None)
247 ft = getattr(opt_cfg.fuel_tank, "initial_pressure_psi", None)
248 poc = getattr(opt_cfg.chamber_geometry, "A_throat", None)
249 at_mm2 = float(poc) * 1e6 if poc is not None and np.isfinite(float(poc)) else float("nan")
250
251 ig = opt_cfg.injector.geometry
252 cd_o_act = perf.get("Cd_O", diag.get("Cd_O"))
253 cd_f_act = perf.get("Cd_F", diag.get("Cd_F"))
254
255 geo = (
256 f"LOX n={ig.oxidizer.n_elements} d={ig.oxidizer.d_jet*1000:.3f}mm "
257 f"sp={ig.oxidizer.spacing*1000:.3f}mm θ={ig.oxidizer.impingement_angle:.1f}° | "
258 f"F n={ig.fuel.n_elements} d={ig.fuel.d_jet*1000:.3f}mm "
259 f"sp={ig.fuel.spacing*1000:.3f}mm θ={ig.fuel.impingement_angle:.1f}°"
260 )
261
262 row = {
263 "case": case_id,
264 "Cd_O_assumed_inf": cdi_o,
265 "Cd_O_assumed_min": cdm_o,
266 "Cd_F_assumed_inf": cdi_f,
267 "Cd_F_assumed_min": cdm_f,
268 "Cd_O_actual": cd_o_act,
269 "Cd_F_actual": cd_f_act,
270 "F_N": perf.get("F"),
271 "MR": perf.get("MR"),
272 "R": perf.get("momentum_ratio_R"),
273 "dP_O_over_Pc": perf.get("injector_dp_ratio_O"),
274 "dP_F_over_Pc": perf.get("injector_dp_ratio_F"),
275 "P_O_tank_psi": float(lt) if lt is not None and np.isfinite(float(lt)) else None,
276 "P_F_tank_psi": float(ft) if ft is not None and np.isfinite(float(ft)) else None,
277 "A_throat_mm2": at_mm2,
278 "injector_geometry": geo,
279 "layer1_objective_approx": results.get("convergence_info", {}).get("final_change"),
280 }
281 rows.append(row)
282
283 print(f"=== Case {case_id} (Cd_inf O={cdi_o}, F={cdi_f}; Cd_min O={cdm_o:.4f}, F={cdm_f:.4f}) ===")
284 print(f" actual Cd_O={cd_o_act} Cd_F={cd_f_act}")
285 print(f" F={perf.get('F')} N MR={perf.get('MR')} R={perf.get('momentum_ratio_R')}")
286 print(f" ΔP_O/Pc={perf.get('injector_dp_ratio_O')} ΔP_F/Pc={perf.get('injector_dp_ratio_F')}")
287 print(f" tanks P_O={lt} psi P_F={ft} psi At={at_mm2:.2f} mm²")
288 print(f" {geo}\n")
289
290 outp = Path(args.output_csv)
291 outp.parent.mkdir(parents=True, exist_ok=True)
292 if rows:
293 fieldnames = list(rows[0].keys())
294 with open(outp, "w", newline="", encoding="utf-8") as f:
295 w = csv.DictWriter(f, fieldnames=fieldnames)
296 w.writeheader()
297 w.writerows(rows)
298 print(f"Wrote {outp}")
299
300
301if __name__ == "__main__":
302 main()
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
static emmc_status_t format(emmc_log_t *log)
Definition emmc_log.c:324
Dict[str, Any] _strip_none_fp(Dict[str, Any] fp)
Tuple[Any, Dict[str, Any], Dict[str, Any]] _build_req_and_cfg(Path base_cfg_path, *, float throat_lo_mm2, float throat_hi_mm2, float p_o_lo, float p_o_hi, float p_f_lo, float p_f_hi, float max_lox_psi, float max_fuel_psi)
Tuple[float, float, float, float] _apply_cd_caps(Any cfg, float cd_o_inf, float cd_f_inf)