STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
stability.py
Go to the documentation of this file.
1"""Numba kernel for the chug gain/phase-margin scan.
2
3This is the dominant per-evaluation stability cost: a 200-point complex frequency
4sweep run on every candidate. Vectorising the pure-Python version in numpy took it
5from 1537 us to 112 us (and that speed-up stands on its own -- it is what every
6pintle and coaxial config runs today, since no accelerated injector path covers
7them). But an end-to-end Layer-1 measurement still put the remaining gap at ~8.8%
8of wall time versus the C kernel, above the 3% we were willing to absorb, so the
9scan itself is compiled here.
10
11Mirrors engine/pipeline/stability/chug.py exactly, including numpy's unwrap
12semantics, which numba does not provide.
13"""
14from __future__ import annotations
15
16import numpy as np
17from numba import njit
18
19_TWO_PI = 2.0 * np.pi
20
21
22@njit(cache=True)
23def _unwrap(p):
24 """np.unwrap(p) for a 1-D float64 array (numba has no np.unwrap).
25
26 Faithful to numpy: correct by the modulo-wrapped difference, zero the
27 correction where the raw step is below the discontinuity threshold, and
28 resolve the -pi boundary toward +pi when the raw step is positive.
29 """
30 n = p.shape[0]
31 out = np.empty(n)
32 if n == 0:
33 return out
34 out[0] = p[0]
35 run = 0.0
36 for i in range(1, n):
37 dd = p[i] - p[i - 1]
38 ddmod = (dd + np.pi) % _TWO_PI - np.pi
39 if ddmod == -np.pi and dd > 0.0:
40 ddmod = np.pi
41 corr = ddmod - dd
42 if abs(dd) < np.pi: # below discont -> no correction
43 corr = 0.0
44 run += corr
45 out[i] = p[i] + run
46 return out
47
48
49@njit(cache=True)
50def chug_margin_kernel(omega, tau, inert, res, invG, Zhf, wc, K_c, theta_c):
51 """Returns (gain_margin, f_chug_hz, phase_margin_deg, stable, crossings_omega).
52
53 Per-stream inputs are the s-independent primitives already resolved by the
54 Python wrapper: transport lag, feed inertance, linearised resistance, 1/G_inj
55 (inf when G<=0), and the regulator high-frequency impedance and corner.
56
57 Mirrors chug.chug_margin_fast: the gain margin is 1/max|L| over EVERY crossing of
58 the negative real axis (Im L changes sign with interpolated Re L < 0 -- phases
59 -pi, -3pi, ...), not only the unwrapped -pi one; the chug frequency is the worst
60 crossing's; the phase margin is the wrapped distance to the negative axis at the
61 gain crossover nearest to it.
62 """
63 n = omega.shape[0]
64 ns = tau.shape[0]
65 mag = np.empty(n)
66 ang = np.empty(n)
67 Lre = np.empty(n)
68 Lim = np.empty(n)
69
70 for i in range(n):
71 s = complex(0.0, omega[i])
72 acc = complex(0.0, 0.0)
73 for k in range(ns):
74 Zr = complex(0.0, 0.0)
75 if Zhf[k] > 0.0:
76 Zr = Zhf[k] * (s / wc[k]) / (1.0 + s / wc[k])
77 Zf = Zr + inert[k] * s + res[k] + invG[k]
78 if Zf == 0.0:
79 continue # scalar path skips a zero-impedance stream
80 acc += np.exp(-s * tau[k]) / Zf
81 L = K_c / (theta_c * s + 1.0) * acc
82 Lre[i] = L.real
83 Lim[i] = L.imag
84 mag[i] = abs(L)
85 ang[i] = np.arctan2(L.imag, L.real)
86
87 phase = _unwrap(ang)
88
89 # --- negative-real-axis crossings (chug._negative_axis_crossings) ---
90 w_all = np.empty(max(n - 1, 0))
91 nc = 0
92 gm_best = np.inf
93 f_pc = np.nan
94 mag_worst = -1.0
95 for i in range(n - 1):
96 i0 = Lim[i]
97 i1 = Lim[i + 1]
98 if i0 == 0.0 or i0 * i1 < 0.0:
99 di = i0 - i1
100 frac = i0 / di if di != 0.0 else 0.0
101 re_c = Lre[i] + frac * (Lre[i + 1] - Lre[i])
102 if re_c < 0.0:
103 w_c = omega[i] + frac * (omega[i + 1] - omega[i])
104 w_all[nc] = w_c
105 nc += 1
106 mag_c = -re_c
107 if mag_c > mag_worst: # argmax: first on a tie
108 mag_worst = mag_c
109 gm_best = 1.0 / mag_c if mag_c > 0 else np.inf
110 f_pc = w_c / _TWO_PI
111
112 # --- gain crossovers (|L| = 1): wrapped distance to the negative axis, smallest wins ---
113 pm_deg = np.nan
114 best_abs = np.inf
115 for i in range(n - 1):
116 h0 = mag[i] - 1.0
117 h1 = mag[i + 1] - 1.0
118 if h0 == 0.0 or h0 * h1 < 0.0:
119 dh = h0 - h1
120 frac = h0 / dh if dh != 0.0 else 0.0
121 ph_c = phase[i] + frac * (phase[i + 1] - phase[i])
122 dist = (ph_c + np.pi) % _TWO_PI
123 if dist > np.pi:
124 dist = dist - _TWO_PI
125 if abs(dist) < best_abs: # argmin: first on a tie
126 best_abs = abs(dist)
127 pm_deg = np.degrees(dist)
128
129 if not np.isfinite(gm_best):
130 # No negative-axis crossing in band: 1/max|L|, floored at 1 when |L| < 1 throughout.
131 mmax = mag.max()
132 gm_best = 1.0 / (mmax if mmax > 1e-12 else 1e-12)
133 if mmax < 1.0 and gm_best < 1.0:
134 gm_best = 1.0
135
136 return gm_best, f_pc, pm_deg, gm_best > 1.0, w_all[:nc].copy()
137
138
139def chug_margin_fast(streams, chamber, *, with_regulator: bool = True,
140 f_lo: float = 2.0, f_hi: float = 2000.0):
141 """Drop-in for chug.chug_margin_fast / native_injector.chug_margin_fast.
142
143 Resolves the s-independent per-stream primitives once here, in Python, then
144 hands the kernel nothing but arrays and floats.
145 """
146 from engine.pipeline.stability.chug import _freq_grid
147
148 omega = np.ascontiguousarray(_freq_grid(f_lo, f_hi), dtype=np.float64)
149 ns = len(streams)
150 tau = np.empty(ns); inert = np.empty(ns); res = np.empty(ns)
151 invG = np.empty(ns); Zhf = np.zeros(ns); wc = np.ones(ns)
152 for k, st in enumerate(streams):
153 G = st.G_inj()
154 tau[k] = st.tau_conv
155 inert[k] = st.inertance()
156 res[k] = st.resistance()
157 invG[k] = (1.0 / G) if G > 0 else np.inf
158 if with_regulator and st.regulator.enabled and st.regulator.Z_hf > 0.0:
159 Zhf[k] = float(st.regulator.Z_hf)
160 wc[k] = 2.0 * np.pi * max(st.regulator.corner_hz, 1e-6)
161
162 gm, f_pc, pm, stable, w_c = chug_margin_kernel(
163 omega, tau, inert, res, invG, Zhf, wc,
164 float(chamber.K_c()), float(chamber.theta_c()))
165 return {
166 "gain_margin": float(gm),
167 "stable": bool(stable),
168 "f_chug_hz": float(f_pc),
169 "crossings_hz": [float(w / (2.0 * np.pi)) for w in w_c],
170 "phase_margin_deg": float(pm),
171 "margin": float(gm),
172 }
#define max(a, b)
Definition Arduino.h:43
chug_margin_kernel(omega, tau, inert, res, invG, Zhf, wc, K_c, theta_c)
Definition stability.py:50
chug_margin_fast(streams, chamber, *, bool with_regulator=True, float f_lo=2.0, float f_hi=2000.0)
Definition stability.py:140