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