STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
study_equal_tank_pressure_feed_allocation.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Equal LOX/fuel stagnation tanks + modeled feed ΔP allocation (three pressure levels).
3
4For each stagnation pressure **P_tank_O = P_tank_F = P₀** ∈ {700, 800, 900} psi:
5
6* Tanks are **frozen** in Layer‑1 (not optimized).
7* **Different injector inlets** arise from hydraulic feed losses: ``P_inj = P_tank − ΔP_feed``.
8* **Reject** evaluation rows when any of:
9
10 - ``P_inj ≤ Pc`` on either stream (no meaningful injector ΔP toward the chamber).
11 - **Negative feed** (diagnostic ``ΔP_feed < 0`` or ``P_inj > P_tank``).
12 - Chamber/nozzle reported **unchoked**: ``chamber_intrinsics["is_choked"] is False`` (see
13 ``calculate_chamber_intrinsics`` — uses ``P_back/Pc`` vs critical ratio).
14 - **Feed loss fraction** ``f_feed = ΔP_feed / (P_tank − Pc)`` exceeds ``--max-feed-fraction``
15 on **either** stream (default ``0.60`` captures “>&nbsp;50–60 % extreme” envelope).
16
17Hydraulic bookkeeping matches ``study_fixed_tanks_feed_allocation.py``.
18
19Does **not** change Cd / injector equations.
20
21Outputs one CSV aggregating every replay row with ``P_equal_psi`` and rejection flags.
22
23Example::
24
25 python scripts/study_equal_tank_pressure_feed_allocation.py \\
26 --output-csv output/study_equal_tank_pressure_feed_allocation.csv
27
28"""
29
30from __future__ import annotations
31
32import argparse
33import copy
34import csv
35import math
36import sys
37from pathlib import Path
38from typing import Any, Dict, List, Tuple
39
40import numpy as np
41
42ROOT = Path(__file__).resolve().parents[1]
43sys.path.insert(0, str(ROOT))
44
45from engine.core.runner import PintleEngineRunner # noqa: E402
46from engine.optimizer.injector_dp_penalty import injector_dp_ratios_from_eval_result # noqa: E402
47from engine.optimizer.layers.layer1_static_optimization import run_layer1_optimization # noqa: E402
48from engine.pipeline.io import load_config # noqa: E402
49
50PSI_TO_PA = 6894.76
51PA_TO_PSI = 1.0 / PSI_TO_PA
52F_TARGET = 8000.0
53MR_TARGET = 3.5
54PC_MARGIN_PA = 2.0 # numerical slack for P_inj > Pc check
55
56
57def _strip_none_fp(fp: Dict[str, Any]) -> Dict[str, Any]:
58 return {k: v for k, v in fp.items() if v is not None}
59
60
61def _apply_throat_mm2(cfg: Any, mm2: float) -> None:
62 cg = cfg.chamber_geometry
63 if cg is None:
64 raise ValueError("chamber_geometry required")
65 At = float(mm2) * 1.0e-6
66 eps = float(cg.expansion_ratio)
67 Ae = eps * At
68 cg.A_throat = At
69 cg.A_exit = Ae
70 cg.exit_diameter = 2.0 * math.sqrt(max(Ae, 1e-30) / math.pi)
71
72
73def _hist_signature(h: Dict[str, Any]) -> Tuple[Any, ...]:
74 return (
75 round(float(h["A_throat"]), 14),
76 int(h["n_elements_O"]),
77 int(h["n_elements_F"]),
78 round(float(h["d_jet_O"]), 8),
79 round(float(h["d_jet_F"]), 8),
80 round(float(h["spacing_O"]), 8),
81 round(float(h["spacing_F"]), 8),
82 round(float(h["impingement_angle_O"]), 4),
83 round(float(h["impingement_angle_F"]), 4),
84 )
85
86
87def _apply_hist_to_cfg(cfg: Any, h: Dict[str, Any]) -> None:
88 _apply_throat_mm2(cfg, float(h["A_throat"]) * 1e6)
89 ig = cfg.injector.geometry
90 ig.oxidizer.n_elements = int(h["n_elements_O"])
91 ig.oxidizer.d_jet = float(h["d_jet_O"])
92 ig.oxidizer.impingement_angle = float(h["impingement_angle_O"])
93 ig.oxidizer.spacing = float(h["spacing_O"])
94 ig.fuel.n_elements = int(h["n_elements_F"])
95 ig.fuel.d_jet = float(h["d_jet_F"])
96 ig.fuel.impingement_angle = float(h["impingement_angle_F"])
97 ig.fuel.spacing = float(h["spacing_F"])
98
99
100def _inj_diag(res: Dict[str, Any], key: str):
101 ip = res.get("injector_pressure") if isinstance(res.get("injector_pressure"), dict) else {}
102 di = res.get("diagnostics") if isinstance(res.get("diagnostics"), dict) else {}
103 v = ip.get(key)
104 if v is None or not np.isfinite(float(v)):
105 v = di.get(key)
106 if v is None or not np.isfinite(float(v)):
107 return None
108 return float(v)
109
110
112 cfg_template: Path,
113 h: Dict[str, Any],
114 *,
115 P_equal_psi: float,
116 dp_inj_pc_lo: float,
117 dp_inj_pc_hi: float,
118 max_feed_fraction: float,
119) -> Dict[str, Any]:
120 P_tank_psi = float(P_equal_psi)
121 p_to = P_tank_psi * PSI_TO_PA
122 p_tf = P_tank_psi * PSI_TO_PA
123 rej: List[str] = []
124 row: Dict[str, Any] = {
125 "P_equal_psi": P_tank_psi,
126 "P_tank_O_psi": P_tank_psi,
127 "P_tank_F_psi": P_tank_psi,
128 "source_iter": int(h.get("iteration", -2)),
129 "accepted_strict": False,
130 "reject_reason": "",
131 "is_choked": None,
132 "P_back_over_Pc": None,
133 "geom_line": "",
134 }
135 cfg = load_config(str(cfg_template))
136 _apply_hist_to_cfg(cfg, h)
137 try:
138 runner = PintleEngineRunner(cfg)
139 res = runner.evaluate(p_to, p_tf, silent=True)
140 except Exception as e:
141 row["reject_reason"] = f"eval_exception:{e}"
142 return row
143
144 pc_pa = float(res.get("Pc", float("nan")))
145 if not np.isfinite(pc_pa) or pc_pa <= 0:
146 row["reject_reason"] = "invalid_pc"
147 return row
148
149 ci = res.get("chamber_intrinsics")
150 if isinstance(ci, dict):
151 row["is_choked"] = ci.get("is_choked")
152 row["P_back_over_Pc"] = ci.get("actual_pressure_ratio")
153 if ci.get("is_choked") is False:
154 rej.append("nozzle_unchoked(P_back/Pc>P_crit)")
155 if ci.get("is_choked") is None:
156 rej.append("missing_is_choked_flag")
157 else:
158 rej.append("missing_chamber_intrinsics")
159 row["is_choked"] = None
160
161 P_inj_O = _inj_diag(res, "P_injector_O")
162 P_inj_F = _inj_diag(res, "P_injector_F")
163 d_inj_O = _inj_diag(res, "delta_p_injector_O")
164 d_inj_F = _inj_diag(res, "delta_p_injector_F")
165 d_fd_O = _inj_diag(res, "delta_p_feed_O")
166 d_fd_F = _inj_diag(res, "delta_p_feed_F")
167
168 if None in (P_inj_O, P_inj_F, d_inj_O, d_inj_F, d_fd_O, d_fd_F):
169 row["reject_reason"] = "missing_pressure_diagnostics"
170 return row
171
172 if P_inj_O <= pc_pa + PC_MARGIN_PA:
173 rej.append("P_inj_O_le_Pc")
174 if P_inj_F <= pc_pa + PC_MARGIN_PA:
175 rej.append("P_inj_F_le_Pc")
176
177 d_feed_geom_O = p_to - P_inj_O
178 d_feed_geom_F = p_tf - P_inj_F
179 if d_fd_O < -1e-3 or d_fd_F < -1e-3:
180 rej.append("diag_negative_delta_p_feed")
181 if d_feed_geom_O < -1e-3:
182 rej.append("negative_feed_allocation_O")
183 if d_feed_geom_F < -1e-3:
184 rej.append("negative_feed_allocation_F")
185
186 dp_tot_O = p_to - pc_pa
187 dp_tot_F = p_tf - pc_pa
188 f_feed_O = (d_fd_O / dp_tot_O) if dp_tot_O > 1000.0 else float("nan")
189 f_feed_F = (d_fd_F / dp_tot_F) if dp_tot_F > 1000.0 else float("nan")
190
191 if np.isfinite(f_feed_O) and f_feed_O > max_feed_fraction:
192 rej.append(f"feed_loss_fraction_O>{max_feed_fraction:.2f}")
193 if np.isfinite(f_feed_F) and f_feed_F > max_feed_fraction:
194 rej.append(f"feed_loss_fraction_F>{max_feed_fraction:.2f}")
195
196 budget_res_O = dp_tot_O - d_fd_O - d_inj_O
197 budget_res_F = dp_tot_F - d_fd_F - d_inj_F
198
199 ratio_o = (d_inj_O / pc_pa) if pc_pa > 0 else float("nan")
200 ratio_f = (d_inj_F / pc_pa) if pc_pa > 0 else float("nan")
201 ro, rf = injector_dp_ratios_from_eval_result(pc_pa, res)
202
203 diag = res.get("diagnostics") if isinstance(res.get("diagnostics"), dict) else {}
204 R = diag.get("momentum_ratio_R")
205 Rv = float(R) if R is not None and np.isfinite(R) else float("nan")
206 Fo = float(res.get("F", float("nan")))
207 MRo = float(res.get("MR", float("nan")))
208
209 ig = cfg.injector.geometry
210 geom = (
211 f"LOX n={ig.oxidizer.n_elements} d={ig.oxidizer.d_jet*1000:.4f}mm "
212 f"sp={ig.oxidizer.spacing*1000:.4f}mm θ={ig.oxidizer.impingement_angle:.2f}° | "
213 f"F n={ig.fuel.n_elements} d={ig.fuel.d_jet*1000:.4f}mm "
214 f"sp={ig.fuel.spacing*1000:.4f}mm θ={ig.fuel.impingement_angle:.2f}°"
215 )
216
217 tgt_ok_o = dp_inj_pc_lo <= ratio_o <= dp_inj_pc_hi if np.isfinite(ratio_o) else False
218 tgt_ok_f = dp_inj_pc_lo <= ratio_f <= dp_inj_pc_hi if np.isfinite(ratio_f) else False
219 mr_ok = abs(MRo / MR_TARGET - 1.0) <= 0.06 if np.isfinite(MRo) else False
220 r_ok = abs(Rv - 1.0) <= 0.10 if np.isfinite(Rv) else False
221
222 row.update(
223 {
224 "F_N": Fo,
225 "MR": MRo,
226 "R": Rv,
227 "Pc_psi": pc_pa * PA_TO_PSI,
228 "Pc_Pa": pc_pa,
229 "P_inj_O_psi": P_inj_O * PA_TO_PSI,
230 "P_inj_F_psi": P_inj_F * PA_TO_PSI,
231 "delta_p_inj_O_psi": d_inj_O * PA_TO_PSI,
232 "delta_p_inj_F_psi": d_inj_F * PA_TO_PSI,
233 "delta_p_feed_O_psi": d_fd_O * PA_TO_PSI,
234 "delta_p_feed_F_psi": d_fd_F * PA_TO_PSI,
235 "dP_inj_over_Pc_O": ratio_o,
236 "dP_inj_over_Pc_F": ratio_f,
237 "dP_inj_over_Pc_solver_O": float(ro) if ro is not None and np.isfinite(ro) else None,
238 "dP_inj_over_Pc_solver_F": float(rf) if rf is not None and np.isfinite(rf) else None,
239 "feed_loss_fraction_O": f_feed_O,
240 "feed_loss_fraction_F": f_feed_F,
241 "budget_residual_O_Pa": budget_res_O,
242 "budget_residual_F_Pa": budget_res_F,
243 "A_throat_mm2": float(h["A_throat"]) * 1e6,
244 "n_O": int(h["n_elements_O"]),
245 "n_F": int(h["n_elements_F"]),
246 "d_jet_O_mm": float(h["d_jet_O"]) * 1000,
247 "d_jet_F_mm": float(h["d_jet_F"]) * 1000,
248 "spacing_O_mm": float(h["spacing_O"]) * 1000,
249 "spacing_F_mm": float(h["spacing_F"]) * 1000,
250 "theta_O_deg": float(h["impingement_angle_O"]),
251 "theta_F_deg": float(h["impingement_angle_F"]),
252 "goal_dp_band_O": tgt_ok_o,
253 "goal_dp_band_F": tgt_ok_f,
254 "goal_mr_3p5_tol6pct": mr_ok,
255 "goal_R_unity_tol10pct": r_ok,
256 "goal_thrust_8k_tol10pct": abs(Fo / F_TARGET - 1.0) <= 0.10 if np.isfinite(Fo) else False,
257 "geom_line": geom,
258 }
259 )
260
261 row["accepted_strict"] = len(rej) == 0
262 row["reject_reason"] = ";".join(rej) if rej else "ok"
263
264 mr_e = abs(MRo / MR_TARGET - 1.0) if np.isfinite(MRo) else 1e9
265 r_e = abs(Rv - 1.0) if np.isfinite(Rv) else 1e9
266 f_err = (
267 max(0.0, dp_inj_pc_lo - ratio_o, ratio_o - dp_inj_pc_hi) ** 2
268 + max(0.0, dp_inj_pc_lo - ratio_f, ratio_f - dp_inj_pc_hi) ** 2
269 if (np.isfinite(ratio_o) and np.isfinite(ratio_f))
270 else 1e9
271 )
272 thrust_e = abs(Fo / F_TARGET - 1.0) if np.isfinite(Fo) else 1e9
273 feed_pen = max(
274 0.0,
275 float(f_feed_O) - max_feed_fraction if np.isfinite(f_feed_O) else 0.0,
276 float(f_feed_F) - max_feed_fraction if np.isfinite(f_feed_F) else 0.0,
277 )
278 row["_sort_key"] = (thrust_e, mr_e, r_e, f_err, feed_pen)
279
280 return row
281
282
284 *,
285 cfg_path: Path,
286 P_equal_psi: float,
287 throat_lo_mm2: float,
288 throat_hi_mm2: float,
289 layer1_max_iterations: int,
290 layer1_cma_restarts: int,
291 dp_lo: float,
292 dp_hi: float,
293 max_pressure_cap_margin_psi: float,
294 max_feed_fraction: float,
295 history_pick: int,
296) -> List[Dict[str, Any]]:
297 pe = float(P_equal_psi)
298 max_lox_cap = pe + float(max_pressure_cap_margin_psi)
299 max_fuel_cap = pe + float(max_pressure_cap_margin_psi)
300
301 cfg = load_config(str(cfg_path))
302 mid_mm2 = 0.5 * (float(throat_lo_mm2) + float(throat_hi_mm2))
303 _apply_throat_mm2(cfg, mid_mm2)
304 if cfg.chamber_geometry is not None:
305 cfg.chamber_geometry.design_MR = float(cfg.design_requirements.optimal_of_ratio)
306
307 ig0 = cfg.injector.geometry
308 ig0.oxidizer.n_elements = 68
309 ig0.oxidizer.d_jet = 1.424e-3
310 ig0.oxidizer.spacing = 5.901e-3
311 ig0.oxidizer.impingement_angle = 63.15
312 ig0.fuel.n_elements = 32
313 ig0.fuel.d_jet = 1.392e-3
314 ig0.fuel.spacing = 4.118e-3
315 ig0.fuel.impingement_angle = 75.05
316
317 req = cfg.design_requirements.model_dump()
318 fp = _strip_none_fp(dict(req.get("frozen_parameters") or {}))
319 fp.pop("A_throat_mm2", None)
320 fp.pop("P_O_start_psi", None)
321 fp.pop("P_F_start_psi", None)
322 fp["P_O_start_psi"] = pe
323 fp["P_F_start_psi"] = pe
324 if cfg.chamber_geometry is not None:
325 fp["expansion_ratio"] = float(cfg.chamber_geometry.expansion_ratio)
326 fp["Lstar_mm"] = float(cfg.chamber_geometry.Lstar) * 1000.0
327 req["frozen_parameters"] = fp
328
329 req["layer1_A_throat_mm2_min"] = float(throat_lo_mm2)
330 req["layer1_A_throat_mm2_max"] = float(throat_hi_mm2)
331 req["max_lox_tank_pressure_psi"] = max(float(req.get("max_lox_tank_pressure_psi", 0.0)), max_lox_cap)
332 req["max_fuel_tank_pressure_psi"] = max(float(req.get("max_fuel_tank_pressure_psi", 0.0)), max_fuel_cap)
333
334 req["injector_dp_ratio_O_min"] = dp_lo
335 req["injector_dp_ratio_O_max"] = dp_hi
336 req["injector_dp_ratio_F_min"] = dp_lo
337 req["injector_dp_ratio_F_max"] = dp_hi
338
339 req["impinging_momentum_R_min"] = 0.93
340 req["impinging_momentum_R_max"] = 1.07
341
342 req["W_MOM"] = 1600.0
343 req["W_geom_ao_af_momentum"] = 6500.0
344 req["W_DP"] = 400.0
345 req["W_DP_O"] = 900.0
346 req["W_DP_F"] = 900.0
347
348 req["target_thrust"] = F_TARGET
349 req["optimal_of_ratio"] = MR_TARGET
350
351 req["require_stable_state"] = False
352 req["min_stability_score"] = min(float(req.get("min_stability_score", 0.75)), 0.58)
353 req["min_stability_margin"] = min(float(req.get("min_stability_margin", 1.2)), 1.05)
354 req["layer1_infeasibility_gate_eps"] = 2.0e-3
355
356 pcfg = {
357 "mode": "optimizer_controlled",
358 "max_lox_pressure_psi": float(req["max_lox_tank_pressure_psi"]),
359 "max_fuel_pressure_psi": float(req["max_fuel_tank_pressure_psi"]),
360 }
361
362 print(f"\n--- P_equal = {pe:.1f} psi (Layer‑1 geometry+throat) ---")
363 _opt_cfg, results = run_layer1_optimization(
364 copy.deepcopy(cfg),
365 PintleEngineRunner(copy.deepcopy(cfg)),
366 req,
367 target_burn_time=float(req.get("target_burn_time", 6.0)),
368 tolerances={"thrust": 0.10, "apogee": 0.15},
369 pressure_config=pcfg,
370 layer1_smoke=False,
371 layer1_max_iterations=int(layer1_max_iterations),
372 layer1_cma_restarts=int(layer1_cma_restarts),
373 )
374
375 hist = list(results.get("iteration_history") or [])
376 oc = _opt_cfg.chamber_geometry
377 og = _opt_cfg.injector.geometry
378 assert oc is not None and og is not None
379 hist.append(
380 {
381 "A_throat": float(oc.A_throat),
382 "n_elements_O": int(og.oxidizer.n_elements),
383 "d_jet_O": float(og.oxidizer.d_jet),
384 "impingement_angle_O": float(og.oxidizer.impingement_angle),
385 "spacing_O": float(og.oxidizer.spacing),
386 "n_elements_F": int(og.fuel.n_elements),
387 "d_jet_F": float(og.fuel.d_jet),
388 "impingement_angle_F": float(og.fuel.impingement_angle),
389 "spacing_F": float(og.fuel.spacing),
390 "iteration": -1,
391 "eval_success": True,
392 "objective": float(results.get("convergence_info", {}).get("final_change", float("nan"))),
393 "thrust": (results.get("performance") or {}).get("F"),
394 }
395 )
396
397 sorted_hist = sorted(hist, key=lambda z: float(z.get("objective", 1e30)))
398 picked: List[Dict[str, Any]] = []
399 seen_sig = set()
400 for h in sorted_hist:
401 if not h.get("eval_success"):
402 continue
403 if h.get("thrust") is not None:
404 thrust_v = float(h["thrust"])
405 if not np.isfinite(thrust_v):
406 continue
407 sig = _hist_signature(h)
408 if sig in seen_sig:
409 continue
410 seen_sig.add(sig)
411 picked.append(h)
412 if len(picked) >= int(history_pick):
413 break
414
415 rows_out: List[Dict[str, Any]] = []
416 for h in picked:
418 cfg_path,
419 h,
420 P_equal_psi=pe,
421 dp_inj_pc_lo=dp_lo,
422 dp_inj_pc_hi=dp_hi,
423 max_feed_fraction=max_feed_fraction,
424 )
425 rows_out.append(r)
426
427 accepted = [r for r in rows_out if r["accepted_strict"]]
428 good = sorted([r for r in accepted if isinstance(r.get("_sort_key"), tuple)], key=lambda z: z["_sort_key"])
429 print(f" Replay: {len(rows_out)} geometries | strictly accepted: {len(accepted)}")
430 if good:
431 b = good[0]
432 print(
433 f" Best strictly-accepted sort: F={b['F_N']:.1f} N MR={b['MR']:.3f} R={b['R']:.3f} "
434 f"Pc={b['Pc_psi']:.1f} psi dP/Pc O={b['dP_inj_over_Pc_O']:.3f} F={b['dP_inj_over_Pc_F']:.3f} "
435 f"f_feed O={b['feed_loss_fraction_O']:.3f} F={b['feed_loss_fraction_F']:.3f} "
436 f"choked={b.get('is_choked')}"
437 )
438 elif rows_out:
439 # show mildest rejection
440 nf = lambda r: (0 if r["accepted_strict"] else len(r.get("reject_reason", "").split(";")))
441 probe = sorted(rows_out, key=nf)[0]
442 print(f" No strict accepts; example row: rejected={probe.get('reject_reason')}")
443
444 return rows_out
445
446
447def main() -> None:
448 ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
449 ap.add_argument("--config", type=str, default=str(ROOT / "configs/impinging_lox_ch4_8000N.yaml"))
450 ap.add_argument(
451 "--pressure-psi",
452 type=float,
453 nargs="+",
454 default=[700.0, 800.0, 900.0],
455 help="Equal tank stagnation pressures [psi] to test (same for LOX and fuel)",
456 )
457 ap.add_argument("--throat-min-mm2", type=float, default=2520.0)
458 ap.add_argument("--throat-max-mm2", type=float, default=2600.0)
459 ap.add_argument("--layer1-max-iterations", type=int, default=280)
460 ap.add_argument("--layer1-cma-restarts", type=int, default=2)
461 ap.add_argument("--dp-inj-over-pc-min", type=float, default=0.15)
462 ap.add_argument("--dp-inj-over-pc-max", type=float, default=0.40)
463 ap.add_argument("--max-feed-fraction", type=float, default=0.60, help="Reject if f_feed>O or F exceeds this")
464 ap.add_argument("--history-pick", type=int, default=48)
465 ap.add_argument("--max-pressure-cap-margin-psi", type=float, default=100.0)
466 ap.add_argument("--output-csv", type=str, default=str(ROOT / "output/study_equal_tank_pressure_feed_allocation.csv"))
467 args = ap.parse_args()
468
469 cfg_path = Path(args.config)
470 if not cfg_path.is_absolute():
471 cfg_path = ROOT / cfg_path
472
473 dp_lo = float(args.dp_inj_over_pc_min)
474 dp_hi = float(args.dp_inj_over_pc_max)
475 all_rows: List[Dict[str, Any]] = []
476
477 print("Equal tank pressure feed-allocation study")
478 print(f" Levels [psi]: {args.pressure_psi}")
479 print(f" Throat [{args.throat_min_mm2:.0f}, {args.throat_max_mm2:.0f}] mm²")
480 print(f" ΔP_inj/Pc goal band [{dp_lo:.2f}, {dp_hi:.2f}] | max feed frac {args.max_feed_fraction:.2f}")
481 print(" Strict reject: P_inj≤Pc | negative ΔP_feed | unchoked | f_feed too high")
482
483 for pe in args.pressure_psi:
485 cfg_path=cfg_path,
486 P_equal_psi=float(pe),
487 throat_lo_mm2=float(args.throat_min_mm2),
488 throat_hi_mm2=float(args.throat_max_mm2),
489 layer1_max_iterations=int(args.layer1_max_iterations),
490 layer1_cma_restarts=int(args.layer1_cma_restarts),
491 dp_lo=dp_lo,
492 dp_hi=dp_hi,
493 max_pressure_cap_margin_psi=float(args.max_pressure_cap_margin_psi),
494 max_feed_fraction=float(args.max_feed_fraction),
495 history_pick=int(args.history_pick),
496 )
497 all_rows.extend(rows)
498
499 out_path = Path(args.output_csv)
500 out_path.parent.mkdir(parents=True, exist_ok=True)
501 preferred = [
502 "P_equal_psi",
503 "accepted_strict",
504 "reject_reason",
505 "source_iter",
506 "is_choked",
507 "P_back_over_Pc",
508 "F_N",
509 "MR",
510 "R",
511 "Pc_psi",
512 "delta_p_inj_O_psi",
513 "delta_p_inj_F_psi",
514 "delta_p_feed_O_psi",
515 "delta_p_feed_F_psi",
516 "dP_inj_over_Pc_O",
517 "dP_inj_over_Pc_F",
518 "feed_loss_fraction_O",
519 "feed_loss_fraction_F",
520 "goal_dp_band_O",
521 "goal_dp_band_F",
522 "goal_mr_3p5_tol6pct",
523 "goal_R_unity_tol10pct",
524 "goal_thrust_8k_tol10pct",
525 "n_O",
526 "n_F",
527 "d_jet_O_mm",
528 "d_jet_F_mm",
529 "spacing_O_mm",
530 "spacing_F_mm",
531 "theta_O_deg",
532 "theta_F_deg",
533 "A_throat_mm2",
534 "geom_line",
535 "budget_residual_O_Pa",
536 "budget_residual_F_Pa",
537 ]
538 if all_rows:
539 extras = sorted(k for k in all_rows[0].keys() if k not in preferred and not str(k).startswith("_"))
540 fieldnames = [k for k in preferred if k in all_rows[0]] + extras
541 with open(out_path, "w", newline="", encoding="utf-8") as f:
542 w = csv.DictWriter(f, fieldnames=fieldnames, extrasaction="ignore")
543 w.writeheader()
544 for r in all_rows:
545 w.writerow({k: r.get(k) for k in fieldnames})
546
547 na = sum(1 for r in all_rows if r.get("accepted_strict"))
548 print(f"\nWrote {out_path} (total rows={len(all_rows)}, strictly accepted={na})")
549
550
551if __name__ == "__main__":
552 main()
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
static std::vector< string_t > split(const string_t &reference_string)
split the string input to reference tokens
Definition json.hpp:14471
Dict[str, Any] _evaluate_row_equal_tanks(Path cfg_template, Dict[str, Any] h, *, float P_equal_psi, float dp_inj_pc_lo, float dp_inj_pc_hi, float max_feed_fraction)
List[Dict[str, Any]] _run_one_pressure_level(*, Path cfg_path, float P_equal_psi, float throat_lo_mm2, float throat_hi_mm2, int layer1_max_iterations, int layer1_cma_restarts, float dp_lo, float dp_hi, float max_pressure_cap_margin_psi, float max_feed_fraction, int history_pick)