1"""High-frequency (acoustic / "screech") stability: per-mode growth rate.
3Implements the modal growth-rate balance [Phys §4.2, eq 4.1] as a **lumped, normalized** design-guidance
4model (we do not have spatial mode shapes / heat-release fields in Layer 1, so the mode-shape/heat-
5release overlap is a documented per-mode factor, not an integral):
7 alpha_mode = alpha_drive - alpha_damp
9 alpha_drive = (omega/2) * (gamma-1) * overlap * Re[ n*(1 - e^{-i*omega*tau_sens}) ]
10 = (omega/2) * (gamma-1) * overlap * n * (1 - cos(omega * tau_sens)) [Phys §2, §4.2]
12 alpha_damp = alpha_nozzle + alpha_viscous + alpha_injector + alpha_twophase (itemized budget)
14A mode **grows** when alpha_mode > 0. The driving is the part of the n-tau response IN PHASE with p'
15(Rayleigh): n(1 - cos omega*tau) >= 0, largest at omega*tau = pi and zero at 2*pi -- Crocco's
16stability bucket (Harrje & Reardon SP-194 ch. 4). The quadrature part n*sin(omega*tau) shifts the
17frequency and does no work; this module used to take it, which booked combustion as DAMPING over
18half of every period of omega*tau.
20Two margins per mode, both 1 at neutral stability:
21 * ``margin`` = damping / driving at the model's tau_sens (nominal phase);
22 * ``margin_worst_phase`` = damping / driving at omega*tau = pi = n_min / n, Crocco's interaction-
23 index margin, independent of tau.
25Why the verdict is report-only by default (analysis.py): omega*tau_sens is tens of radians (1T ~ 29
26rad on the 6500 N) and tau_sens = chi*tau_vap with chi uncalibrated, so the nominal-phase margin
27flips with a few percent of tau; the worst-phase margin is set by gamma, n, the overlap and the
28injector/two-phase fractions, none of which the design moves. Harrje & Reardon rate HF stability by
29test (bomb / pulse, >= 25 kHz Pc), and so does this tool until T7/H3 calibrate the damping.
31Pure numeric module — params in as scalars/dataclasses; integration (config -> params) lives in analysis.py.
34from __future__
import annotations
36from dataclasses
import dataclass
37from typing
import Dict, List, Optional
47DEFAULT_OVERLAP: Dict[str, float] = {
48 "1L": 0.70,
"2L": 0.45,
"3L": 0.30,
49 "1T": 0.40,
"2T": 0.30,
"1R": 0.35,
"3T": 0.25,
"1T1R": 0.25,
55 """Chamber gas + nozzle conditions needed for the damping budget."""
59 mach_nozzle_entrance: float = 0.2
60 prandtl: Optional[float] =
None
65 """Injector-face and two-phase damping, as fractions of pi*f. ASSUMED, uncalibrated: no closed
66 form exists; calibrate against a cold ring-down (T7) and hot-fire pulse test (H3)."""
67 injector_frac: float = 0.02
68 twophase_frac: float = 0.03
69 droplet_loading: float = 1.0
73 """Pure axial mode ("1L", "2L", ...). Anything else is transverse or mixed."""
74 return mode_name
is None or (mode_name.endswith(
"L")
and mode_name[:-1].isdigit())
82 coeffs: Optional[DampingCoeffs] =
None,
83 mode_name: Optional[str] =
None) -> Dict[str, float]:
84 """Itemized acoustic damping rates [1/s] for a mode at frequency ``f_hz``. [Phys §4.2 — viz #2]
86 Keys: ``nozzle``, ``viscous``, ``injector``, ``twophase``, ``total``. All >= 0 (damping).
87 ``mode_name`` sets the nozzle-plane weight (longitudinal 1, transverse 1/2; None = longitudinal).
91 a_noz = core.nozzle_damping_rate(gas.a_sound, L_ch, gas.mach_nozzle_entrance, gas.gamma,
93 a_vis = core.viscous_damping_rate(f_hz, D_ch, gas.nu_g, gas.gamma, gas.prandtl)
94 a_inj = coeffs.injector_frac * np.pi * f_hz
95 a_2ph = coeffs.twophase_frac * np.pi * f_hz * coeffs.droplet_loading
96 total = a_noz + a_vis + a_inj + a_2ph
97 return {
"nozzle": float(a_noz),
"viscous": float(a_vis),
"injector": float(a_inj),
98 "twophase": float(a_2ph),
"total": float(total)}
105def mode_driving_rate(f_hz: float, gamma: float, overlap: float, n: float, tau_sens: float) -> float:
106 """Combustion driving rate [1/s]: (omega/2)(gamma-1)*overlap*n*(1 - cos(omega*tau_sens)).
108 Rayleigh: the part of the n-tau response in phase with p', ``Re[core.n_tau_gain]``. >= 0.
110 omega = 2.0 * np.pi * f_hz
111 drive_phase = core.n_tau_gain(omega, n, tau_sens).real
112 return float(0.5 * omega * (gamma - 1.0) * overlap * drive_phase)
116 """Largest driving any lag can give [1/s]: omega*tau = pi, ``omega*(gamma-1)*overlap*n``."""
117 return float(2.0 * np.pi * f_hz * (gamma - 1.0) * overlap * n)
120def _ratio(num: float, den: float) -> float:
121 """damping/driving; no driving -> infinite margin."""
122 return float(num / den)
if den > 0.0
else float(
"inf")
126 *, n: float, tau_sens: float,
127 overlap: Optional[float] =
None,
128 coeffs: Optional[DampingCoeffs] =
None) -> Dict[str, float]:
129 """Net growth rate for one acoustic mode, with its damping budget and margins.
131 ``alpha = driving - damping_total``; **stable if alpha < 0**. [Phys §4.2, eq 4.1]
132 ``margin`` = damping/driving at tau_sens; ``margin_worst_phase`` = damping/driving at
133 omega*tau = pi (= n_min/n). Both > 1 is stable; ``n_min`` is the interaction index at which the
134 mode goes neutral at the worst phase.
136 ov = overlap
if overlap
is not None else DEFAULT_OVERLAP.get(mode_name, 0.3)
139 damp =
damping_budget(f_hz, D_ch, L_ch, gas, coeffs, mode_name=mode_name)
140 alpha = drive - damp[
"total"]
141 m_worst =
_ratio(damp[
"total"], drive_max)
143 "mode": mode_name,
"f_hz": float(f_hz),
"overlap": float(ov),
144 "driving": float(drive),
"driving_max": float(drive_max),
145 "damping": damp,
"damping_total": damp[
"total"],
146 "alpha": float(alpha),
"stable": bool(alpha < 0.0),
147 "margin":
_ratio(damp[
"total"], drive),
148 "margin_worst_phase": m_worst,
149 "n_min": float(n * m_worst)
if np.isfinite(m_worst)
else float(
"inf"),
158 freqs: Dict[str, float] = {}
159 for i, f
in enumerate(core.longitudinal_mode_frequencies(gas.a_sound, L_ch, n_long), start=1):
161 freqs.update(core.transverse_mode_frequencies(gas.a_sound, D_ch))
166 *, n: float, tau_sens: float,
167 coeffs: Optional[DampingCoeffs] =
None,
168 n_long: int = 3) -> Dict[str, object]:
169 """Rich acoustic analysis: growth rate + damping budget for every mode (1L..3L, 1T, 2T, 1R, 3T...).
171 Returns ``{"modes": [per-mode dicts sorted by alpha desc], "limiting_mode": name, "any_unstable": bool}``.
172 [Phys §4.2 — feeds viz #2 (damping bars), #3 (frequency ladder), #4 (phase clock)]
175 modes: List[Dict[str, float]] = []
176 for name, f
in freqs.items():
179 modes.append(
mode_growth_rate(f, name, D_ch, L_ch, gas, n=n, tau_sens=tau_sens, coeffs=coeffs))
180 modes.sort(key=
lambda m: m[
"alpha"], reverse=
True)
181 limiting = modes[0][
"mode"]
if modes
else None
184 "limiting_mode": limiting,
185 "any_unstable": bool(any(m[
"alpha"] > 0.0
for m
in modes)),
190 *, n: float, tau_sens: float,
191 coeffs: Optional[DampingCoeffs] =
None) -> Dict[str, float]:
192 """Fast acoustic check for the Layer-1 loop: only 1L and 1T (the usual first offenders).
194 Returns ``alpha_max``, ``limiting_mode``, ``stable``, ``f_1L``, ``f_1T``, and the smallest
195 ``margin`` (nominal phase) and ``margin_worst_phase`` over the two modes, each with the mode
199 f_1L = core.longitudinal_mode_frequencies(a, L_ch, 1)
200 f_1L = f_1L[0]
if f_1L
else 0.0
201 f_1T = core.transverse_mode_frequencies(a, D_ch).get(
"1T", 0.0)
204 cand.append(
mode_growth_rate(f_1L,
"1L", D_ch, L_ch, gas, n=n, tau_sens=tau_sens, coeffs=coeffs))
206 cand.append(
mode_growth_rate(f_1T,
"1T", D_ch, L_ch, gas, n=n, tau_sens=tau_sens, coeffs=coeffs))
209 return {
"alpha_max": nan,
"limiting_mode":
None,
"stable":
False,
"f_1L": f_1L,
"f_1T": f_1T,
210 "margin": nan,
"margin_mode":
None,
"margin_worst_phase": nan,
"worst_phase_mode":
None}
211 worst =
max(cand, key=
lambda m: m[
"alpha"])
212 m_nom =
min(cand, key=
lambda m: m[
"margin"])
213 m_wp =
min(cand, key=
lambda m: m[
"margin_worst_phase"])
215 "alpha_max": float(worst[
"alpha"]),
"limiting_mode": worst[
"mode"],
216 "stable": bool(worst[
"alpha"] < 0.0),
"f_1L": float(f_1L),
"f_1T": float(f_1T),
217 "margin": float(m_nom[
"margin"]),
"margin_mode": m_nom[
"mode"],
218 "margin_worst_phase": float(m_wp[
"margin_worst_phase"]),
"worst_phase_mode": m_wp[
"mode"],
Dict[str, float] mode_growth_rate(float f_hz, str mode_name, float D_ch, float L_ch, GasState gas, *, float n, float tau_sens, Optional[float] overlap=None, Optional[DampingCoeffs] coeffs=None)
float mode_driving_rate_max(float f_hz, float gamma, float overlap, float n)
float mode_driving_rate(float f_hz, float gamma, float overlap, float n, float tau_sens)
Dict[str, float] fast_acoustic(float D_ch, float L_ch, GasState gas, *, float n, float tau_sens, Optional[DampingCoeffs] coeffs=None)
Dict[str, object] analyze_acoustic_modes(float D_ch, float L_ch, GasState gas, *, float n, float tau_sens, Optional[DampingCoeffs] coeffs=None, int n_long=3)
bool _is_longitudinal(Optional[str] mode_name)
Dict[str, float] damping_budget(float f_hz, float D_ch, float L_ch, GasState gas, Optional[DampingCoeffs] coeffs=None, Optional[str] mode_name=None)
float _ratio(float num, float den)
Dict[str, float] _mode_frequencies(GasState gas, float L_ch, float D_ch, int n_long)