STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
acoustic.py
Go to the documentation of this file.
1"""High-frequency (acoustic / "screech") stability: per-mode growth rate.
2
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):
6
7 alpha_mode = alpha_drive - alpha_damp
8
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]
11
12 alpha_damp = alpha_nozzle + alpha_viscous + alpha_injector + alpha_twophase (itemized budget)
13
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.
19
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.
24
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.
30
31Pure numeric module — params in as scalars/dataclasses; integration (config -> params) lives in analysis.py.
32"""
33
34from __future__ import annotations
35
36from dataclasses import dataclass
37from typing import Dict, List, Optional
38import numpy as np
39
40from engine.pipeline.stability import core
41
42
43# Default mode-shape / heat-release overlap per mode [-], in [0,1]. The flame sits near the injector
44# face; longitudinal modes have a pressure antinode there (high overlap), transverse modes are
45# distributed across the face (moderate). ASSUMED, uncalibrated -- no source; override when better
46# data exists.
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,
50}
51
52
53@dataclass
55 """Chamber gas + nozzle conditions needed for the damping budget."""
56 gamma: float
57 a_sound: float # m/s
58 nu_g: float # m^2/s, gas kinematic viscosity (mu/rho) in the chamber
59 mach_nozzle_entrance: float = 0.2 # mean Mach at nozzle entrance (sets nozzle damping)
60 prandtl: Optional[float] = None # hot-gas Pr for the Kirchhoff thermal layer; None = viscous only
61
62
63@dataclass
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 # alpha_inj ~ injector_frac * pi * f
68 twophase_frac: float = 0.03 # alpha_2phi ~ twophase_frac * pi * f * droplet_loading
69 droplet_loading: float = 1.0 # O(1) relative liquid loading near the front end
70
71
72def _is_longitudinal(mode_name: Optional[str]) -> bool:
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())
75
76
77# ---------------------------------------------------------------------------
78# Damping budget
79# ---------------------------------------------------------------------------
80
81def damping_budget(f_hz: float, D_ch: float, L_ch: float, gas: GasState,
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]
85
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).
88 """
89 coeffs = coeffs or DampingCoeffs()
90 w_end = 1.0 if _is_longitudinal(mode_name) else 0.5
91 a_noz = core.nozzle_damping_rate(gas.a_sound, L_ch, gas.mach_nozzle_entrance, gas.gamma,
92 end_weight=w_end)
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)}
99
100
101# ---------------------------------------------------------------------------
102# Per-mode growth rate
103# ---------------------------------------------------------------------------
104
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)).
107
108 Rayleigh: the part of the n-tau response in phase with p', ``Re[core.n_tau_gain]``. >= 0.
109 """
110 omega = 2.0 * np.pi * f_hz
111 drive_phase = core.n_tau_gain(omega, n, tau_sens).real # = n*(1 - cos(omega*tau_sens))
112 return float(0.5 * omega * (gamma - 1.0) * overlap * drive_phase)
113
114
115def mode_driving_rate_max(f_hz: float, gamma: float, overlap: float, n: float) -> float:
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)
118
119
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")
123
124
125def mode_growth_rate(f_hz: float, mode_name: str, D_ch: float, L_ch: float, gas: GasState,
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.
130
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.
135 """
136 ov = overlap if overlap is not None else DEFAULT_OVERLAP.get(mode_name, 0.3)
137 drive = mode_driving_rate(f_hz, gas.gamma, ov, n, tau_sens)
138 drive_max = mode_driving_rate_max(f_hz, gas.gamma, ov, n)
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)
142 return {
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"),
150 }
151
152
153# ---------------------------------------------------------------------------
154# Full analysis (rich) and fast subset
155# ---------------------------------------------------------------------------
156
157def _mode_frequencies(gas: GasState, L_ch: float, D_ch: float, n_long: int) -> Dict[str, float]:
158 freqs: Dict[str, float] = {}
159 for i, f in enumerate(core.longitudinal_mode_frequencies(gas.a_sound, L_ch, n_long), start=1):
160 freqs[f"{i}L"] = f
161 freqs.update(core.transverse_mode_frequencies(gas.a_sound, D_ch))
162 return freqs
163
164
165def analyze_acoustic_modes(D_ch: float, L_ch: float, gas: GasState,
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...).
170
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)]
173 """
174 freqs = _mode_frequencies(gas, L_ch, D_ch, n_long)
175 modes: List[Dict[str, float]] = []
176 for name, f in freqs.items():
177 if f <= 0:
178 continue
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
182 return {
183 "modes": modes,
184 "limiting_mode": limiting,
185 "any_unstable": bool(any(m["alpha"] > 0.0 for m in modes)),
186 }
187
188
189def fast_acoustic(D_ch: float, L_ch: float, gas: GasState,
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).
193
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
196 that sets it.
197 """
198 a = gas.a_sound
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)
202 cand = []
203 if f_1L > 0:
204 cand.append(mode_growth_rate(f_1L, "1L", D_ch, L_ch, gas, n=n, tau_sens=tau_sens, coeffs=coeffs))
205 if f_1T > 0:
206 cand.append(mode_growth_rate(f_1T, "1T", D_ch, L_ch, gas, n=n, tau_sens=tau_sens, coeffs=coeffs))
207 if not cand:
208 nan = float("nan")
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"])
214 return {
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"],
219 }
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
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)
Definition acoustic.py:128
float mode_driving_rate_max(float f_hz, float gamma, float overlap, float n)
Definition acoustic.py:115
float mode_driving_rate(float f_hz, float gamma, float overlap, float n, float tau_sens)
Definition acoustic.py:105
Dict[str, float] fast_acoustic(float D_ch, float L_ch, GasState gas, *, float n, float tau_sens, Optional[DampingCoeffs] coeffs=None)
Definition acoustic.py:191
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)
Definition acoustic.py:168
bool _is_longitudinal(Optional[str] mode_name)
Definition acoustic.py:72
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)
Definition acoustic.py:83
float _ratio(float num, float den)
Definition acoustic.py:120
Dict[str, float] _mode_frequencies(GasState gas, float L_ch, float D_ch, int n_long)
Definition acoustic.py:157