39 eta_window: Optional[Tuple[float, float]] =
None) -> List[List[float]]:
40 """Viz #1: the chug stability boundary in the design's own frame: x = the streams' mass-
41 weighted injector stiffness, reached by scaling every stream's drop by one factor; y = their
42 mass-weighted lag over theta_c, reached by scaling every lag by one factor k. At each x, k is
43 bisected to gain margin 1. The design itself is the point (1, 1) of both scalings, so it sits
44 in the same plane as the curve (``_chug_design_point``) -- the old curve set every stream to
45 one common eta and scaled their mean lag, and then plotted each stream against it, which is
46 two different systems on one chart."""
48 theta_c = chamber.theta_c()
49 if not np.isfinite(theta_c)
or theta_c <= 0:
51 eta0 = chug.mean_eta(streams)
53 lo_eta, hi_eta = eta_window
if eta_window
else _eta_window(streams)
54 curve: List[List[float]] = []
55 for eta
in np.linspace(lo_eta, hi_eta, n_pts):
56 def gm_at(kfac: float) -> float:
60 s2.eta_inj = float(s.eta_inj * eta / eta0)
61 s2.tau_conv = float(s.tau_conv * kfac)
63 return chug.chug_margin_fast(sc, chamber)[
"gain_margin"] - 1.0
65 ks = np.geomspace(0.05, 20.0, 41)
66 fs = [gm_at(float(k))
for k
in ks]
67 j = next((i
for i
in range(1, len(ks))
if np.isfinite(fs[i - 1])
and np.isfinite(fs[i])
68 and fs[i - 1] > 0 >= fs[i]),
None)
69 if j
is None or fs[0] <= 0:
71 lo, hi = float(ks[j - 1]), float(ks[j])
74 mid = float(np.sqrt(lo * hi))
75 if gm_at(mid) * f_lo > 0:
79 curve.append([float(eta), float(tau0 * np.sqrt(lo * hi) / theta_c)])
138 """d^2-law droplet decay for ONE stream. ``key`` is "O" or "F". Used only when the solve
139 carried no droplet march (see ``_stream_from_march``): the drop's speed held at injection and
140 its life taken as the chug convective lag, which over-counts both ways."""
143 D32 = float(inp[f
"D32_{key}"])
144 L_ch = float(inp[
"L_ch"])
145 phase = str(inp.get(f
"phase_{key}",
"liquid"))
146 fluid = str(inp.get(f
"fluid_name_{key}", key))
147 tau_vap = float(inp[f
"tau_conv_{key}"])
148 side =
"oxidizer" if key ==
"O" else "fuel"
150 if phase.lower().startswith(
"g"):
152 return {
"stream": key,
"fluid": fluid,
"phase": phase,
"smd_um":
None,
153 "tau_conv_s": tau_vap,
"L_vap_m":
None,
"L_ch_m": L_ch,
154 "vaporized_in_chamber":
True,
"d2_profile": [],
155 "note": f
"{fluid} is injected as a gas — no atomization or vaporization to plot."}
157 rho = inp.get(f
"rho_{key}")
158 if rho
is None or not np.isfinite(float(rho))
or float(rho) <= 0.0:
159 rho = assume(f
"stability.viz.rho_{side}", 1140.0
if key ==
"O" else 800.0, unit=
"kg/m^3",
160 reason=f
"{side} density missing when drawing the vaporization profile")
162 eta = float(inp[f
"eta_inj_{key}"])
166 u = inp.get(f
"u_{key}")
167 if u
is not None and np.isfinite(float(u))
and float(u) > 0.0:
170 Cd = inp.get(f
"Cd_{key}")
171 if Cd
is None or not np.isfinite(float(Cd))
or float(Cd) <= 0.0:
172 Cd = assume(f
"stability.viz.Cd_{side}", 0.6, unit=
"-",
173 reason=f
"solved {side} discharge coefficient unavailable for the droplet "
174 f
"velocity; sharp-edged-orifice value")
175 v_drop = float(Cd) * float(np.sqrt(
max(2.0 * eta * Pc / rho, 1.0)))
177 L_vap = v_drop * tau_vap
if np.isfinite(tau_vap)
else float(
"nan")
178 x_max = float(
max(L_ch, L_vap
if np.isfinite(L_vap)
else L_ch) * 1.1)
179 xs = np.linspace(0.0, x_max, n_pts)
180 d2 = (np.clip(1.0 - xs / L_vap, 0.0, 1.0)
181 if (np.isfinite(L_vap)
and L_vap > 0)
else np.ones_like(xs))
183 "stream": key,
"fluid": fluid,
"phase": phase,
184 "smd_um": float(D32 * 1e6),
"smd_band_um": [float(D32 * 0.8e6), float(D32 * 1.2e6)],
185 "tau_conv_s": float(tau_vap),
186 "L_vap_m": float(L_vap),
"L_ch_m": L_ch,
187 "vaporized_in_chamber": bool(np.isfinite(L_vap)
and L_vap <= L_ch),
188 "d2_profile": [[float(x), float(y)]
for x, y
in zip(xs, d2)],
232def _sensitivity(inp: Dict[str, Any], nominal_gm: float) -> Dict[str, Any]:
233 """Sensitivity of both verdicts to the inputs nobody has measured (cheap sweeps).
235 Acoustic: alpha of the limiting mode over n and chi (chi scales the RATE-LIMITING stream's lag,
236 which is what tau_sens is built from). Chug: GM over the feed lengths (x0.5, x2) and over a
237 uniform scale on every conversion lag (x0.5, x2). The liquid bulk modulus does not enter the
238 lumped chug loop at all (no line compliance), so the chug verdict cannot move with it."""
240 D_ch, L_ch, gas, coeffs = inp[
"D_ch"], inp[
"L_ch"], inp[
"gas"], inp[
"damping_coeffs"]
241 tv = inp[
"tau_rate_limiting"]
242 a_n = [acoustic.fast_acoustic(D_ch, L_ch, gas, n=nn, tau_sens=inp[
"tau_sens"], coeffs=coeffs)[
"alpha_max"]
243 for nn
in (0.3, 0.6)]
245 a_chi = [acoustic.fast_acoustic(D_ch, L_ch, gas, n=inp[
"n_interaction"], tau_sens=cc * tv, coeffs=coeffs)[
"alpha_max"]
246 for cc
in np.linspace(0.05, 0.30, 51)]
248 def gm_scaled(attr: str, k: float) -> float:
250 for s0
in inp[
"streams"]:
252 setattr(s1, attr, float(getattr(s0, attr)) * k)
254 return float(chug.chug_margin_fast(st, inp[
"chamber"])[
"gain_margin"])
256 return {
"acoustic_alpha_vs_n": [float(
min(a_n)), float(
max(a_n))],
257 "acoustic_alpha_vs_chi": [float(
min(a_chi)), float(
max(a_chi))],
258 "chug_gm_nominal": float(nominal_gm),
259 "chug_gm_vs_feed_length": {
"x0.5": gm_scaled(
"feed_length", 0.5),
"x2": gm_scaled(
"feed_length", 2.0)},
260 "chug_gm_vs_lag_scale": {
"x0.5": gm_scaled(
"tau_conv", 0.5),
"x2": gm_scaled(
"tau_conv", 2.0)},
261 "chug_bulk_modulus":
"not an input: the lumped chug loop has no line compliance"}
290def _radar(chug_margin: float, ac: Dict[str, Any], vap: Dict[str, Any],
291 gate_threshold: float) -> Dict[str, Any]:
292 """Viz #7: one-glance health radar. Every axis is a ratio that is 1 at neutral stability:
293 chug GM, acoustic damping/driving per mode (nominal phase, report-only unless gated), and
294 L_ch/L_vap for vaporization. Plot caps: 3 for the margins, 1.3 for vaporization (an infinite
295 damping/driving would otherwise serialize as null)."""
296 def mode_margin(name):
297 for m
in ac[
"modes"]:
298 if m[
"mode"] == name:
301 lv = vap.get(
"L_vap_m")
302 if lv
is not None and np.isfinite(lv)
and lv > 0:
303 vap_complete = float(np.clip(vap[
"L_ch_m"] / lv, 0.0, 1.3))
304 elif vap.get(
"frac_vaporized_end")
is not None:
306 vap_complete = float(np.clip(vap[
"frac_vaporized_end"] / 0.95, 0.0, 1.0))
309 def worst_phase(name):
310 for m
in ac[
"modes"]:
311 if m[
"mode"] == name:
312 return m[
"margin_worst_phase"]
314 axes = [
"chug",
"1L",
"1T",
"vaporization"]
315 cap =
lambda v: float(np.clip(v, 0.0, 3.0))
if not np.isnan(v)
else float(
"nan")
318 values = [cap(chug_margin), cap(worst_phase(
"1L")), cap(worst_phase(
"1T")), vap_complete]
319 return {
"axes": axes,
"values": values,
"threshold": [gate_threshold] * len(axes),
320 "gated": [
True,
False,
False,
False],
321 "basis": [
"chug Nyquist gain margin, worst mixing lag (the gate)",
322 "damping / worst-phase driving (uncalibrated damping; report only)",
323 "damping / worst-phase driving (uncalibrated damping; report only)",
324 "L_ch / L_vap(95 %) from the droplet march (performance, not stability)"]}
340def _diagnostics(state: str, chug_margin: float, acoustic_margin: float, gate_threshold: float,
341 limiting: Optional[str], chug_rich: Dict[str, Any], ac: Dict[str, Any],
342 vap: Dict[str, Any], fallbacks: List[Dict[str, Any]],
343 acoustic_gate: str =
"report_only") -> Dict[str, Any]:
344 """Turn the rich quantities into a verdict, findings, and design actions tied to the
345 sensitivity sliders (η_inj, SMD, n, χ). Derived from the SAME numbers the cards render,
346 so the headline can never disagree with the charts."""
347 findings: List[Dict[str, str]] = []
348 actions: List[Dict[str, Optional[str]]] = []
349 ac_gated = acoustic_gate !=
"report_only"
352 chug_stable = chug_rich.get(
"stable",
True)
353 chug_f = chug_rich.get(
"f_chug_hz")
354 driver = chug_rich.get(
"driver",
"unknown")
355 driver_lbl = _DRIVER_LABEL.get(driver, driver)
356 if not chug_stable
or not (chug_margin >= gate_threshold):
357 txt =
"Chug " + (
"grows" if not chug_stable
else "is short of margin")
358 if chug_f
is not None and np.isfinite(chug_f)
and chug_f > 0:
359 txt += f
" near {chug_f:.0f} Hz"
360 txt += f
": gain margin {chug_margin:.2f} against {gate_threshold:.2f}, driven by {driver_lbl}."
361 findings.append({
"severity":
"critical" if not chug_stable
else "warn",
"text": txt})
363 "text":
"Raise injector ΔP/Pc.",
364 "rationale":
"A stiffer injector decouples chamber-pressure oscillations from the feed, "
365 "shrinking the chug loop gain.",
369 "text":
"Atomize finer (smaller D32).",
370 "rationale":
"Finer droplets shorten the vaporization lag τ, pushing the chug pole left.",
375 ac_modes = ac.get(
"modes", [])
376 lim_name = ac.get(
"limiting_mode")
377 lim = next((m
for m
in ac_modes
if m.get(
"mode") == lim_name), ac_modes[0]
if ac_modes
else None)
379 txt = (
"Acoustic stability is not assessed: the damping is uncalibrated. Rate it by test "
380 "(≥ 25 kHz Pc, pulse or bomb; Harrje & Reardon SP-194).")
381 findings.append({
"severity":
"warn",
"text": txt})
382 elif ac.get(
"any_unstable")
or not (acoustic_margin >= gate_threshold):
384 driven = lim.get(
"alpha", 0.0) > 0
385 txt = (f
"Acoustic mode {lim.get('mode')} ({lim.get('f_hz', 0):.0f} Hz) is "
386 f
"{'driven' if driven else 'only lightly damped'} "
387 f
"(α={lim.get('alpha', 0):.0f} 1/s, damping/driving {acoustic_margin:.2f}).")
389 txt = f
"Acoustic margin is low ({acoustic_margin:.2f})."
390 findings.append({
"severity":
"critical" if ac.get(
"any_unstable")
else "warn",
"text": txt})
392 "text": f
"Add chamber acoustic damping (baffles / acoustic liner) tuned to "
393 f
"{lim_name or 'the limiting mode'}.",
394 "rationale":
"Passive damping raises the nozzle/viscous/two-phase budget against the "
395 "combustion driving term.",
399 "text":
"Soften the combustion response (lower n, check χ).",
400 "rationale":
"A smaller n weakens the heat-release feedback that drives the mode (Rayleigh criterion).",
405 if not vap.get(
"vaporized_in_chamber",
True):
406 lvap, lch = vap.get(
"L_vap_m"), vap.get(
"L_ch_m")
407 ratio = (lvap / lch)
if (lch
and lvap
is not None and np.isfinite(lvap)
and lch > 0)
else float(
"nan")
408 txt =
"Spray not 95 % vaporized in the chamber"
409 if np.isfinite(ratio):
410 txt += f
" (L_vap ≈ {ratio:.1f}× L_ch)"
411 elif vap.get(
"frac_vaporized_end")
is not None:
412 txt += f
" ({vap['frac_vaporized_end'] * 100:.1f} % of the {vap.get('fluid', '')} by the chamber end)"
414 findings.append({
"severity":
"warn",
"text": txt})
416 "text":
"Atomize finer or lengthen the chamber.",
417 "rationale":
"A shorter vaporization length completes burning upstream of the nozzle.",
422 findings.append({
"severity":
"ok",
423 "text":
"Nothing driven."})
425 gm_txt = f
"chug gain margin {chug_margin:.2f} against {gate_threshold:.2f}"
426 if state ==
"stable":
427 headline = f
"Stable: {gm_txt}."
428 elif state ==
"marginal":
429 headline = f
"Marginal: {gm_txt}."
430 elif state ==
"unknown":
431 headline =
"Not assessed: the stability model could not be evaluated here."
433 headline = f
"Unstable: {limiting or 'a mode'} is driven ({gm_txt})."
437 names =
", ".join(str(f.get(
"name",
"?"))
for f
in fb[:3])
438 more =
"…" if len(fb) > 3
else ""
442 kinds = {(
"propellant" if ".fluids." in str(f.get(
"name",
""))
else
443 "plumbing" if ".feed." in str(f.get(
"name",
""))
else
444 "model")
for f
in fb}
446 if "propellant" in kinds:
447 hints.append(
"load a propellant preset for the fluid properties")
448 if "plumbing" in kinds:
449 hints.append(
"set feed_system lengths/bores for the plumbing")
451 hints.append(
"the rest are model calibration defaults")
452 assumptions_note = (f
"{len(fb)} physics input(s) fell back to recorded defaults "
453 f
"({names}{more}). " +
"; ".join(hints).capitalize() +
".")
455 assumptions_note =
"Config fully specified the stability physics — no fallbacks used."
458 "headline": headline,
460 "limiting_mode": limiting,
461 "driver": driver_lbl
if limiting ==
"chug" else None,
462 "findings": findings,
464 "assumptions_note": assumptions_note,
473 gamma: float, R: float, Tc: float, diagnostics: Dict[str, Any],
474 cg: Any, *, gate_threshold: Optional[float] =
None,
475 overrides: Optional[Dict[str, float]] =
None) -> Dict[str, Any]:
476 """Assemble the full rich stability payload (plan §A5 schema). <=5 s.
478 ``gate_threshold`` defaults to design_requirements.min_stability_margin (as Layer 1 relaxes
479 it), so the report and the optimizer gate on the same number."""
481 if gate_threshold
is None:
482 gate_threshold = analysis._stability_requirement(config)
483 with _assumptions.scope()
as _used_here:
484 return _build_rich_report(config, Pc, MR, mdot_total, cstar, gamma, R, Tc, diagnostics, cg,
485 gate_threshold=gate_threshold, overrides=overrides,
486 used_here=_used_here)
490 gamma: float, R: float, Tc: float, diagnostics: Dict[str, Any],
491 cg: Any, *, gate_threshold: float, overrides: Optional[Dict[str, float]],
492 used_here: Dict[str, Any]) -> Dict[str, Any]:
493 inp = analysis.build_stability_inputs(
494 config, Pc, MR, mdot_total, cstar, gamma, R, Tc, diagnostics, cg, overrides=overrides,
496 streams, chamber, gas = inp[
"streams"], inp[
"chamber"], inp[
"gas"]
499 chug_rich = chug.chug_growth_rate(streams, chamber)
500 chug_fast = chug.chug_margin_fast(streams, chamber)
501 gm_nominal = float(chug_fast.get(
"gain_margin", float(
"nan")))
502 band = analysis.chug_band(inp, gm_nominal)
503 chug_margin = band[
"min"]
if band
is not None else gm_nominal
507 theta_c = chamber.theta_c()
509 lag_break = inp.get(
"lag_breakdown")
or {}
510 for label, eta, tau
in (
511 (
"O", inp[
"eta_inj_O"], inp[
"tau_conv_O"]),
512 (
"F", inp[
"eta_inj_F"], inp[
"tau_conv_F"]),
514 tt = float(tau / theta_c)
if (np.isfinite(theta_c)
and theta_c > 0)
else float(
"nan")
515 lb = lag_break.get(label, {})
516 design_streams.append({
518 "fluid": inp.get(f
"fluid_name_{label}", label),
519 "phase": inp.get(f
"phase_{label}",
"liquid"),
520 "eta_inj": float(eta),
"tau_theta_c": tt,
"tau_s": float(tau),
521 "tau_atom_s": lb.get(
"tau_atom_s"),
"tau_vap_s": lb.get(
"tau_vap_s"),
522 "tau_mix_s": lb.get(
"tau_mix_s"),
526 locus = chug.chug_root_locus(
527 streams, chamber, eta_values=np.linspace(eta_window[0], eta_window[1], 26), scale_design=
True)
531 ac = acoustic.analyze_acoustic_modes(inp[
"D_ch"], inp[
"L_ch"], gas,
532 n=inp[
"n_interaction"], tau_sens=inp[
"tau_sens"],
533 coeffs=inp[
"damping_coeffs"])
534 ac_gate_mode = inp[
"acoustic_gate"]
535 ac_gated = ac_gate_mode !=
"report_only"
536 m_key =
"margin_worst_phase" if ac_gate_mode ==
"worst_phase" else "margin"
537 ac_margins = [m[m_key]
for m
in ac[
"modes"]]
538 acoustic_margin_model = float(
min(ac_margins))
if ac_margins
else float(
"nan")
539 acoustic_margin = acoustic_margin_model
if ac_gated
else float(
"inf")
541 "name": m[
"mode"],
"freq_hz": m[
"f_hz"],
"alpha": m[
"alpha"],
542 "driving": m[
"driving"],
"driving_max": m[
"driving_max"],
543 "margin": m[
"margin"],
"margin_worst_phase": m[
"margin_worst_phase"],
"n_min": m[
"n_min"],
544 "damping": {
"noz": m[
"damping"][
"nozzle"],
"visc": m[
"damping"][
"viscous"],
545 "inj": m[
"damping"][
"injector"],
"twophase": m[
"damping"][
"twophase"]},
546 }
for m
in ac[
"modes"]]
552 for m
in ac[
"modes"]:
553 wt = float(2 * np.pi * m[
"f_hz"] * inp[
"tau_sens"])
554 phase.append({
"mode": m[
"mode"],
"omega_tau": wt,
"omega_tau_mod": float(wt % (2 * np.pi)),
555 "drive_share": float(m[
"driving"] / m[
"driving_max"])
if m[
"driving_max"] > 0
else 0.0})
559 march = vaporization_profile((diagnostics
or {}).get(
"cstar_efficiency")
or {})
565 radar =
_radar(chug_margin, ac, vap, gate_threshold)
567 ac_alpha_nominal = ac[
"modes"][0][
"alpha"]
if ac[
"modes"]
else float(
"nan")
568 state = analysis.classify_stability(gm_nominal, chug_margin, acoustic_margin,
569 ac_alpha_nominal, ac_gated, gate_threshold)
570 if chug_rich.get(
"stable")
is False and state
in (
"stable",
"marginal"):
572 min_margin = float(
min(chug_margin, acoustic_margin))
573 limiting =
"chug" if (
not ac_gated
or chug_margin <= acoustic_margin)
else ac.get(
"limiting_mode")
574 diag =
_diagnostics(state, chug_margin, acoustic_margin, gate_threshold, limiting,
575 chug_rich, ac, vap, fallbacks, acoustic_gate=ac_gate_mode)
578 "summary": {
"state": state,
"min_margin": min_margin,
579 "gate_margin_threshold": gate_threshold,
"limiting_mode": limiting,
580 "margin_basis":
"chug Nyquist gain margin at the low end of the mixing-lag band"
581 + (
"" if not ac_gated
else f
"; acoustic damping/driving ({ac_gate_mode})")},
584 "alpha": chug_rich.get(
"alpha"),
"freq_hz": chug_rich.get(
"f_chug_hz"),
585 "zeta": chug_rich.get(
"zeta"),
"margin": chug_margin,
586 "gain_margin_nominal": gm_nominal,
587 "gain_margin_db": analysis.gain_margin_db(chug_margin),
588 "gain_margin_nominal_db": analysis.gain_margin_db(gm_nominal),
590 "phase_margin_deg": chug_fast.get(
"phase_margin_deg"),
591 "theta_c_s": float(theta_c),
592 "regulator_status": chug_rich.get(
"regulator_status"),
593 "alpha_no_reg": chug_rich.get(
"alpha_no_reg"),
"driver": chug_rich.get(
"driver"),
594 "boundary_curve": boundary,
595 "boundary_basis":
"every stream's drop and lag scaled together from the design; x = "
596 "mass-weighted ΔP_inj/Pc, y = mass-weighted τ/θ_c; nominal mixing lag",
597 "design_point": design_point,
598 "eta_mean": float(chug.mean_eta(streams)),
600 "design_streams": design_streams,
602 "locus_param":
"eta_inj_mean (every stream's drop scaled by one factor)",
603 "eta_window": [float(eta_window[0]), float(eta_window[1])],
604 "eta_critical": eta_critical,
605 "lag_model": inp.get(
"lag_model"),
606 "convection_model": inp.get(
"convection_model"),
607 "lag_breakdown": lag_break,
609 "acoustic": {
"margin": acoustic_margin,
"margin_model": acoustic_margin_model,
610 "gate_status": ac_gate_mode,
"modes": acoustic_modes,
611 "any_unstable": ac[
"any_unstable"],
"limiting_mode": ac[
"limiting_mode"],
612 "chamber_length_m": float(inp[
"L_ch"]),
"chamber_diameter_m": float(inp[
"D_ch"]),
613 "sound_speed_m_s": float(gas.a_sound)},
618 "n": inp[
"n_interaction"],
"chi_acoustic": inp[
"chi_acoustic"],
619 "dP_reg_max_psi": float(streams[0].regulator.max_excursion_pa / _PA_PER_PSI),
620 "eta_inj_O": inp[
"eta_inj_O"],
"eta_inj_F": inp[
"eta_inj_F"],
621 "smd_O_um": float(inp[
"D32_O"] * 1e6),
622 "smd_F_um": float(inp[
"D32_F"] * 1e6),
623 "rate_limiting_stream": inp.get(
"rate_limiting_stream"),
624 "mach_nozzle_entrance": float(inp[
"mach_nozzle_entrance"]),
625 "contraction_ratio": float(inp[
"contraction_ratio"]),
626 "feed_length_O_m": float(inp[
"feed_length_O"]),
"feed_length_F_m": float(inp[
"feed_length_F"]),
627 "acoustic_gate": ac_gate_mode,
628 "acoustic_overlap": {m[
"mode"]: m[
"overlap"]
for m
in ac[
"modes"]},
629 "damping_injector_frac": float(inp[
"damping_coeffs"].injector_frac),
630 "damping_twophase_frac": float(inp[
"damping_coeffs"].twophase_frac),
633 "time_lag_model": inp.get(
"lag_model"),
634 "convection_model": inp.get(
"convection_model"),
635 "mixing_lag_fraction": inp.get(
"mixing_lag_fraction"),
636 "mixing_lag_fraction_band": list(inp[
"chug_band"])
if inp.get(
"chug_band")
else None,
637 "injector_type": inp.get(
"injector_type"),
638 "fluid_O": inp.get(
"fluid_name_O"),
"fluid_F": inp.get(
"fluid_name_F"),
639 "phase_O": inp.get(
"phase_O"),
"phase_F": inp.get(
"phase_F"),
640 "tau_conv_O_s": float(inp[
"tau_conv_O"]),
"tau_conv_F_s": float(inp[
"tau_conv_F"]),
641 "lag_breakdown": lag_break,
644 "fallbacks_used": fallbacks,
Dict[str, Any] build_rich_report(config, float Pc, float MR, float mdot_total, float cstar, float gamma, float R, float Tc, Dict[str, Any] diagnostics, Any cg, *, Optional[float] gate_threshold=None, Optional[Dict[str, float]] overrides=None)
Dict[str, Any] _diagnostics(str state, float chug_margin, float acoustic_margin, float gate_threshold, Optional[str] limiting, Dict[str, Any] chug_rich, Dict[str, Any] ac, Dict[str, Any] vap, List[Dict[str, Any]] fallbacks, str acoustic_gate="report_only")
Dict[str, Any] _build_rich_report(config, float Pc, float MR, float mdot_total, float cstar, float gamma, float R, float Tc, Dict[str, Any] diagnostics, Any cg, *, float gate_threshold, Optional[Dict[str, float]] overrides, Dict[str, Any] used_here)