STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
forward_report.py
Go to the documentation of this file.
1"""Forward mode's result, one number per quantity.
2
3``forward_report(cfg, result)`` turns a solve (``PintleEngineRunner.evaluate``) into what forward mode
4shows: a headline, verdicts, and sections of quantities. Every quantity is read from the solve
5ONCE, here, with its unit, what it is (``basis``) and which unmeasured inputs it rests on
6(``assumed``). The frontend draws these and computes nothing: two cards cannot disagree about a
7number neither of them computes.
8
9A verdict carries its own status and threshold, decided here, so no card grades anything.
10
11Quantity: {key, label, value, unit, digits, basis, assumed: [calibration keys], status?, band?, detail?}
12Verdict: {key, label, value, unit, digits, status: ok|warn|bad|unknown, threshold, basis, assumed}
13Section: {key, title, summary: [quantity keys], quantities: [Quantity]}
14"""
15from __future__ import annotations
16
17import math
18from typing import Any, Dict, List, Mapping, Optional, Sequence
19
20PSI = 6894.757
21G0 = 9.80665
22
23
24# ---- the inputs nobody has measured -------------------------------------------------------------
25
26#: Every assumed input a displayed number can rest on: what it is, where the config holds it and
27#: what the literature allows. A quantity lists the keys it depends on; the frontend marks it.
28#: ``measurements.<key>`` in the config (not yet in the schema) will replace one when it is measured.
29CALIBRATION: Dict[str, Dict[str, str]] = {
30 "cd": {"label": "Orifice Cd", "where": "discharge.<side>",
31 "basis": "Lichtarowicz (1965) Cd(Re, L/d) for a sharp inlet; not flow-tested"},
32 "em": {"label": "Mixing factor E_m", "where": "combustion.efficiency.rupe_Em_opt",
33 "basis": "Rupe, JPL TR 32-1546: 0.70–0.85 for 1-on-1 unlike doublets"},
34 "smd": {"label": "Spray D32", "where": "spray.smd",
35 "basis": "NACA TN 4222 carried to the propellants; three published transfers disagree"},
36 "mix_lag": {"label": "Chug mixing lag", "where": "stability.mixing_lag_fraction",
37 "basis": "Leonardi 2017 double time lag; fraction unmeasured, gate takes the worst"},
38 "n_chi": {"label": "Combustion response n, χ", "where": "stability",
39 "basis": "Crocco n–τ constants; no measurement for this propellant pair"},
40 "ac_damping": {"label": "Acoustic damping", "where": "stability.damping_*_frac",
41 "basis": "injector-face and two-phase damping as fixed fractions of πf"},
42 "nozzle": {"label": "Nozzle efficiency ζ_n", "where": "chamber_geometry.nozzle_efficiency",
43 "basis": "divergence and boundary-layer loss taken as one factor"},
44 "manifold": {"label": "Manifold losses", "where": "injector.plate.channel_entry_K / _pressure_regain",
45 "basis": "Idelchik entry K and Acrivos regain; not flow-tested"},
46 "wall_heat": {"label": "Wall heat transfer", "where": "ablative_cooling",
47 "basis": "Bartz-type convection and gas radiation; liner not tested"},
48}
49
50# What the headline numbers rest on: flow through Cd, c* through E_m and SMD, thrust through zeta_n.
51_FLOW = ("cd", "manifold")
52_CSTAR = ("em", "smd")
53_THRUST = _FLOW + _CSTAR + ("nozzle",)
54
55
56def _f(v: Any) -> Optional[float]:
57 try:
58 x = float(v)
59 except (TypeError, ValueError):
60 return None
61 return x if math.isfinite(x) else None
62
63
64def Q(key: str, label: str, value: Any, unit: str = "", *, digits: int = 3, basis: str = "",
65 assumed: Sequence[str] = (), status: Optional[str] = None, band: Optional[str] = None,
66 detail: bool = False) -> Dict[str, Any]:
67 v = value if isinstance(value, str) else _f(value)
68 out = {"key": key, "label": label, "value": v, "unit": unit, "digits": digits, "basis": basis,
69 "assumed": list(assumed)}
70 if status:
71 out["status"] = status
72 if band:
73 out["band"] = band
74 if detail:
75 out["detail"] = True
76 return out
77
78
79def V(key: str, label: str, value: Any, unit: str, status: str, threshold: str, basis: str,
80 assumed: Sequence[str] = (), digits: int = 2) -> Dict[str, Any]:
81 v = value if isinstance(value, str) else _f(value)
82 return {"key": key, "label": label, "value": v, "unit": unit, "digits": digits,
83 "status": status, "threshold": threshold, "basis": basis, "assumed": list(assumed)}
84
85
86#: How each CEA table axis reads in a sentence, and the factor to its display unit.
87_CEA_AXES = {"Pc": ("Pc", 1.0 / PSI, "psia", 0), "MR": ("O/F", 1.0, "", 2), "eps": ("ε", 1.0, "", 2)}
88
89
90def cea_off_table(d: Mapping[str, Any], at: Optional[Mapping[str, Any]] = None) -> Optional[str]:
91 """None when the solve read its thermochemistry inside the CEA table; else a sentence.
92
93 ``d`` is a solve's diagnostics (chamber_solver: ``cea_clamped``, ``cea_table_range``); ``at``
94 the solved values by axis ({"Pc": Pa, "MR": .., "eps": ..}) when known. The cache clamps an
95 off-table input to the edge without a word, so a point past it is solved on the edge's c*, Tc
96 and γ: on the LE4 (3) stand at 578/300 psia the solve lands at O/F 3.52 on a 1.0–2.5 table.
97 """
98 axes = [a for a in (d.get("cea_clamped") or ()) if a in _CEA_AXES]
99 if not axes:
100 return None
101 rng = d.get("cea_table_range") or {}
102 parts = []
103 for a in axes:
104 name, k, unit, dg = _CEA_AXES[a]
105 u = f" {unit}" if unit else ""
106 lo_hi = rng.get(a)
107 val = _f((at or {}).get(a))
108 span = f"{lo_hi[0] * k:.{dg}f}–{lo_hi[1] * k:.{dg}f}{u}" if lo_hi else "the table"
109 parts.append(f"{name} {val * k:.{dg}f}{u} outside {span}" if val is not None else f"{name} outside {span}")
110 return "; ".join(parts) + ": thermochemistry clamped to the table edge"
111
112
113def _grade(value: Optional[float], ok: float, warn: Optional[float] = None, *, higher_is_better: bool = True) -> str:
114 """ok at/past ``ok``; warn between ``warn`` and ``ok``; bad beyond. unknown without a value."""
115 if value is None:
116 return "unknown"
117 if higher_is_better:
118 if value >= ok:
119 return "ok"
120 return "warn" if (warn is not None and value >= warn) else "bad"
121 if value <= ok:
122 return "ok"
123 return "warn" if (warn is not None and value <= warn) else "bad"
124
125
126# ---- sections -------------------------------------------------------------------------------------
127
128def _headline(r: Mapping[str, Any]) -> List[Dict[str, Any]]:
129 return [
130 Q("F", "Thrust", _f(r.get("F")) and r["F"] / 1000.0, "kN", digits=2, basis="F = Cf · Pc_ns · At (Pc_ns = Pc / κ, in Chamber)",
131 assumed=_THRUST),
132 Q("Isp", "Isp", r.get("Isp"), "s", digits=1, basis="F / (ṁ g0)", assumed=_THRUST),
133 Q("Pc", "Chamber pressure", _f(r.get("Pc")) and r["Pc"] / PSI, "psia", digits=1,
134 basis="the chamber solve: feed + injector flow = nozzle flow", assumed=_FLOW + _CSTAR),
135 Q("OF", "O/F", r.get("MR"), "", digits=3, basis="ṁ_O / ṁ_F", assumed=_FLOW),
136 Q("mdot", "Mass flow", r.get("mdot_total"), "kg/s", digits=3, basis="ṁ_O + ṁ_F", assumed=_FLOW),
137 Q("eta_cstar", "η c*", r.get("eta_cstar"), "", digits=3, basis="η_vap · η_mix · η_heat-loss",
138 assumed=_CSTAR),
139 ]
140
141
142def _verdicts(cfg: Any, r: Mapping[str, Any], sr: Mapping[str, Any]) -> List[Dict[str, Any]]:
143 d = r.get("diagnostics") or {}
144 out: List[Dict[str, Any]] = []
145 # Chug: the gate (worst mixing lag) against min_stability_margin.
146 ch = (sr or {}).get("chug") or {}
147 summ = (sr or {}).get("summary") or {}
148 gate = _f(summ.get("gate_margin_threshold")) or 1.05
149 gm = _f(ch.get("margin"))
150 out.append(V("chug", "Chug", gm, "", _grade(gm, gate, 1.0), f"≥ {gate:g}",
151 "Nyquist gain margin of the feed–chamber loop at the worst mixing lag",
152 assumed=("mix_lag", "smd")))
153 # Vaporization: where the slowest stream is 95 % vapour, against the chamber.
154 vap = (sr or {}).get("vaporization") or {}
155 frac = _f(vap.get("frac_vaporized_end"))
156 lv, lch = _f(vap.get("L_vap_m")), _f(vap.get("L_ch_m"))
157 st = "unknown" if frac is None else ("ok" if (lv is not None and lch and lv <= lch) else "warn")
158 out.append(V("vaporized", "Vaporized", frac and 100.0 * frac, "%", st, "95 % inside L_ch",
159 "the droplet march that sets η_vap, slowest stream at the chamber end", assumed=("smd",),
160 digits=1))
161 # Cavitation: the worse hole.
162 cav = r.get("injector_cavitation") or {}
163 margins = [_f((cav.get(k) or {}).get("margin")) for k in ("O", "F")]
164 margins = [m for m in margins if m is not None]
165 m = min(margins) if margins else None
166 out.append(V("cavitation", "Cavitation", m, "×", _grade(m, 1.2, 1.0), "K / K_crit ≥ 1",
167 "Nurick (1976): cavitation margin of the worse orifice", assumed=("cd",)))
168 # Injector stiffness: the softer side, against Huzel & Huang's 15 %.
169 Pc = _f(r.get("Pc"))
170 eta = [(_f(d.get(f"delta_p_injector_{k}")) or 0.0) / Pc for k in ("O", "F")] if Pc else []
171 e = min(eta) if eta else None
172 out.append(V("stiffness", "Injector ΔP/Pc", e and 100.0 * e, "%", _grade(e, 0.15, 0.10), "≥ 15 %",
173 "the softer injector's drop over Pc (Huzel & Huang 4.2)", assumed=_FLOW, digits=0))
174 # Thermochemistry: was the point inside the CEA table, or read off its clamped edge?
175 off = cea_off_table(d, {"Pc": r.get("Pc"), "MR": r.get("MR"), "eps": r.get("eps")})
176 out.append(V("cea_table", "CEA table", "outside" if off else "inside", "", "bad" if off else "ok",
177 "Pc, O/F and ε inside the table",
178 off or "c*, Tc and γ interpolated inside the CEA table at the solved Pc and O/F"))
179 return out
180
181
182def _chamber(cfg: Any, r: Mapping[str, Any]) -> Dict[str, Any]:
183 ci = r.get("chamber_intrinsics") or {}
184 d = r.get("diagnostics") or {}
185 At = _f(r.get("A_throat"))
186 cg = cfg.chamber_geometry
187 Dc = _f(getattr(cg, "chamber_diameter", None))
188 CR = (Dc / math.sqrt(4 * At / math.pi)) ** 2 if (Dc and At) else None
189 qs = [
190 Q("Pc", "Chamber pressure", _f(r.get("Pc")) and r["Pc"] / PSI, "psia", digits=1, assumed=_FLOW + _CSTAR),
191 # The pressure the nozzle relations use: the injector-end Pc less the Rayleigh loss of heat
192 # added to moving gas. Shown so F = Cf · Pc_ns · At and c* = Pc_ns · At / ṁ close from the page.
193 Q("Pc_ns", "Nozzle stagnation pressure", _f(d.get("Pc_ns")) and d["Pc_ns"] / PSI, "psia", digits=2,
194 basis="Pc / κ (Rayleigh loss)", assumed=_FLOW + _CSTAR),
195 Q("kappa", "Rayleigh loss κ", d.get("stagnation_loss_kappa"), "", digits=5, basis="Pc / Pc_ns, from the "
196 "contraction ratio and γ", detail=True),
197 Q("Tc", "Chamber temperature", r.get("Tc"), "K", digits=0, basis="CEA at Pc and O/F"),
198 Q("Lstar", "L*", _f(ci.get("Lstar")) and ci["Lstar"] * 1000.0, "mm", digits=0, basis="V_c / A_t"),
199 Q("theta_c", "Gas residence", _f(ci.get("residence_time")) and ci["residence_time"] * 1000.0, "ms",
200 digits=2, basis="ρ_c V_c / ṁ"),
201 Q("CR", "Contraction ratio", CR, "", digits=2, basis="(D_c / D_t)²"),
202 Q("M_c", "Chamber Mach", ci.get("mach_number_chamber"), "", digits=3, basis="mean gas speed / a"),
203 Q("cstar", "c*", r.get("cstar_actual"), "m/s", digits=0, basis="η c* · c*_CEA = Pc_ns · At / ṁ", assumed=_CSTAR),
204 Q("cstar_ideal", "c* ideal", r.get("cstar_ideal"), "m/s", digits=0, basis="CEA", detail=True),
205 Q("gamma", "γ", r.get("gamma"), "", digits=4, basis="CEA, chamber", detail=True),
206 Q("R", "R", r.get("R"), "J/(kg·K)", digits=1, basis="CEA, chamber", detail=True),
207 Q("rho_c", "Gas density", ci.get("density"), "kg/m³", digits=2, detail=True),
208 Q("a_c", "Sound speed", ci.get("sound_speed"), "m/s", digits=0, detail=True),
209 ]
210 return {"key": "chamber", "title": "Chamber", "summary": ["Tc", "Lstar", "theta_c", "CR"], "quantities": qs}
211
212
213def _nozzle(cfg: Any, r: Mapping[str, Any]) -> Dict[str, Any]:
214 Pa = _f(r.get("P_ambient"))
215 pe = _f(r.get("P_exit"))
216 sep = pe / Pa if (pe and Pa) else None
217 qs = [
218 Q("Cf", "Cf", r.get("Cf_actual"), "", digits=4, basis="F / (Pc_ns · At) = ζ_n · Cf_vac − pa ε / Pc_ns", assumed=("nozzle",)),
219 Q("Cf_ideal", "Cf ideal", r.get("Cf_ideal"), "", digits=4, basis="CEA", detail=True),
220 Q("eps", "Expansion ratio", r.get("eps"), "", digits=2, basis="A_e / A_t"),
221 Q("P_exit", "Exit pressure", pe and pe / PSI, "psia", digits=2, basis="CEA Pc/Pe at ε"),
222 Q("pe_pa", "Exit / ambient", sep, "", digits=2, basis="Summerfield: separation below ~0.4",
223 status=_grade(sep, 0.4) if sep is not None else None),
224 Q("v_exit", "Exit velocity", r.get("v_exit"), "m/s", digits=0, detail=True),
225 Q("M_exit", "Exit Mach", r.get("M_exit"), "", digits=2, detail=True),
226 Q("At", "Throat area", _f(r.get("A_throat")) and r["A_throat"] * 1e6, "mm²", digits=1, detail=True),
227 ]
228 return {"key": "nozzle", "title": "Nozzle", "summary": ["Cf", "eps", "pe_pa"], "quantities": qs}
229
230
231def _injector(cfg: Any, r: Mapping[str, Any]) -> Dict[str, Any]:
232 d = r.get("diagnostics") or {}
233 Pc = _f(r.get("Pc"))
234 cav = r.get("injector_cavitation") or {}
235 tag = {"O": "LOX", "F": "fuel"}
236 per: Dict[str, List[Dict[str, Any]]] = {}
237 for k in ("O", "F"):
238 dp = _f(d.get(f"delta_p_injector_{k}"))
239 eta = dp / Pc if (dp and Pc) else None
240 per[k] = [
241 Q(f"dpPc_{k}", f"ΔP/Pc {tag[k]}", eta and 100.0 * eta, "%", digits=1, basis="injector drop over Pc",
242 assumed=_FLOW, status=_grade(eta, 0.15, 0.10)),
243 Q(f"dp_inj_{k}", f"ΔP injector {tag[k]}", dp and dp / PSI, "psi", digits=1, assumed=_FLOW),
244 Q(f"dp_feed_{k}", f"ΔP feed {tag[k]}", _f(d.get(f"delta_p_feed_{k}")) and d[f"delta_p_feed_{k}"] / PSI,
245 "psi", digits=1, basis="tank to injector: line friction, fittings, manifold", assumed=("manifold",)),
246 Q(f"Cd_{k}", f"Cd {tag[k]}", r.get(f"Cd_{k}"), "", digits=3, assumed=("cd",),
247 basis="hole Cd at the solved Re (cavitation limited)"),
248 Q(f"v_{k}", f"Jet velocity {tag[k]}", d.get(f"v_{k}_bulk"), "m/s", digits=1,
249 basis="ṁ / (ρ A_hole): the jet fills the bore (attached, L/d ≥ 2)", assumed=_FLOW),
250 Q(f"cav_{k}", f"Cavitation margin {tag[k]}", (cav.get(k) or {}).get("margin"), "×", digits=2,
251 basis="K / K_crit (Nurick 1976)", assumed=("cd",),
252 status=_grade(_f((cav.get(k) or {}).get("margin")), 1.2, 1.0)),
253 Q(f"mdot_{k}", f"ṁ {tag[k]}", r.get(f"mdot_{k}"), "kg/s", digits=3, assumed=_FLOW, detail=True),
254 Q(f"P_inj_{k}", f"P injector {tag[k]}", _f(d.get(f"P_injector_{k}")) and d[f"P_injector_{k}"] / PSI,
255 "psia", digits=1, detail=True),
256 ]
257 # The lines themselves: their own acoustic modes and the valve-slam spike.
258 lines = ((((r.get("stability") or {}).get("feed_system")) or {}).get("feed_lines")) or {}
259 for k, side in (("O", "oxidizer"), ("F", "fuel")):
260 ln = lines.get(side) or {}
261 per[k] += [
262 Q(f"line_qw_{k}", f"Line quarter-wave {tag[k]}", ln.get("pogo_frequency"), "Hz", digits=0,
263 basis="a / 4L, tank end open; for spotting a coincidence with chug or the chamber", detail=True),
264 Q(f"hammer_{k}", f"Water hammer {tag[k]}",
265 _f(ln.get("water_hammer_pressure")) and ln["water_hammer_pressure"] / PSI, "psi", digits=0,
266 basis="Joukowsky ρ·a·v for an instantaneous valve stop (upper bound)", detail=True),
267 ]
268 # LOX and fuel side by side: the view lays quantities out two to a row.
269 qs: List[Dict[str, Any]] = [q for pair in zip(per["O"], per["F"]) for q in pair]
270 lo, hi = _f(d.get("element_mixture_ratio_min")), _f(d.get("element_mixture_ratio_max"))
271 MR = _f(r.get("MR"))
272 if lo is not None and hi is not None and MR:
273 qs.append(Q("of_spread", "Element O/F spread", 100.0 * (hi - lo) / MR, "%", digits=2,
274 basis="ring-manifold solve, around each ring", assumed=("manifold",)))
275 return {"key": "injector", "title": "Feed & injector",
276 "summary": ["dpPc_O", "dpPc_F", "Cd_O", "cav_O"], "quantities": qs}
277
278
279def _spray(cfg: Any, r: Mapping[str, Any], sr: Mapping[str, Any]) -> Dict[str, Any]:
280 d = r.get("diagnostics") or {}
281 ce = d.get("cstar_efficiency") or {}
282 vap = (sr or {}).get("vaporization") or {}
283 M = _f(d.get("rupe_M"))
284 qs = [
285 Q("D32_O", "D32 LOX", _f(d.get("D32_O")) and d["D32_O"] * 1e6, "µm", digits=0, basis="NACA TN 4222",
286 assumed=("smd",)),
287 Q("D32_F", "D32 fuel", _f(d.get("D32_F")) and d["D32_F"] * 1e6, "µm", digits=0, basis="NACA TN 4222",
288 assumed=("smd",)),
289 Q("rupe_M", "Rupe M", M, "", digits=3, basis="ρO vO² dO / (ρF vF² dF); best mixing near 1",
290 status=("ok" if M is not None and 0.8 <= M <= 1.25 else "warn") if M is not None else None, assumed=_FLOW),
291 Q("Em", "Mixing factor E_m", ce.get("rupe_Em"), "", digits=3, basis="Rupe, at this element's M",
292 assumed=("em",)),
293 Q("eta_mix", "η mixing", ce.get("eta_mixing"), "", digits=4, basis="stream-tube c* over the E_m spread",
294 assumed=("em",)),
295 Q("eta_vap", "η vaporization", ce.get("eta_vaporization"), "", digits=4, basis="droplet march",
296 assumed=("smd",)),
297 Q("L_vap", "95 % vaporized at", _f(vap.get("L_vap_m")) and vap["L_vap_m"] * 1000.0, "mm", digits=0,
298 basis=f"slowest stream ({vap.get('fluid', '')}), from the face", assumed=("smd",)),
299 Q("L_ch", "Chamber (L*·At/Ac)", _f(vap.get("L_ch_m")) and vap["L_ch_m"] * 1000.0, "mm", digits=0,
300 basis="the chamber the droplet march runs in"),
301 Q("eta_hl", "η heat loss", ce.get("eta_heat_loss"), "", digits=4, detail=True, assumed=("wall_heat",)),
302 ]
303 return {"key": "spray", "title": "Spray & mixing", "summary": ["rupe_M", "Em", "L_vap", "eta_vap"],
304 "quantities": qs}
305
306
307def _cooling(cfg: Any, r: Mapping[str, Any]) -> Optional[Dict[str, Any]]:
308 ab = ((r.get("cooling") or {}).get("ablative")) or {}
309 if not ab.get("enabled"):
310 return None
311 qs = [
312 Q("recession", "Recession rate", _f(ab.get("recession_rate_mean")) and ab["recession_rate_mean"] * 1e6,
313 "µm/s", digits=0, basis="liner mean", assumed=("wall_heat",)),
314 Q("recession_peak", "Peak recession", _f(ab.get("recession_rate_peak")) and ab["recession_rate_peak"] * 1e6,
315 "µm/s", digits=0, basis="at the liner's hottest station", assumed=("wall_heat",)),
316 Q("q_incident", "Incident heat flux", _f(ab.get("incident_heat_flux")) and ab["incident_heat_flux"] / 1e6,
317 "MW/m²", digits=2, basis="convective + radiative", assumed=("wall_heat",)),
318 Q("T_surface", "Surface temperature", ab.get("surface_temperature"), "K", digits=0, detail=True),
319 Q("cooling_power", "Heat to the liner", _f(ab.get("cooling_power")) and ab["cooling_power"] / 1000.0, "kW",
320 digits=0, assumed=("wall_heat",), detail=True),
321 ]
322 return {"key": "cooling", "title": "Cooling", "summary": ["recession", "recession_peak", "q_incident"],
323 "quantities": qs}
324
325
326def _stability(sr: Mapping[str, Any]) -> Dict[str, Any]:
327 ch = (sr or {}).get("chug") or {}
328 ac = (sr or {}).get("acoustic") or {}
329 worst = min((m.get("margin_worst_phase") for m in ac.get("modes", []) if m.get("margin_worst_phase") is not None),
330 default=None)
331 qs = [
332 Q("chug_gate", "Chug gain margin (gate)", ch.get("margin"), "", digits=2,
333 basis="worst mixing lag in its band", assumed=("mix_lag", "smd")),
334 Q("chug_nominal", "Chug gain margin (nominal)", ch.get("gain_margin_nominal"), "", digits=2,
335 assumed=("mix_lag", "smd")),
336 Q("chug_f", "Chug frequency", ch.get("freq_hz"), "Hz", digits=0, assumed=("mix_lag", "smd")),
337 Q("ac_worst", "Acoustic, worst phase", worst, "", digits=2,
338 basis="report only: damping over the most driving any lag could give",
339 assumed=("n_chi", "ac_damping")),
340 ]
341 return {"key": "stability", "title": "Stability", "summary": ["chug_gate", "chug_f", "ac_worst"],
342 "quantities": qs}
343
344
345def _calibration(cfg: Any, used: Sequence[str]) -> Dict[str, Dict[str, Any]]:
346 """The inputs this result rests on, each assumed or measured (engine/pipeline/measurements.py)."""
347 from engine.pipeline.measurements import calibration_state
348 st = calibration_state(cfg)
349 out = {}
350 for k in used:
351 s = st.get(k, {"state": "assumed", "sources": []})
352 out[k] = {**CALIBRATION[k], "state": s["state"], "sources": s["sources"]}
353 return out
354
355
356def _references(cfg: Any, sr: Mapping[str, Any]) -> List[Dict[str, Any]]:
357 """Measurements that are compared with the model rather than fed into it."""
358 ms = getattr(cfg, "measurements", None)
359 out = []
360 cf = getattr(ms, "chug_frequency_hz", None) if ms is not None else None
361 if cf is not None:
362 out.append({"key": "chug_frequency", "label": "Chug frequency", "unit": "Hz", "measured": cf.value,
363 "uncertainty": cf.uncertainty, "source": cf.source,
364 "model": _f(((sr or {}).get("chug") or {}).get("freq_hz"))})
365 return out
366
367
368# ---- the report -----------------------------------------------------------------------------------
369
370def forward_report(cfg: Any, r: Mapping[str, Any], *, handcheck_rows: Optional[Mapping[str, Any]] = None
371 ) -> Dict[str, Any]:
372 """Forward mode's view of solve ``r`` of ``cfg``. ``r`` must carry ``stability_rich``."""
373 sr = r.get("stability_rich") or {}
374 sections = [_chamber(cfg, r), _nozzle(cfg, r), _injector(cfg, r), _spray(cfg, r, sr), _cooling(cfg, r),
375 _stability(sr)]
376 sections = [s for s in sections if s is not None]
377 # A hot-fire chug frequency sits beside the model's, in the Stability section.
378 for ref in _references(cfg, sr):
379 if ref["key"] == "chug_frequency":
380 st = next(s for s in sections if s["key"] == "stability")
381 st["quantities"].append(Q("chug_f_measured", "Chug frequency, measured", ref["measured"], "Hz", digits=0,
382 basis=f"hot fire: {ref['source']}"))
383 st["summary"].append("chug_f_measured")
384 used = sorted({a for s in sections for q in s["quantities"] for a in q["assumed"]}
385 | {a for q in _headline(r) for a in q["assumed"]})
386 return {
387 "headline": _headline(r),
388 "verdicts": _verdicts(cfg, r, sr),
389 "sections": sections,
390 "calibration": _calibration(cfg, used),
391 "measured_reference": _references(cfg, sr),
392 "stability": sr,
393 "handcheck": handcheck_rows,
394 }
#define min(a, b)
Definition Arduino.h:42
Optional[Dict[str, Any]] _cooling(Any cfg, Mapping[str, Any] r)
str _grade(Optional[float] value, float ok, Optional[float] warn=None, *, bool higher_is_better=True)
List[Dict[str, Any]] _headline(Mapping[str, Any] r)
Dict[str, Any] Q(str key, str label, Any value, str unit="", *, int digits=3, str basis="", Sequence[str] assumed=(), Optional[str] status=None, Optional[str] band=None, bool detail=False)
Dict[str, Any] _spray(Any cfg, Mapping[str, Any] r, Mapping[str, Any] sr)
Dict[str, Any] V(str key, str label, Any value, str unit, str status, str threshold, str basis, Sequence[str] assumed=(), int digits=2)
List[Dict[str, Any]] _verdicts(Any cfg, Mapping[str, Any] r, Mapping[str, Any] sr)
Dict[str, Any] _stability(Mapping[str, Any] sr)
List[Dict[str, Any]] _references(Any cfg, Mapping[str, Any] sr)
Dict[str, Any] _nozzle(Any cfg, Mapping[str, Any] r)
Dict[str, Any] _injector(Any cfg, Mapping[str, Any] r)
Optional[float] _f(Any v)
Dict[str, Dict[str, Any]] _calibration(Any cfg, Sequence[str] used)
Dict[str, Any] _chamber(Any cfg, Mapping[str, Any] r)
Optional[str] cea_off_table(Mapping[str, Any] d, Optional[Mapping[str, Any]] at=None)