STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
sweep_layer1_of_fuel_pressure.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Sweep Layer-1 over target O/F and fuel tank pressure cap.
3
4Each run: free throat ±12.5%, R ∈ [0.85, 1.15], ΔP_inj/Pc ∈ [0.15, 0.40] both streams,
5target thrust 8000 N via ``design_requirements``, ``optimal_of_ratio`` sets MR target.
6
7Writes CSV under ``output/layer1_sweep_of_fuel_pressure.csv`` and prints a summary table.
8"""
9
10from __future__ import annotations
11
12import argparse
13import copy
14import csv
15import math
16import sys
17from pathlib import Path
18from typing import Any, Dict, List, Tuple
19
20ROOT = Path(__file__).resolve().parents[1]
21sys.path.insert(0, str(ROOT))
22
23from engine.core.runner import PintleEngineRunner # noqa: E402
24from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization # noqa: E402
25from engine.pipeline.io import load_config # noqa: E402
26
27
28def _clean_frozen(req: Dict[str, Any]) -> None:
29 fp = req.get("frozen_parameters")
30 if not isinstance(fp, dict):
31 fp = {}
32 else:
33 fp = {k: v for k, v in fp.items() if v is not None}
34 fp.pop("A_throat_mm2", None)
35 req["frozen_parameters"] = fp
36
37
38def _sf(x: Any) -> float:
39 try:
40 if x is None:
41 return float("nan")
42 v = float(x)
43 return v if math.isfinite(v) else float("nan")
44 except (TypeError, ValueError):
45 return float("nan")
46
47
48def _dp_ratios(perf: Dict[str, Any]) -> Tuple[float, float]:
49 ro = perf.get("injector_dp_ratio_O")
50 rf = perf.get("injector_dp_ratio_F")
51 if ro is not None and rf is not None:
52 return float(ro), float(rf)
53 diag = perf.get("diagnostics") or {}
54 pc = float(perf.get("Pc", float("nan")))
55 dpo = diag.get("delta_p_injector_O")
56 dpf = diag.get("delta_p_injector_F")
57 ro_v = float(dpo) / pc if dpo is not None and pc > 0 else float("nan")
58 rf_v = float(dpf) / pc if dpf is not None and pc > 0 else float("nan")
59 return ro_v, rf_v
60
61
62def _cds(perf: Dict[str, Any]) -> Tuple[float, float]:
63 diag = perf.get("diagnostics") or {}
64 co = perf.get("Cd_O", diag.get("Cd_O"))
65 cf = perf.get("Cd_F", diag.get("Cd_F"))
66 return _sf(co), _sf(cf)
67
68
69def _geom_row(opt_cfg: Any) -> str:
70 ig = opt_cfg.injector.geometry
71 return (
72 f"LOX n={int(ig.oxidizer.n_elements)} d={ig.oxidizer.d_jet*1000:.3f}mm "
73 f"sp={ig.oxidizer.spacing*1000:.3f}mm θ={ig.oxidizer.impingement_angle:.1f}° | "
74 f"F n={int(ig.fuel.n_elements)} d={ig.fuel.d_jet*1000:.3f}mm "
75 f"sp={ig.fuel.spacing*1000:.3f}mm θ={ig.fuel.impingement_angle:.1f}°"
76 )
77
78
79def main() -> None:
80 ap = argparse.ArgumentParser(description=__doc__)
81 ap.add_argument("--config", type=str, default=str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
82 ap.add_argument("--a-throat-center-mm2", type=float, default=2443.263433372886)
83 ap.add_argument("--throat-span-fraction", type=float, default=0.125)
84 ap.add_argument("--layer1-max-iterations", type=int, default=150)
85 ap.add_argument("--layer1-cma-restarts", type=int, default=1)
86 ap.add_argument("--output-csv", type=str, default=str(ROOT / "output/layer1_sweep_of_fuel_pressure.csv"))
87 ap.add_argument(
88 "--of-targets",
89 type=str,
90 default="3.5,3.7,4.0",
91 help="Comma-separated optimal_of_ratio targets (default: 3.5,3.7,4.0)",
92 )
93 ap.add_argument(
94 "--fuel-caps",
95 type=str,
96 default="900,800,700",
97 help="Comma-separated max fuel tank pressure caps [psi] (default: 900,800,700)",
98 )
99 args = ap.parse_args()
100
101 cfg_path = Path(args.config)
102 center = float(args.a_throat_center_mm2)
103 frac = float(args.throat_span_fraction)
104 dp_band = (0.15, 0.40)
105 r_lo, r_hi = 0.85, 1.15
106
107 of_targets = [float(x.strip()) for x in args.of_targets.split(",") if x.strip()]
108 fuel_caps_psi = [float(x.strip()) for x in args.fuel_caps.split(",") if x.strip()]
109
110 rows: List[Dict[str, Any]] = []
111
112 out_path = Path(args.output_csv)
113 out_path.parent.mkdir(parents=True, exist_ok=True)
114
115 for of_tgt in of_targets:
116 for pf_cap in fuel_caps_psi:
117 cfg = load_config(str(cfg_path))
118 if cfg.chamber_geometry is not None:
119 cfg.chamber_geometry.design_MR = float(of_tgt)
120
121 req = cfg.design_requirements.model_dump()
122 _clean_frozen(req)
123
124 req["target_thrust"] = 8000.0
125 req["optimal_of_ratio"] = float(of_tgt)
126 req["layer1_A_throat_mm2_min"] = center * (1.0 - frac)
127 req["layer1_A_throat_mm2_max"] = center * (1.0 + frac)
128 req["injector_dp_ratio_O_min"] = dp_band[0]
129 req["injector_dp_ratio_O_max"] = dp_band[1]
130 req["injector_dp_ratio_F_min"] = dp_band[0]
131 req["injector_dp_ratio_F_max"] = dp_band[1]
132 req["W_MOM"] = 300.0
133 req["W_geom_ao_af_momentum"] = 2800.0
134 req["impinging_momentum_R_min"] = float(r_lo)
135 req["impinging_momentum_R_max"] = float(r_hi)
136
137 req["max_fuel_tank_pressure_psi"] = float(pf_cap)
138
139 pcfg = {
140 "mode": "optimizer_controlled",
141 "max_lox_pressure_psi": float(req["max_lox_tank_pressure_psi"]),
142 "max_fuel_pressure_psi": float(pf_cap),
143 }
144
145 print(f"\n>>> OF_target={of_tgt} max_fuel_psi={pf_cap:.0f}", flush=True)
146
147 opt_cfg, results = run_layer1_optimization(
148 copy.deepcopy(cfg),
149 PintleEngineRunner(copy.deepcopy(cfg)),
150 req,
151 target_burn_time=float(req.get("target_burn_time", 6.0)),
152 tolerances={"thrust": 0.10, "apogee": 0.15},
153 pressure_config=pcfg,
154 layer1_smoke=False,
155 layer1_max_iterations=int(args.layer1_max_iterations),
156 layer1_cma_restarts=int(args.layer1_cma_restarts),
157 )
158
159 perf = results.get("performance") or {}
160 ro, rf = _dp_ratios(perf)
161 cdo, cdf = _cds(perf)
162 R = _sf(perf.get("momentum_ratio_R"))
163
164 ci = results.get("convergence_info") or {}
165 ls = results.get("layer_status") or {}
166 diag = perf.get("diagnostics") if isinstance(perf.get("diagnostics"), dict) else {}
167 finite_core = (
168 math.isfinite(_sf(perf.get("F")))
169 and math.isfinite(_sf(perf.get("MR")))
170 and math.isfinite(_sf(perf.get("Pc")))
171 )
172 inj_succ = diag.get("constraints_satisfied")
173 mdot_ratio = diag.get("mdot_ratio")
174
175 row = {
176 "OF_target": of_tgt,
177 "max_fuel_psi_cap": pf_cap,
178 "F_N": round(_sf(perf.get("F")), 2),
179 "MR_actual": round(_sf(perf.get("MR")), 5),
180 "R": round(R, 5),
181 "dP_O_over_Pc": round(ro, 5),
182 "dP_F_over_Pc": round(rf, 5),
183 "P_O_psi": round(_sf(perf.get("P_O_start_psi")), 4),
184 "P_F_psi": round(_sf(perf.get("P_F_start_psi")), 4),
185 "Cd_O": round(cdo, 5),
186 "Cd_F": round(cdf, 5),
187 "injector_geometry": _geom_row(opt_cfg),
188 "cma_converged": bool(ci.get("converged")),
189 "pressure_candidate_valid": bool(ls.get("layer_1_pressure_candidate")),
190 "finite_F_MR_Pc": bool(finite_core),
191 "injector_constraints_ok": inj_succ if isinstance(inj_succ, bool) else "",
192 "mdot_ratio_diag": mdot_ratio if mdot_ratio is not None else "",
193 "thrust_ok_5pct": abs(_sf(perf.get("F")) - 8000.0) / 8000.0 <= 0.05,
194 "dP_F_ok": dp_band[0] <= rf <= dp_band[1] if math.isfinite(rf) else False,
195 "R_ok": r_lo <= R <= r_hi if math.isfinite(R) else False,
196 }
197 rows.append(row)
198
199 fieldnames = list(rows[0].keys()) if rows else []
200 if fieldnames:
201 with open(out_path, "w", newline="", encoding="utf-8") as f:
202 w = csv.DictWriter(f, fieldnames=fieldnames)
203 w.writeheader()
204 w.writerows(rows)
205
206 print("\n" + "=" * 100)
207 print("SUMMARY (target thrust 8000 N, R∈[0.85,1.15], ΔP/Pc∈[0.15,0.40])")
208 print("=" * 100)
209 hdr = (
210 f"{'OF':>5} {'P_F_cap':>8} {'F[N]':>8} {'MR':>8} {'R':>8} "
211 f"{'dP_O':>8} {'dP_F':>8} {'P_O':>8} {'P_F':>8} {'Cd_O':>7} {'Cd_F':>7} "
212 f"{'cma':>4} {'pCV':>4}"
213 )
214 print(hdr)
215 print("-" * len(hdr))
216 for r in rows:
217 print(
218 f"{r['OF_target']:5.1f} {r['max_fuel_psi_cap']:8.0f} {r['F_N']:8.1f} "
219 f"{r['MR_actual']:8.4f} {r['R']:8.4f} {r['dP_O_over_Pc']:8.4f} {r['dP_F_over_Pc']:8.4f} "
220 f"{r['P_O_psi']:8.1f} {r['P_F_psi']:8.1f} {r['Cd_O']:7.4f} {r['Cd_F']:7.4f} "
221 f"{int(r['cma_converged']):4d} {int(r['pressure_candidate_valid']):4d}"
222 )
223
224 hits = [
225 r
226 for r in rows
227 if r["thrust_ok_5pct"] and r["dP_F_ok"] and r["R_ok"] and dp_band[0] <= r["dP_O_over_Pc"] <= dp_band[1]
228 ]
229 print("\nJoint feasibility (|F−8000|/8000≤5%, both ΔP/Pc in band, R in band): ", end="")
230 if hits:
231 print(f"YES — {len(hits)} row(s)")
232 for h in hits:
233 print(f" OF={h['OF_target']} P_F_cap={h['max_fuel_psi_cap']:.0f} → dP_F={h['dP_F_over_Pc']:.4f}")
234 else:
235 print("NO")
236
237 if fieldnames:
238 print(f"\nCSV written: {out_path}")
239
240
241if __name__ == "__main__":
242 main()
Tuple[float, float] _cds(Dict[str, Any] perf)
Tuple[float, float] _dp_ratios(Dict[str, Any] perf)