STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
engine.pipeline.stability.chug Namespace Reference

Classes

class  Regulator
class  ChugStream
class  ChugChamber

Functions

complex chug_open_loop (complex s, List[ChugStream] streams, ChugChamber chamber, *, bool with_regulator=True)
np.ndarray _open_loop_grid (np.ndarray omega, List[ChugStream] streams, ChugChamber chamber, *, bool with_regulator=True)
complex chug_characteristic (complex s, List[ChugStream] streams, ChugChamber chamber, *, bool with_regulator=True)
np.ndarray _freq_grid_cached (float f_lo, float f_hi, int n)
np.ndarray _freq_grid (float f_lo=2.0, float f_hi=2000.0, int n=200)
Tuple[np.ndarray, np.ndarray] _negative_axis_crossings (np.ndarray omega, np.ndarray L)
Dict[str, float] chug_margin_fast (List[ChugStream] streams, ChugChamber chamber, *, bool with_regulator=True, float f_lo=2.0, float f_hi=2000.0)
Tuple[float, float, float] _solve_dominant_root (List[ChugStream] streams, ChugChamber chamber, *, bool with_regulator, float omega_seed)
Tuple[float, float, float] _dominant_root (List[ChugStream] streams, ChugChamber chamber, Dict[str, float] fast_res, *, bool with_regulator=True)
str _dominant_driver (List[ChugStream] streams, complex s)
float damping_ratio (float sigma, float omega)
Tuple[float, float, float] _solve_root_near (List[ChugStream] streams, ChugChamber chamber, complex s0, *, bool with_regulator=True)
float mean_eta (List[ChugStream] streams)
List[Dict[str, float]] chug_root_locus (List[ChugStream] streams, ChugChamber chamber, *, Optional[np.ndarray] eta_values=None, bool with_regulator=True, bool scale_design=False)
Dict[str, float] chug_growth_rate (List[ChugStream] streams, ChugChamber chamber, *, bool with_regulator=True)

Detailed Description

Low-frequency (chug) stability: lumped feed <-> chamber <-> combustion model.

Implements the impedance-form characteristic equation [Phys §3.2, eq 3.3]:

    F(s) = 1 + Y_ch(s) * sum_k  exp(-s*tau_conv_k) / Z_feed_k(s)  = 0

    Y_ch(s)   = K_c / (theta_c*s + 1)          chamber transfer function   (K_c = c*/A_t)
    Z_feed_k  = Z_reg_k(s) + I_k*s + R_k + 1/G_inj_k   series feed+injector+regulator impedance

All quantities use the **mass-flow** through-variable convention (impedances in Pa·s/kg). Pure numeric
module — params come in as dataclasses; config extraction lives in the integration layer (analysis.py).

Two tiers (plan A2):
  * ``chug_margin_fast``  — gain/phase-margin proxy from a 1-D frequency scan (no transcendental
    root-find). For the Layer-1 inner loop. Calibrated to the **sign** of alpha.
  * ``chug_growth_rate``  — complex root-find of (3.3) for the dominant (alpha, omega). For the report.

The regulator enters in **two separate roles** [Phys §3.2 / §6.1]:
  * impedance ``Z_reg(s)`` inside ``Z_feed`` — shifts the poles / alpha (homogeneous stability);
  * forcing bound ``max_excursion_pa`` — disturbance amplitude (reported separately, NOT a pole-shifter).

Function Documentation

◆ _dominant_driver()

str engine.pipeline.stability.chug._dominant_driver ( List[ChugStream] streams,
complex s )
protected
Heuristic: which feed term dominates |Z_feed| at the root -> the limiting physics.

Definition at line 324 of file chug.py.

References max.

Referenced by chug_growth_rate().

◆ _dominant_root()

Tuple[float, float, float] engine.pipeline.stability.chug._dominant_root ( List[ChugStream] streams,
ChugChamber chamber,
Dict[str, float] fast_res,
* ,
bool with_regulator = True )
protected
The most unstable root of F(s) = 0: seeded at every negative-real-axis crossing of L(iw)
(``fast_res``, from ``chug_margin_fast``), the largest alpha kept. (alpha, omega, |F|).

Definition at line 308 of file chug.py.

References _solve_dominant_root().

Referenced by chug_growth_rate(), and chug_root_locus().

◆ _freq_grid()

np.ndarray engine.pipeline.stability.chug._freq_grid ( float f_lo = 2.0,
float f_hi = 2000.0,
int n = 200 )
protected

Definition at line 187 of file chug.py.

References _freq_grid_cached().

Referenced by chug_margin_fast().

◆ _freq_grid_cached()

np.ndarray engine.pipeline.stability.chug._freq_grid_cached ( float f_lo,
float f_hi,
int n )
protected

Definition at line 181 of file chug.py.

Referenced by _freq_grid().

◆ _negative_axis_crossings()

Tuple[np.ndarray, np.ndarray] engine.pipeline.stability.chug._negative_axis_crossings ( np.ndarray omega,
np.ndarray L )
protected
(omega_c, |L|_c) where L(iw) crosses the negative real axis: Im L changes sign with Re L < 0.

Every such crossing is a phase of -pi - 2*pi*k. Testing the unwrapped phase against -pi alone
(the old rule) misses k >= 1, where a slow second stream can put the loop's largest gain.

Definition at line 195 of file chug.py.

Referenced by chug_margin_fast().

◆ _open_loop_grid()

np.ndarray engine.pipeline.stability.chug._open_loop_grid ( np.ndarray omega,
List[ChugStream] streams,
ChugChamber chamber,
* ,
bool with_regulator = True )
protected
Vectorised L(iw) over a whole frequency grid.

Same computation as calling chug_open_loop once per point: every operation in
Z_feed and Y_ch is element-wise in s, and the per-stream primitives (G_inj,
inertance, resistance, regulator corner) do not depend on s at all. The
per-point version recomputed all of them at each of the 200 grid points, which
made this the dominant per-eval stability cost.

Term order is kept identical to Z_feed/chug_open_loop deliberately, since
floating-point addition is not associative. Agreement is ~1 ULP rather than
bit-for-bit: numpy's complex exp takes a different code path on arrays than on
scalars, which is far below any tolerance here but is not exactly zero.

Definition at line 138 of file chug.py.

References max.

Referenced by chug_margin_fast().

◆ _solve_dominant_root()

Tuple[float, float, float] engine.pipeline.stability.chug._solve_dominant_root ( List[ChugStream] streams,
ChugChamber chamber,
* ,
bool with_regulator,
float omega_seed )
protected
Newton/fsolve for the dominant complex root of F(s)=0 near omega_seed. Returns (alpha, omega, |F|).

Definition at line 277 of file chug.py.

References chug_characteristic().

Referenced by _dominant_root().

◆ _solve_root_near()

Tuple[float, float, float] engine.pipeline.stability.chug._solve_root_near ( List[ChugStream] streams,
ChugChamber chamber,
complex s0,
* ,
bool with_regulator = True )
protected
Single-seed Newton solve for the root of F(s)=0 nearest ``s0``. (alpha, omega, |F|).

The continuation step of the root locus: each point seeds from its predecessor, so one fsolve
per point is enough. ``_solve_dominant_root`` fans out over nine seeds because it has no
predecessor to start from; doing that per locus point would cost ~40x for no extra accuracy.

Definition at line 351 of file chug.py.

References chug_characteristic().

Referenced by chug_root_locus().

◆ chug_characteristic()

complex engine.pipeline.stability.chug.chug_characteristic ( complex s,
List[ChugStream] streams,
ChugChamber chamber,
* ,
bool with_regulator = True )
F(s) = 1 + L(s).  Roots s = alpha + i*omega give chug frequency/growth rate.

Definition at line 170 of file chug.py.

References chug_open_loop().

Referenced by _solve_dominant_root(), and _solve_root_near().

◆ chug_growth_rate()

Dict[str, float] engine.pipeline.stability.chug.chug_growth_rate ( List[ChugStream] streams,
ChugChamber chamber,
* ,
bool with_regulator = True )
Rich chug analysis: dominant growth rate alpha and frequency from root-find of (3.3).

The root-find is seeded at EVERY negative-real-axis crossing of L(iw) and keeps the root with
the largest alpha; seeding only at the -pi crossing found a stable root while a -3pi one grew.

Returns dict: ``alpha`` [1/s], ``f_chug_hz``, ``zeta`` (= -alpha/|s|), ``margin`` (= 1+zeta),
``stable``, ``regulator_status``, ``alpha_no_reg`` (only when a regulator is modelled; None
otherwise), ``driver``, ``residual``.

Definition at line 464 of file chug.py.

References _dominant_driver(), _dominant_root(), chug_margin_fast(), and damping_ratio().

◆ chug_margin_fast()

Dict[str, float] engine.pipeline.stability.chug.chug_margin_fast ( List[ChugStream] streams,
ChugChamber chamber,
* ,
bool with_regulator = True,
float f_lo = 2.0,
float f_hi = 2000.0 )
Fast chug margin via the Nyquist gain margin of L(iw). No root-find.  [Phys §3.2 fast form]

Char. eq 1+L=0 => instability when L(iw) encircles -1. L is open-loop stable (chamber and feed
poles are real and negative; delays add none), so the loop is stable when L stays inside the
unit circle wherever it crosses the negative real axis: GM = 1 / max |L| over ALL those
crossings (phase -pi, -3pi, ...), and GM < 1 means encirclement.

Returns dict: ``gain_margin`` (>1 stable), ``stable`` (bool), ``f_chug_hz`` (frequency of the
worst crossing, the chug-frequency estimate), ``crossings_hz`` (every negative-axis crossing,
for seeding the root-find), ``phase_margin_deg``, ``margin`` (= gain_margin).

Definition at line 215 of file chug.py.

References _freq_grid(), _negative_axis_crossings(), _open_loop_grid(), and max.

Referenced by chug_growth_rate(), and chug_root_locus().

◆ chug_open_loop()

complex engine.pipeline.stability.chug.chug_open_loop ( complex s,
List[ChugStream] streams,
ChugChamber chamber,
* ,
bool with_regulator = True )
Open-loop transfer L(s) = Y_ch(s) * sum_k exp(-s*tau_k)/Z_feed_k(s). Char. eq is 1 + L(s) = 0.

Definition at line 126 of file chug.py.

Referenced by chug_characteristic().

◆ chug_root_locus()

List[Dict[str, float]] engine.pipeline.stability.chug.chug_root_locus ( List[ChugStream] streams,
ChugChamber chamber,
* ,
Optional[np.ndarray] eta_values = None,
bool with_regulator = True,
bool scale_design = False )
Track the dominant chug pole through the s-plane as injector stiffness sweeps.

This is a root locus in the textbook sense: ``eta_inj = dP_inj/Pc`` is the swept gain, and each
returned point is the eigenvalue ``s = sigma + j*omega`` of the closed-loop characteristic
equation ``1 + L(s) = 0`` at that gain. The imaginary axis is the stability boundary — the
branch crosses it where the loop goes neutrally stable, and the crossing frequency is the chug
frequency the engine would ring at.

Solved by continuation from the softest injector upward, each point seeded on its predecessor,
which is the standard way to follow a branch rather than re-discover it. Honest caveat: on every
case tried so far (lags 0.8-9 ms, feed runs 0.08-1.5 m, eta from 0.02 to 1.2) a single fixed seed
found the same branch, so the continuation is insurance against branch-hopping rather than a
demonstrated fix for it. ``test_locus_is_continuous_in_frequency`` checks the OUTPUT is a branch;
it does not, and cannot currently, distinguish the two seeding strategies.

``scale_design``: each value is the streams' mass-weighted mean stiffness, reached by scaling
every stream's OWN drop by one factor, so the design (unequal drops and all) sits exactly on
the branch at its own mean. Off, every stream is set to the same eta.

Returns points in ascending ``eta`` with keys ``eta``, ``real``, ``imag``, ``f_hz``, ``zeta``.
Points where the branch could not be followed are dropped, so the caller gets a clean polyline.

Definition at line 385 of file chug.py.

References _dominant_root(), _solve_root_near(), chug_margin_fast(), damping_ratio(), and mean_eta().

◆ damping_ratio()

float engine.pipeline.stability.chug.damping_ratio ( float sigma,
float omega )
Damping ratio of a complex pole ``s = sigma + j*omega``: ``zeta = -sigma / |s|``.

This is the standard definition — the cosine of the pole's angle from the negative real axis —
and it is what a constant-zeta ray on a root locus means. (An earlier version of this module
reported ``-sigma/omega``, which is ``zeta/sqrt(1-zeta^2)``: indistinguishable below zeta ~ 0.1
and 15 % off by zeta = 0.5, so a pole plotted against its own reported zeta did not sit on the
ray. Fixed here so the diagram and the number agree.)

Definition at line 336 of file chug.py.

Referenced by chug_growth_rate(), and chug_root_locus().

◆ mean_eta()

float engine.pipeline.stability.chug.mean_eta ( List[ChugStream] streams)
Mass-flow-weighted injector stiffness of the streams.

Definition at line 379 of file chug.py.

References max.

Referenced by chug_root_locus().