1"""Forward mode's result, one number per quantity.
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.
9A verdict carries its own status and threshold, decided here, so no card grades anything.
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]}
15from __future__
import annotations
18from typing
import Any, Dict, List, Mapping, Optional, Sequence
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"},
51_FLOW = (
"cd",
"manifold")
53_THRUST = _FLOW + _CSTAR + (
"nozzle",)
56def _f(v: Any) -> Optional[float]:
59 except (TypeError, ValueError):
61 return x
if math.isfinite(x)
else None
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)}
71 out[
"status"] = status
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)}
87_CEA_AXES = {
"Pc": (
"Pc", 1.0 / PSI,
"psia", 0),
"MR": (
"O/F", 1.0,
"", 2),
"eps": (
"ε", 1.0,
"", 2)}
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.
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.
98 axes = [a
for a
in (d.get(
"cea_clamped")
or ())
if a
in _CEA_AXES]
101 rng = d.get(
"cea_table_range")
or {}
104 name, k, unit, dg = _CEA_AXES[a]
105 u = f
" {unit}" if unit
else ""
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"
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."""
120 return "warn" if (warn
is not None and value >= warn)
else "bad"
123 return "warn" if (warn
is not None and value <= warn)
else "bad"
128def _headline(r: Mapping[str, Any]) -> List[Dict[str, Any]]:
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)",
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",
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]] = []
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")))
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",),
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",)))
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))
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"))
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
190 Q(
"Pc",
"Chamber pressure",
_f(r.get(
"Pc"))
and r[
"Pc"] / PSI,
"psia", digits=1, assumed=_FLOW + _CSTAR),
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),
210 return {
"key":
"chamber",
"title":
"Chamber",
"summary": [
"Tc",
"Lstar",
"theta_c",
"CR"],
"quantities": qs}
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
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),
228 return {
"key":
"nozzle",
"title":
"Nozzle",
"summary": [
"Cf",
"eps",
"pe_pa"],
"quantities": qs}
231def _injector(cfg: Any, r: Mapping[str, Any]) -> Dict[str, Any]:
232 d = r.get(
"diagnostics")
or {}
234 cav = r.get(
"injector_cavitation")
or {}
235 tag = {
"O":
"LOX",
"F":
"fuel"}
236 per: Dict[str, List[Dict[str, Any]]] = {}
238 dp =
_f(d.get(f
"delta_p_injector_{k}"))
239 eta = dp / Pc
if (dp
and Pc)
else None
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),
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 {}
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),
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"))
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}
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"))
285 Q(
"D32_O",
"D32 LOX",
_f(d.get(
"D32_O"))
and d[
"D32_O"] * 1e6,
"µm", digits=0, basis=
"NACA TN 4222",
287 Q(
"D32_F",
"D32 fuel",
_f(d.get(
"D32_F"))
and d[
"D32_F"] * 1e6,
"µm", digits=0, basis=
"NACA TN 4222",
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",
293 Q(
"eta_mix",
"η mixing", ce.get(
"eta_mixing"),
"", digits=4, basis=
"stream-tube c* over the E_m spread",
295 Q(
"eta_vap",
"η vaporization", ce.get(
"eta_vaporization"),
"", digits=4, basis=
"droplet march",
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",)),
303 return {
"key":
"spray",
"title":
"Spray & mixing",
"summary": [
"rupe_M",
"Em",
"L_vap",
"eta_vap"],
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"):
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),
322 return {
"key":
"cooling",
"title":
"Cooling",
"summary": [
"recession",
"recession_peak",
"q_incident"],
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),
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")),
341 return {
"key":
"stability",
"title":
"Stability",
"summary": [
"chug_gate",
"chug_f",
"ac_worst"],
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)."""
348 st = calibration_state(cfg)
351 s = st.get(k, {
"state":
"assumed",
"sources": []})
352 out[k] = {**CALIBRATION[k],
"state": s[
"state"],
"sources": s[
"sources"]}
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)
360 cf = getattr(ms,
"chug_frequency_hz",
None)
if ms
is not None else 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"))})
370def forward_report(cfg: Any, r: Mapping[str, Any], *, handcheck_rows: Optional[Mapping[str, Any]] =
None
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),
376 sections = [s
for s
in sections
if s
is not None]
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"]})
389 "sections": sections,
393 "handcheck": handcheck_rows,
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)