STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
chamber.py
Go to the documentation of this file.
1"""Numba mirror of the Python chamber solve, for the Layer 1 inner loop.
2
3Each function here is a transcription of the Python function named in its docstring, in the same
4order of operations, so the two paths compute the same physics and differ only by rounding and
5by the one tabulation below. tests/test_numba_ab_parity.py diffs them live at 1e-6.
6
7 residual(Pc) = mdot_injector(Pc) - (Pc/kappa) At / (eta_c* c*_ideal) chamber_solver.residual
8 eta_c* = eta_vap eta_mix eta_HL combustion_eff.eta_cstar
9
10The one thing numba cannot call is CoolProp, which the droplet heat-up reads at the chamber
11pressure (combustion_physics.liquid_heatup_properties). It is tabulated here once per fluid as
12functions of ln P -- cp at the 24 Gauss-Legendre nodes the Python integral uses, cp at the upper
13limit, the density at the mean drop temperature, and CoolProp's saturation temperature -- as
14cubic splines between the pressures where the integrand changes form (the critical pressure, where
15the Clausius-Clapeyron surface temperature meets CoolProp's saturation line, where it reaches the
16critical temperature, where the heat-up vanishes). Every interval is checked against CoolProp at
17its midpoint when the table is built; an interval that misses is refused, and a pressure there
18makes the kernel return no solution so the caller runs the Python solve.
19"""
20from __future__ import annotations
21
22import math
23from collections import OrderedDict
24
25import numpy as np
26from numba import njit
27
28from engine.accel.kernels import _clip, _sign, _solve_injector, cea_eval
29from engine.accel.params import _IDX
31 P_BOIL_REF, WATSON_EXPONENT, _HEATUP_GL_W, _HEATUP_GL_X, _MARCH_STEPS, _RR_NODES,
32 _RR_WEIGHTS, _UNIT_MAD, _V_FLOOR, _march_core,
33)
34from engine.pipeline.constants import STEFAN_BOLTZMANN_W_M2_K4 as SIGMA_SB
35from engine.pipeline.physics_constants import PRANDTL_DEFAULT, R_UNIVERSAL_KMOL
37 BARTZ_OMEGA, CYLINDER_BEAM_LENGTH_OVER_D, LECKNER_T_MAX, THROAT_RC_OVER_RT,
38 _LECKNER_CO2, _LECKNER_H2O,
39)
40
41globals().update(_IDX) # injector param indices (P vector) as module constants
42
43G0 = 9.80665
44RR_W = np.ascontiguousarray(_RR_WEIGHTS, np.float64)
45GL_X = np.ascontiguousarray(_HEATUP_GL_X, np.float64)
46GL_W = np.ascontiguousarray(_HEATUP_GL_W, np.float64)
47LK_H2O = np.ascontiguousarray(_LECKNER_H2O, np.float64)
48LK_CO2 = np.ascontiguousarray(_LECKNER_CO2, np.float64)
49N_HEAT = GL_X.size # heat-up table columns: cp at each node, cp(T_hi), rho(T_mean)
50
51# ---- chamber scalar vector layout (the Q in every signature below) ---------------------------
52_QNAMES = [
53 "AT", "AE", "EPS", "EPS_CUR", "LSTAR", "AC", "CR", "ZETA_N",
54 "EFF_MODEL", "EFF_C", "RR_Q", "RR_XSCALE", "BLOW_AS", "EM_OPT", "M_OPT", "EM_CURV", "USE_COUP",
55 "IMP",
56 "SO_OK", "SO_RHO", "SO_TB", "SO_LB", "SO_MW", "SO_TCRIT", "SO_T0", "SO_CP", "SO_HT",
57 "SF_OK", "SF_RHO", "SF_TB", "SF_LB", "SF_MW", "SF_TCRIT", "SF_T0", "SF_CP", "SF_HT",
58 "AB_ON", "AB_TS", "AB_EW", "AB_E", "AB_BELOW", "AB_PHYS", "AB_BC", "AB_BMIN", "AB_BEFF",
59 "AB_RT", "AB_LB",
60 "AX_MR0", "AX_MR1", "AX_NMR", "AX_PC0", "AX_PC1", "AX_NPC", "AX_E0", "AX_E1", "AX_NE",
61]
62_Q = {n: i for i, n in enumerate(_QNAMES)}
63globals().update({f"Q_{n}": i for n, i in _Q.items()})
64NQ = len(_QNAMES)
65_S_STRIDE = _Q["SF_OK"] - _Q["SO_OK"] # stream block: O then F
66
67# ---- result vector layout (evaluate_core) ------------------------------------------------------
68_RNAMES = [
69 "PC", "F", "ISP", "MR", "CSTAR", "GAMMA", "R", "TC_EFF", "TC_IDEAL", "MDOT", "MDOT_O",
70 "MDOT_F", "V_EXIT", "CF", "CF_IDEAL", "P_EXIT", "P_THROAT", "T_EXIT", "T_THROAT", "M_EXIT",
71 "CSTAR_IDEAL", "ETA", "ETA_VAP", "ETA_MIX", "ETA_HL", "KAPPA", "P0", "Q_ABL", "F_VAP",
72 "F_VAP_O", "F_VAP_F", "X0", "U_DROP0", "EM", "RUPE_M", "M_MOL", "CF_VAC",
73]
74_R = {n: i for i, n in enumerate(_RNAMES)}
75globals().update({f"R_{n}": i for n, i in _R.items()})
76NR = len(_RNAMES)
77
78
79# =============================================================================================
80# Liquid heat-up tables (the CoolProp half of combustion_physics.liquid_heatup_properties)
81# =============================================================================================
82_P_LO, _P_HI = 1.0e5, 2.5e7 # the chamber solve's Pc window lies inside (2 atm .. tank)
83_DX_KNOT = 0.004 # knot spacing in ln P
84# Spline tolerances against CoolProp at each interval's midpoint. eta_c* moves 2.4e-3 per unit
85# relative change of the heat-up integral and 3.3e-2 per unit of the liquid density (6.5 kN
86# design), so these hold the table's share of the parity budget below 1e-7.
87_TOL_CP, _TOL_RHO, _TOL_T = 1e-5, 2e-7, 2e-6
88_HEAT_MEMO: dict = {}
89
90
91def _sat_T(P, Tb, Lb, MW, Tcrit):
92 """combustion_physics.saturation_state's surface temperature (Tcrit 0 => none)."""
93 from engine.pipeline.combustion_physics import saturation_state
94 return saturation_state(P, Tb, Lb, MW, Tcrit if Tcrit > 0 else None)[0]
95
96
97def _spline_block(xs, ys):
98 """Per-interval cubic coefficients (highest power first) of not-a-knot splines through ys."""
99 from scipy.interpolate import CubicSpline
100 out = np.empty((ys.shape[0], xs.size - 1, 4))
101 for k in range(ys.shape[0]):
102 out[k] = CubicSpline(xs, ys[k]).c.T
103 return out
104
105
106_REFINE, _MAX_DEPTH = 8, 4 # a failing run is re-tabulated 8x finer, at most 4 times
107
108
109def _tab_range(lo, hi, flag, h_target, fn, nf, tol_rel, tol_abs, depth):
110 """Pieces (lo, hi, h, n, coef, ok, flag) covering [lo, hi]: one not-a-knot spline per run
111 of knots, each interval checked against fn at its midpoint; a run of intervals that misses
112 is tabulated again finer, and still refused after _MAX_DEPTH refinements."""
113 n = max(3, int(math.ceil((hi - lo) / h_target)))
114 xs = np.linspace(lo, hi, n + 1)
115 ys = np.empty((nf, n + 1))
116 good = np.ones(n + 1, dtype=bool)
117 for i, x in enumerate(xs):
118 try:
119 ys[:, i] = fn(x, flag)
120 except Exception:
121 good[i] = False
122 idx = np.flatnonzero(good)
123 if idx.size == 0:
124 return []
125 for i in np.flatnonzero(~good):
126 # a knot CoolProp would not evaluate: carry its neighbour so the spline exists, and
127 # refuse the intervals that touch it
128 ys[:, i] = ys[:, idx[np.argmin(np.abs(idx - i))]]
129 c = _spline_block(xs, ys)
130 h = (hi - lo) / n
131 ok = good[:-1] & good[1:]
132 for i in range(n):
133 if not ok[i]:
134 continue
135 try:
136 want = fn(lo + (i + 0.5) * h, flag)
137 except Exception:
138 ok[i] = False
139 continue
140 dx = 0.5 * h
141 got = ((c[:, i, 0] * dx + c[:, i, 1]) * dx + c[:, i, 2]) * dx + c[:, i, 3]
142 if not np.all(np.isfinite(want)) or np.any(np.abs(got - want) > tol_rel * np.abs(want) + tol_abs):
143 ok[i] = False
144 pieces = []
145 i = 0
146 while i < n:
147 j = i
148 while j + 1 < n and ok[j + 1] == ok[i]:
149 j += 1
150 a, b = lo + i * h, lo + (j + 1) * h
151 if j == n - 1:
152 b = hi
153 if ok[i] or depth >= _MAX_DEPTH:
154 pieces.append((a, b, h, j - i + 1, c[:, i:j + 1, :], ok[i:j + 1].copy(), flag))
155 else:
156 pieces.extend(_tab_range(a, b, flag, h / _REFINE, fn, nf, tol_rel, tol_abs, depth + 1))
157 i = j + 1
158 return pieces
159
160
161def _piecewise(segments, fn, nf, tol_rel, tol_abs):
162 """Tabulate fn(x, flag) -> (nf,) on each (lo, hi, flag) segment. Returns (segs, coef, ok)."""
163 tol_rel = np.broadcast_to(np.asarray(tol_rel, dtype=float), (nf,))
164 pieces = []
165 for lo, hi, flag in segments:
166 pieces.extend(_tab_range(lo, hi, flag, _DX_KNOT, fn, nf, tol_rel, tol_abs, 0))
167 if not pieces:
168 return np.zeros((0, 6)), np.zeros((nf, 0, 4)), np.zeros(0)
169 pieces.sort(key=lambda p: p[0])
170 rows, coefs, oks = [], [], []
171 off = 0
172 for a, b, h, n, c, ok, flag in pieces:
173 rows.append((a, b, h, n, off, flag))
174 coefs.append(c)
175 oks.append(ok)
176 off += n
177 return (np.array(rows, dtype=np.float64), np.ascontiguousarray(np.concatenate(coefs, axis=1)),
178 np.concatenate(oks).astype(np.float64))
179
180
181def _roots(f, a, b, n=800):
182 """Sign changes of f on [a, b], each refined by Brent."""
183 from scipy.optimize import brentq
184 xs = np.linspace(a, b, n + 1)
185 vals = []
186 for x in xs:
187 try:
188 vals.append(f(x))
189 except Exception:
190 vals.append(np.nan)
191 out = []
192 for i in range(n):
193 fa, fb = vals[i], vals[i + 1]
194 if np.isfinite(fa) and np.isfinite(fb) and np.sign(fa) != np.sign(fb) and fa != 0.0:
195 try:
196 out.append(brentq(f, xs[i], xs[i + 1], xtol=1e-13, rtol=1e-14))
197 except Exception:
198 pass
199 return out
200
201
202def heatup_tables(name, T0, Tb, Lb, MW, Tcrit):
203 """(HT segs, HT coef, HT ok, TL segs, TL coef) for a fluid CoolProp knows, else None.
204
205 HT columns, as functions of x = ln P: cp at T_n = T0 + (T_hi - T0)(x_n + 1)/2 for each
206 Gauss-Legendre node x_n, cp at T_hi, and rho at (T0 + T_hi)/2, with T_hi = min(T_s, T_lim);
207 segment flag 1 = above the critical pressure (T_lim infinite). TL: T_lim(x) below it.
208 """
209 key = (str(name), float(T0), float(Tb), float(Lb), float(MW), float(Tcrit))
210 if key in _HEAT_MEMO:
211 return _HEAT_MEMO[key]
212 from engine.pipeline.combustion_physics import _coolprop_state
213 st = _coolprop_state(name)
214 if st is None:
215 _HEAT_MEMO[key] = None
216 return None
217 CP, AS = st
218 x_lo, x_hi = math.log(_P_LO), math.log(_P_HI)
219 try:
220 xc = math.log(float(AS.p_critical()))
221 except Exception:
222 xc = x_hi + 1.0
223 gap = 1e-7 # no knot on the critical pressure itself
224
225 def T_lim(x):
226 AS.update(CP.PQ_INPUTS, math.exp(x), 0.0)
227 return float(AS.T()) - 0.05
228
229 def T_hi(x, sup):
230 Ts = _sat_T(math.exp(x), Tb, Lb, MW, Tcrit)
231 return Ts if sup else min(Ts, T_lim(x))
232
233 def ht(x, sup):
234 P = math.exp(x)
235 th = T_hi(x, sup)
236 Tn = 0.5 * (th - T0) * (GL_X + 1.0) + T0
237 out = np.empty(N_HEAT + 2)
238 for k, T in enumerate(Tn):
239 AS.update(CP.PT_INPUTS, P, float(T))
240 out[k] = AS.cpmass()
241 AS.update(CP.PT_INPUTS, P, th)
242 out[N_HEAT] = AS.cpmass()
243 AS.update(CP.PT_INPUTS, P, 0.5 * (T0 + th))
244 out[N_HEAT + 1] = AS.rhomass()
245 return out
246
247 # breakpoints in x where a column changes form
248 br = {x_lo, x_hi}
249 sub_hi = min(xc - gap, x_hi)
250 if xc < x_hi:
251 br.update({xc - gap, xc + gap})
252 if Tcrit > 0:
253 x_cc = math.log(P_BOIL_REF) + (1.0 / Tb - 1.0 / Tcrit) * MW * Lb / R_UNIVERSAL_KMOL
254 if x_lo < x_cc < x_hi:
255 br.add(x_cc)
256 if sub_hi > x_lo:
257 br.update(_roots(lambda x: _sat_T(math.exp(x), Tb, Lb, MW, Tcrit) - T_lim(x), x_lo, sub_hi))
258 br.update(_roots(lambda x: T_hi(x, False) - T0, x_lo, sub_hi))
259 if xc + gap < x_hi:
260 br.update(_roots(lambda x: T_hi(x, True) - T0, xc + gap, x_hi))
261 pts = sorted(b for b in br if x_lo <= b <= x_hi)
262
263 segs = []
264 for a, b in zip(pts[:-1], pts[1:]):
265 if b - a < 1e-9 or (xc - gap < 0.5 * (a + b) < xc + gap):
266 continue
267 sup = 0.5 * (a + b) > xc
268 try:
269 if not T_hi(0.5 * (a + b), sup) > T0:
270 continue # no heat-up here: the kernel skips the table
271 except Exception:
272 continue
273 segs.append((a, b, 1.0 if sup else 0.0))
274
275 def ht_rel(x, flag):
276 return ht(x, flag > 0.5)
277
278 tol = np.full(N_HEAT + 2, _TOL_CP)
279 tol[N_HEAT + 1] = _TOL_RHO
280 HT = _piecewise(segs, ht_rel, N_HEAT + 2, tol, 0.0)
281 TL = (_piecewise([(x_lo, sub_hi, 0.0)], lambda x, f: np.array([T_lim(x)]), 1, 0.0, _TOL_T)
282 if sub_hi > x_lo else (np.zeros((0, 6)), np.zeros((1, 0, 4)), np.zeros(0)))
283 out = (HT[0], HT[1], HT[2], TL[0], TL[1], TL[2])
284 _HEAT_MEMO[key] = out
285 return out
286
287
288_NO_HT = (np.zeros((0, 6)), np.zeros((N_HEAT + 2, 0, 4)), np.zeros(0),
289 np.zeros((0, 6)), np.zeros((1, 0, 4)), np.zeros(0))
290
291
292@njit(cache=True)
293def _pw_find(segs, ok, x):
294 """(interval index, dx) of x in a piecewise table, or (-1, 0) outside or on a refused one."""
295 for s in range(segs.shape[0]):
296 lo = segs[s, 0]
297 if lo <= x <= segs[s, 1]:
298 h = segs[s, 2]
299 n = int(segs[s, 3])
300 i = int((x - lo)/h)
301 if i >= n:
302 i = n - 1
303 j = int(segs[s, 4]) + i
304 if ok[j] < 0.5:
305 return -1, 0.0
306 return j, x - (lo + i*h)
307 return -1, 0.0
308
309
310@njit(cache=True)
311def _pw_val(coef, k, j, dx):
312 return ((coef[k, j, 0]*dx + coef[k, j, 1])*dx + coef[k, j, 2])*dx + coef[k, j, 3]
313
314
315@njit(cache=True)
316def _pw_flag(segs, x):
317 for s in range(segs.shape[0]):
318 if segs[s, 0] <= x <= segs[s, 1]:
319 return segs[s, 5]
320 return -1.0
321
322
323# =============================================================================================
324# Small mirrors
325# =============================================================================================
326@njit(cache=True)
327def _saturation_state(P, Tb, Lb, MW, Tcrit):
328 """combustion_physics.saturation_state -> (ok, T_sat, h_fg)."""
329 if not (P > 0.0 and Tb > 0.0 and Lb > 0.0 and MW > 0.0):
330 return False, 0.0, 0.0
331 inv_T = 1.0/Tb - (R_UNIVERSAL_KMOL/(MW*Lb))*math.log(P/P_BOIL_REF)
332 if Tcrit > 0.0:
333 if Tcrit <= Tb:
334 return False, 0.0, 0.0
335 if inv_T <= 1.0/Tcrit:
336 return True, Tcrit, 0.0
337 T_sat = 1.0/inv_T
338 return True, T_sat, Lb*((Tcrit - T_sat)/(Tcrit - Tb))**WATSON_EXPONENT
339 if inv_T <= 0.0:
340 return False, 0.0, 0.0
341 return True, 1.0/inv_T, Lb
342
343
344@njit(cache=True)
345def _huzel_mu(T, M):
346 """regen_cooling.calculate_gas_viscosity_huzel."""
347 return 46.6e-10*(M**0.5)*((T*1.8)**0.6)*(0.45359237/0.0254)
348
349
350@njit(cache=True)
351def _area_ratio(M, gamma):
352 return (1.0/M)*((2.0/(gamma + 1.0))*(1.0 + 0.5*(gamma - 1.0)*M*M))**(
353 (gamma + 1.0)/(2.0*(gamma - 1.0)))
354
355
356@njit(cache=True)
357def _mach(ar, gamma, sup):
358 """gas_side.mach_from_area_ratio for one station: the root of A/A*(M) = ar on the requested
359 branch. The Python bisects 64 times; this is Newton kept inside the same shrinking bracket,
360 which lands on the same root to the last bit or two in ~6 steps instead of 64."""
361 if ar <= 1.0 + 1e-12:
362 return 1.0
363 if sup:
364 lo = 1.0; hi = 50.0
365 M = 1.0 + math.sqrt(2.0*(ar - 1.0)/(gamma + 1.0)) if ar < 2.0 else \
366 ((gamma + 1.0)/(gamma - 1.0))**((gamma + 1.0)/4.0)*ar**((gamma - 1.0)/2.0)
367 else:
368 lo = 1e-9; hi = 1.0
369 M = (2.0/(gamma + 1.0))**((gamma + 1.0)/(2.0*(gamma - 1.0)))/ar
370 if ar < 2.0:
371 M = 1.0 - math.sqrt(2.0*(ar - 1.0)/(gamma + 1.0))
372 if not (lo < M < hi):
373 M = 0.5*(lo + hi)
374 for _ in range(100):
375 A = _area_ratio(M, gamma)
376 f = A - ar
377 # keep the bracket: subsonic A/A* falls with M, supersonic rises
378 if (f < 0.0) if sup else (f > 0.0):
379 lo = M
380 else:
381 hi = M
382 dA = A*(M*M - 1.0)/(M*(1.0 + 0.5*(gamma - 1.0)*M*M))
383 Mn = M - f/dA if dA != 0.0 else 0.5*(lo + hi)
384 if not (lo < Mn < hi):
385 Mn = 0.5*(lo + hi)
386 if abs(Mn - M) <= 1e-16*M or hi - lo <= 1e-16*M:
387 return Mn
388 M = Mn
389 return M
390
391
392@njit(cache=True)
393def _kappa(CR, gamma):
394 """nozzle.nozzle_stagnation_loss (CR <= 1: bore not declared)."""
395 if not (CR > 1.0) or not np.isfinite(CR):
396 return 1.0
397 M = _mach(CR, gamma, False)
398 g = gamma
399 return (1.0 + g*M*M)/(1.0 + 0.5*(g - 1.0)*M*M)**(g/(g - 1.0))
400
401
402@njit(cache=True)
403def _axis(lo, hi, n, x, log):
404 """cea_cache._aux_axis on a uniform (or log-uniform) grid of n points."""
405 xc = min(max(x, lo), hi)
406 if log:
407 f = (math.log(xc) - math.log(lo))/(math.log(hi) - math.log(lo))*(n - 1)
408 else:
409 f = (xc - lo)/(hi - lo)*(n - 1)
410 i = int(f)
411 if i > n - 2:
412 i = n - 2
413 return i, f - i
414
415
416@njit(cache=True)
417def _aux2(Q, tab, k, MR, Pc):
418 """CEAAuxTables._bilinear."""
419 i, wi = _axis(Q[Q_AX_MR0], Q[Q_AX_MR1], int(Q[Q_AX_NMR]), MR, False)
420 j, wj = _axis(Q[Q_AX_PC0], Q[Q_AX_PC1], int(Q[Q_AX_NPC]), Pc, True)
421 return ((1 - wi)*(1 - wj)*tab[k, i, j] + wi*(1 - wj)*tab[k, i + 1, j]
422 + (1 - wi)*wj*tab[k, i, j + 1] + wi*wj*tab[k, i + 1, j + 1])
423
424
425@njit(cache=True)
426def _aux3(Q, tab, k, MR, Pc, eps):
427 """CEAAuxTables._exit."""
428 i, wi = _axis(Q[Q_AX_MR0], Q[Q_AX_MR1], int(Q[Q_AX_NMR]), MR, False)
429 j, wj = _axis(Q[Q_AX_PC0], Q[Q_AX_PC1], int(Q[Q_AX_NPC]), Pc, True)
430 kk, wk = _axis(Q[Q_AX_E0], Q[Q_AX_E1], int(Q[Q_AX_NE]), eps, True)
431 v = 0.0
432 for di in range(2):
433 fi = (1 - wi) if di == 0 else wi
434 for dj in range(2):
435 fj = (1 - wj) if dj == 0 else wj
436 for dk in range(2):
437 fk = (1 - wk) if dk == 0 else wk
438 v += fi*fj*fk*tab[k, i + di, j + dj, kk + dk]
439 return v
440
441
442# =============================================================================================
443# c*(O/F) from the wide CEA table (combustion_physics.CstarOfMR)
444# =============================================================================================
445@njit(cache=True)
446def _wide_row(lnPc, cs, Pc, row):
447 """CstarWideTable.row into ``row``."""
448 x = math.log(Pc)
449 if x < lnPc[0]:
450 x = lnPc[0]
451 elif x > lnPc[-1]:
452 x = lnPc[-1]
453 j = np.searchsorted(lnPc, x) - 1
454 if j < 0:
455 j = 0
456 elif j > lnPc.size - 2:
457 j = lnPc.size - 2
458 f = (x - lnPc[j])/(lnPc[j + 1] - lnPc[j])
459 for i in range(row.size):
460 row[i] = (1.0 - f)*cs[i, j] + f*cs[i, j + 1]
461
462
463@njit(cache=True)
464def _interp(x, xp, fp):
465 """np.interp for one point."""
466 n = xp.size
467 if x <= xp[0]:
468 return fp[0]
469 if x >= xp[n - 1]:
470 return fp[n - 1]
471 j = np.searchsorted(xp, x, side="right") - 1
472 slope = (fp[j + 1] - fp[j])/(xp[j + 1] - xp[j])
473 return slope*(x - xp[j]) + fp[j]
474
475
476@njit(cache=True)
477def _cs_of_r(r, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi):
478 """CstarOfMR.of_fraction for one oxidizer mass fraction -> (c*, outside the table)."""
479 r = _clip(r, 0.0, 1.0)
480 if r < r_lo:
481 return c_lo*math.sqrt(r/r_lo), True
482 if r > r_hi:
483 return c_hi*math.sqrt((1.0 - r)/(1.0 - r_hi)), True
484 mr = _clip(r/(1.0 - r), MR_lo, MR_hi)
485 return _interp(math.log(mr), lnMR, row), False
486
487
488@njit(cache=True)
489def _cs_of_MR(MR, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi):
490 """CstarOfMR.__call__."""
491 r = MR/(1.0 + MR) if np.isfinite(MR) else 1.0
492 return _cs_of_r(r, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi)[0]
493
494
495@njit(cache=True)
496def _rupe_Em_at_M(M, Em_opt, M_opt, curv):
497 """combustion_physics.rupe_Em_at_M (NaN where the Python raises)."""
498 if not (np.isfinite(M) and M > 0.0 and np.isfinite(M_opt) and M_opt > 0.0):
499 return np.nan
500 if not (Em_opt > 0.0 and Em_opt <= 1.0) or not (np.isfinite(curv) and curv >= 0.0):
501 return np.nan
502 N = M/(1.0 + M)
503 N0 = M_opt/(1.0 + M_opt)
504 return max(Em_opt*(1.0 - curv*(N - N0)**2), 0.0)
505
506
507# =============================================================================================
508# eta_vap: the droplet march (combustion_physics.calculate_vaporization_efficiency)
509# =============================================================================================
510@njit(cache=True)
511def _heatup(Q, b, HTs, HTc, HTo, TLs, TLc, TLo, Pc, T0, Ts, Tc):
512 """liquid_heatup_properties from the tables -> (status, I, rho_l).
513
514 status 1: CoolProp values; 0: Python returns None here (configured props);
515 -1: the tables cannot answer (a refused interval / outside them) -- no solution."""
516 x = math.log(Pc)
517 flag = _pw_flag(HTs, x)
518 if flag > 0.5:
519 T_lim = np.inf
520 else:
521 j, dx = _pw_find(TLs, TLo, x)
522 if j < 0:
523 return -1, 0.0, 0.0
524 T_lim = _pw_val(TLc, 0, j, dx)
525 T_hi = Ts if Ts < T_lim else T_lim
526 if not (T_hi > T0):
527 return 0, 0.0, 0.0
528 j, dx = _pw_find(HTs, HTo, x)
529 if j < 0:
530 return -1, 0.0, 0.0
531 acc = 0.0
532 for k in range(N_HEAT):
533 Tn = 0.5*(T_hi - T0)*(GL_X[k] + 1.0) + T0
534 acc += GL_W[k]*_pw_val(HTc, k, j, dx)/(Tc - Tn)
535 I = 0.5*(T_hi - T0)*acc
536 if Ts > T_hi:
537 I += _pw_val(HTc, N_HEAT, j, dx)*math.log((Tc - T_hi)/(Tc - Ts))
538 rho = _pw_val(HTc, N_HEAT + 1, j, dx)
539 cp_mean = I/math.log((Tc - T0)/(Tc - Ts))
540 if not (np.isfinite(I) and I > 0.0 and np.isfinite(rho) and rho > 0.0
541 and np.isfinite(cp_mean) and cp_mean > 0.0):
542 return 0, 0.0, 0.0
543 return 1, I, rho
544
545
546@njit(cache=True)
547def _eta_vap(Q, CH, Pc, Tc, gamma, R, MR, mdot, D32_O, D32_F, u_O, u_F, u_ax, L_imp, L_b,
548 lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi, out):
549 """calculate_vaporization_efficiency -> eta_vap (NaN where the Python raises)."""
550 w = np.empty(2)
551 w[0] = MR/(1.0 + MR)
552 w[1] = 1.0/(1.0 + MR)
553 D32 = np.empty(2); D32[0] = D32_O; D32[1] = D32_F
554 uu = np.empty(2); uu[0] = u_O; uu[1] = u_F
555 instant = np.zeros(2, dtype=np.bool_)
556 Ts = np.zeros(2); hfg = np.zeros(2); rho_l = np.zeros(2); I_h = np.zeros(2); T0s = np.zeros(2)
557 has_I = np.zeros(2, dtype=np.bool_)
558 n_valid_d32 = 0
559 for s in range(2):
560 b = Q_SO_OK + s*_S_STRIDE
561 d = D32[s]
562 # the injector publishes NaN for a stream that does not flow
563 ok_d = np.isfinite(d) and d > 0.0 and uu[s] > 0.0
564 if ok_d:
565 n_valid_d32 += 1
566 if not ok_d or Q[b] < 0.5:
567 instant[s] = True
568 continue
569 okS, T_s, h_fg = _saturation_state(Pc, Q[b + 2], Q[b + 3], Q[b + 4], Q[b + 5])
570 if not okS:
571 return np.nan
572 T0 = Q[b + 6]
573 Ts[s] = T_s; hfg[s] = h_fg; T0s[s] = T0
574 st = 0
575 I = 0.0; rho = 0.0
576 if T_s > T0 and Q[b + 8] > 0.5:
577 if s == 0:
578 st, I, rho = _heatup(Q, b, CH[12], CH[13], CH[14], CH[15], CH[16], CH[17], Pc, T0, T_s, Tc)
579 else:
580 st, I, rho = _heatup(Q, b, CH[18], CH[19], CH[20], CH[21], CH[22], CH[23], Pc, T0, T_s, Tc)
581 if st < 0:
582 return np.nan
583 if st == 1:
584 rho_l[s] = rho; I_h[s] = I; has_I[s] = True
585 else:
586 rho_l[s] = Q[b + 1]
587 I_h[s] = Q[b + 7]*max(math.log((Tc - T0)/(Tc - T_s)), 0.0)
588 if n_valid_d32 == 0:
589 return np.nan
590
591 L_ch = Q[Q_LSTAR]*Q[Q_AT]/Q[Q_AC]
592 x0 = 0.0
593 if np.isfinite(L_imp) and L_imp >= 0.0:
594 x0 += L_imp
595 if np.isfinite(L_b) and L_b >= 0.0:
596 x0 += L_b
597 L_march = L_ch - x0
598 marched_any = (not instant[0]) or (not instant[1])
599 F0 = 0.0
600 for s in range(2):
601 if instant[s]:
602 F0 += w[s]
603 fO = 1.0; fF = 1.0
604 if L_march > 0.0 or not marched_any:
605 L_c = L_march if L_march > 1e-9 else 1e-9
606 if not (Q[Q_RR_Q] > 1.0 and np.isfinite(Q[Q_RR_Q])):
607 return np.nan
608 if not (Pc > 0.0 and Tc > 0.0 and R > 0.0 and mdot > 0.0 and Q[Q_AC] > 0.0
609 and u_ax > 0.0 and np.isfinite(u_ax)) or not (gamma > 1.0):
610 return np.nan
611 cp_g = gamma*R/(gamma - 1.0)
612 rho_c = Pc/(R*Tc)
613 U_c = mdot/(rho_c*Q[Q_AC])
614 M_gas = R_UNIVERSAL_KMOL/R
615 Pr = PRANDTL_DEFAULT
616 pr3 = Pr**(1.0/3.0)
617 nq = _RR_NODES.size
618 nm = (0 if instant[0] else 1) + (0 if instant[1] else 1)
619 if nm > 0:
620 n = nm*nq
621 D2_0 = np.empty(n); w_cls = np.empty(n); rl = np.empty(n); mu = np.empty(n)
622 rf = np.empty(n); kf = np.empty(n); lnB = np.empty(n); t_h = np.empty(n)
623 inv_FB = np.empty(n); cd_blow = np.empty(n)
624 starts = np.empty(nm, dtype=np.int64)
625 m = 0
626 for s in range(2):
627 if instant[s]:
628 continue
629 T_s = Ts[s]
630 T_f = T_s + (Tc - T_s)/3.0
631 mu_f = _huzel_mu(T_f, M_gas)
632 k_f = mu_f*cp_g/Pr
633 rho_f = Pc/(R*T_f)
634 B = cp_g*(Tc - T_s)/hfg[s] if hfg[s] > 0 else np.inf
635 lB = math.log1p(B)
636 iFB = 1.0; cdb = 1.0
637 if Q[Q_BLOW_AS] > 0.5 and np.isfinite(B):
638 if not (B >= 0.0):
639 return np.nan
640 FB = 1.0 if B < 1e-8 else (1.0 + B)**0.7*math.log1p(B)/B
641 iFB = 1.0/FB
642 cdb = (1.0 + B)**-0.2
643 starts[m] = m*nq
644 for k in range(nq):
645 D0 = D32[s]*Q[Q_RR_XSCALE]*CH[11][k]
646 Nu0 = 2.0 + 0.6*math.sqrt(rho_f*u_ax*D0/mu_f)*pr3
647 idx = m*nq + k
648 D2_0[idx] = D0**2
649 w_cls[idx] = w[s]*RR_W[k]
650 rl[idx] = rho_l[s]; mu[idx] = mu_f; rf[idx] = rho_f; kf[idx] = k_f
651 lnB[idx] = lB
652 t_h[idx] = rho_l[s]*D0**2/(6.0*Nu0*k_f)*max(I_h[s], 0.0)
653 inv_FB[idx] = iFB; cd_blow[idx] = cdb
654 m += 1
655 dx = L_c/_MARCH_STEPS
656 fv, xv, F = _march_core(D2_0, w_cls, rl, mu, rf, kf, lnB, t_h, inv_FB, cd_blow,
657 U_c, rho_c, pr3, cp_g, dx, _MARCH_STEPS, u_ax, F0, _V_FLOOR,
658 RR_W, starts, nq)
659 m = 0
660 for s in range(2):
661 if instant[s]:
662 continue
663 if s == 0:
664 fO = fv[m]
665 else:
666 fF = fv[m]
667 m += 1
668 else:
669 F = F0
670 else:
671 F = F0
672 if not instant[0]:
673 fO = 0.0
674 if not instant[1]:
675 fF = 0.0
676 MR_vap = MR*fO/fF if fF > 0.0 else np.inf
677 ratio = (_cs_of_MR(MR_vap, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi)
678 / _cs_of_MR(MR, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi))
679 out[R_F_VAP] = F; out[R_F_VAP_O] = fO; out[R_F_VAP_F] = fF; out[R_X0] = x0; out[R_U_DROP0] = u_ax
680 return F*ratio
681
682
683# =============================================================================================
684# eta_mix: Rupe E_m -> stream tubes (combustion_physics.stream_tube_mixing_efficiency)
685# =============================================================================================
686@njit(cache=True)
687def _eta_mix(Q, CH, MR, rupe_M, holes_O, holes_F, lnMR, row, MR_lo, MR_hi, r_lo, r_hi,
688 c_lo, c_hi, out):
689 Em_opt = Q[Q_EM_OPT]; M_opt = Q[Q_M_OPT]; curv = Q[Q_EM_CURV]
690 has_M = np.isfinite(rupe_M) and rupe_M > 0.0
691 if has_M:
692 Em = _rupe_Em_at_M(rupe_M, Em_opt, M_opt, curv)
693 else:
694 Em = Em_opt
695 if not np.isfinite(Em):
696 return np.nan
697 out[R_EM] = Em
698 split = holes_O.size > 0 and holes_F.size > 0 and holes_O.size == holes_F.size
699 ne = holes_O.size if split else 1
700 W = np.empty(ne); mr = np.empty(ne); Emj = np.empty(ne)
701 if split:
702 for j in range(ne):
703 o = holes_O[j]; f = holes_F[j]
704 mr_j = o/f if f > 0.0 else np.inf
705 tot = o + f
706 if not (np.isfinite(mr_j) and mr_j > 0.0 and np.isfinite(tot) and tot >= 0.0):
707 return np.nan # _element_split raises
708 W[j] = tot; mr[j] = mr_j
709 if has_M:
710 Emj[j] = _rupe_Em_at_M(rupe_M*(mr_j/MR)**2, Em_opt, M_opt, curv)
711 if not np.isfinite(Emj[j]):
712 return np.nan
713 else:
714 Emj[j] = Em
715 else:
716 W[0] = 1.0; mr[0] = MR; Emj[0] = Em
717 if not (np.isfinite(MR) and MR > 0.0):
718 return np.nan
719 R = MR/(1.0 + MR)
720 Ws = 0.0
721 for j in range(ne):
722 if W[j] < 0.0:
723 return np.nan
724 Ws += W[j]
725 if not (Ws > 0.0):
726 return np.nan
727 for j in range(ne):
728 W[j] = W[j]/Ws
729 rj = np.empty(ne)
730 sw = 0.0
731 for j in range(ne):
732 rj[j] = mr[j]/(1.0 + mr[j])
733 sw += W[j]*rj[j]
734 shift = R - sw
735 z = CH[9]; wz = CH[10]
736 num = 0.0
737 for j in range(ne):
738 r0 = rj[j] + shift
739 mad = 2.0*(1.0 - Emj[j])*r0*(1.0 - r0)
740 for k in range(z.size):
741 r = r0 + mad*z[k]
742 r = _clip(r, 0.0, 1.0)
743 num += W[j]*wz[k]*_cs_of_r(r, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi)[0]
744 return num/_cs_of_MR(MR, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi)
745
746
747# =============================================================================================
748# Ablative liner (ablative_cooling.liner_response over gas_side.profile)
749# =============================================================================================
750@njit(cache=True)
751def _lk_eps0(c, t, paL):
752 x = math.log10(max(paL, 1e-12))
753 a0 = 0.0; a1 = 0.0; a2 = 0.0
754 for j in range(c.shape[1]):
755 tj = t**j
756 a0 += c[0, j]*tj
757 a1 += c[1, j]*tj
758 a2 += c[2, j]*tj
759 return math.exp(a0 + a1*x + a2*x*x)
760
761
762@njit(cache=True)
763def _lk_species(h2o, T, P_bar, pa_bar, L_cm):
764 t = T/1000.0
765 tc = min(T, LECKNER_T_MAX)/1000.0
766 paL = pa_bar*L_cm
767 if h2o:
768 e0 = _lk_eps0(LK_H2O, t, paL)
769 PE = P_bar + 2.56*pa_bar/math.sqrt(tc)
770 a = 1.888 - 2.053*math.log10(tc) if tc > 0.75 else 2.144
771 b = 1.10/tc**1.4
772 cc = 0.5
773 paL_m = 13.2*tc*tc
774 else:
775 e0 = _lk_eps0(LK_CO2, t, paL)
776 PE = P_bar + 0.28*pa_bar
777 a = 1.0 + 0.1/tc**1.45
778 b = 0.23
779 cc = 1.47
780 paL_m = 0.054/(tc*tc) if tc < 0.7 else 0.225*tc*tc
781 ratio = 1.0 - (a - 1.0)*(1.0 - PE)/(a + b - 1.0 + PE)*math.exp(
782 -cc*math.log10(paL_m/max(paL, 1e-12))**2)
783 return e0*ratio if paL > 0.0 else 0.0
784
785
786@njit(cache=True)
787def _lk_overlap(pH, pC, L_cm):
788 tot = max(pH + pC, 1e-30)
789 zeta = pH/tot
790 x = math.log10(max(tot*L_cm, 1e-30))
791 d = (zeta/(10.7 + 101.0*zeta) - 0.0089*zeta**10.4)*max(x, 0.0)**2.76
792 return d if (pH > 0 and pC > 0) else 0.0
793
794
795@njit(cache=True)
796def _ablative_Q(Q, CH, T0, P0, gamma, G, mu, cp, Pr, xH, xC, gas_mdot):
797 """liner_response's heat_removed [W]: the lined stations' flux at the ablation temperature,
798 through the pyrolysis-blowing fixed point."""
799 xs = CH[6]; rs = CH[7]; dA = CH[8]
800 n = xs.size
801 Tw = Q[Q_AB_TS]; ew = Q[Q_AB_EW]; Rt = Q[Q_AB_RT]; beam = Q[Q_AB_LB]
802 Dt = 2.0*Rt
803 rc = THROAT_RC_OVER_RT*Rt
804 E = Q[Q_AB_E]
805 qc = np.empty(n); qr = np.empty(n)
806 rr = Pr**(1.0/3.0)
807 for k in range(n):
808 r = rs[k]
809 if k > 0 and r == rs[k - 1] and (xs[k] > 0.0) == (xs[k - 1] > 0.0):
810 # the barrel: every station at the same radius sees the same gas
811 qc[k] = qc[k - 1]; qr[k] = qr[k - 1]
812 continue
813 ar = (r/Rt)**2
814 if ar < 1.0:
815 ar = 1.0
816 M = _mach(ar, gamma, xs[k] > 0.0)
817 kk = 1.0 + 0.5*(gamma - 1.0)*M*M
818 tT = 1.0/kk
819 tP = tT**(gamma/(gamma - 1.0))
820 k2 = 0.5*(gamma - 1.0)*M*M
821 Taw = T0*(1.0 + rr*k2)/(1.0 + k2)
822 sig = 1.0/((0.5*(Tw/T0)*kk + 0.5)**(0.8 - BARTZ_OMEGA/5.0)*kk**(BARTZ_OMEGA/5.0))
823 h = (0.026/Dt**0.2*(mu**0.2*cp/Pr**0.6)*G**0.8
824 *(Dt/rc)**0.1*(1.0/ar)**0.9*sig)
825 Tg = T0*tT
826 P_bar = P0*tP/1e5
827 L = min(beam, CYLINDER_BEAM_LENGTH_OVER_D*2.0*r)
828 L_cm = L*100.0
829 pH = xH*P_bar; pC = xC*P_bar
830 eps_g = max(_lk_species(True, Tg, P_bar, pH, L_cm) + _lk_species(False, Tg, P_bar, pC, L_cm)
831 - _lk_overlap(pH, pC, L_cm), 0.0)
832 Twa = Tw if Tw < Tg else Tg
833 s = Twa/Tg
834 aH = _lk_species(True, Twa, P_bar, pH*s, L_cm)
835 aC = _lk_species(False, Twa, P_bar, pC*s, L_cm)
836 dd = _lk_overlap(pH*s, pC*s, L_cm)
837 alpha = max((1.0/s)**0.45*aH + (1.0/s)**0.65*aC - dd, 0.0)
838 fe = ew/(1.0 - (1.0 - ew)*(1.0 - alpha))
839 q_rad = fe*SIGMA_SB*(eps_g*Tg**4 - alpha*Tw**4)
840 q_conv = h*(Taw - Tw)
841 qc[k] = q_conv if q_conv > 0.0 else 0.0
842 qr[k] = q_rad if q_rad > 0.0 else 0.0
843 f = 1.0
844 for _ in range(50):
845 m_pyro = 0.0
846 for k in range(n):
847 qn = f*qc[k] + qr[k]
848 if qn < 0.0:
849 qn = 0.0
850 m_pyro += qn/E*dA[k]
851 if Q[Q_AB_PHYS] > 0.5:
852 if not (gas_mdot > 0.0):
853 return np.nan
854 B = m_pyro/gas_mdot
855 f_new = max(1.0/(1.0 + Q[Q_AB_BC]*B), Q[Q_AB_BMIN])
856 else:
857 f_new = 1.0 - Q[Q_AB_BEFF]
858 if abs(f_new - f) < 1e-12:
859 f = f_new
860 break
861 f = f_new
862 if Q[Q_AB_BELOW] > 0.5:
863 return 0.0
864 Qt = 0.0
865 for k in range(n):
866 qn = f*qc[k] + qr[k]
867 if qn < 0.0:
868 qn = 0.0
869 Qt += qn*dA[k]
870 return Qt
871
872
873# =============================================================================================
874# One chamber state at Pc (chamber_solver.residual / the converged diagnostics)
875# =============================================================================================
876@njit(cache=True)
877def _point(Pc, P, CH, C, P_O, P_F, eps_cea, out):
878 """supply - demand at Pc (NaN where the Python residual is NaN or raises); fills ``out``."""
879 if not (np.isfinite(Pc) and Pc > 0.0):
880 return np.nan
881 Q = CH[0]
882 sol = _solve_injector(P, P_O, P_F, Pc)
883 ok = sol[0]; mO = sol[1]; mF = sol[2]; uO = sol[3]; uF = sol[4]; D32O = sol[5]; D32F = sol[6]
884 if ok == 0.0 or not (np.isfinite(mO) and np.isfinite(mF)):
885 return np.nan
886 holes_O = sol[26]; holes_F = sol[27]
887 mdot = mO + mF
888 if abs(mF) < 1e-12:
889 return np.nan
890 MR = mO/mF
891 if not np.isfinite(MR):
892 return np.nan
893 cs_id, cf_id, Tc, gm, Rg, Mg, cfv = cea_eval(C[0], C[1], C[2], C[3], C[4], C[5], C[6], C[7],
894 C[8], C[9], MR, Pc, eps_cea)
895 if not (np.isfinite(cs_id) and cs_id > 0.0):
896 return np.nan
897
898 # ablative liner and the heat it takes
899 Q_abl = 0.0
900 Tc_eff = Tc
901 if Q[Q_AB_ON] > 0.5 and mdot > 0.0:
902 MRt = mO/mF if mF > 0.0 else MR
903 aux2 = CH[1]
904 mu = _aux2(Q, aux2, 0, MRt, Pc)
905 cpg = _aux2(Q, aux2, 1, MRt, Pc)
906 Prg = _aux2(Q, aux2, 2, MRt, Pc)
907 xH = max(_aux2(Q, aux2, 3, MRt, Pc), 0.0)
908 xC = max(_aux2(Q, aux2, 4, MRt, Pc), 0.0)
909 Q_abl = _ablative_Q(Q, CH, Tc, Pc, gm, mdot/Q[Q_AT], mu, cpg, Prg, xH, xC, mdot)
910 if not np.isfinite(Q_abl):
911 return np.nan
912 if Q_abl > 0.0:
913 cp = gm*Rg/max(gm - 1.0, 1e-6)
914 Tc_eff = max(Tc - Q_abl/max(mdot*cp, 1e-6), 1.0)
915
916 # c*(O/F) at this Pc
917 lnMR = CH[3]; lnPc = CH[4]; cs = CH[5]
918 row = np.empty(lnMR.size)
919 _wide_row(lnPc, cs, Pc, row)
920 MR_lo = CH[2][0]; MR_hi = CH[2][1] # CstarOfMR: wide.MR_min / MR_max
921 r_lo = MR_lo/(1.0 + MR_lo); r_hi = MR_hi/(1.0 + MR_hi)
922 c_lo = _interp(math.log(MR_lo), lnMR, row)
923 c_hi = _interp(math.log(MR_hi), lnMR, row)
924
925 # injector quantities the c* model reads from the diagnostics
926 u_ax = np.nan; L_imp = np.nan; L_b = np.nan; rupe_M = np.nan
927 w_O = MR/(1.0 + MR); w_F = 1.0/(1.0 + MR)
928 if Q[Q_IMP] > 0.5:
929 # spray.spray_axial_velocity, u_rel when it is not positive (impinging.py u_transport)
930 if mdot > 0.0:
931 ua = (mO*uO*math.cos(math.radians(P[ANG_O])) + mF*uF*math.cos(math.radians(P[ANG_F])))/mdot
932 else:
933 ua = np.nan
934 u_ax = ua if (np.isfinite(ua) and ua > 0.0) else sol[20]
935 # impinging.impingement_standoff_m and the sheet breakup length
936 nn = max(1.0, 0.5*(P[NO] + P[NF]))
937 dr = 0.5*abs(nn*P[SPO]/math.pi - nn*P[SPF]/math.pi)
938 tan_sum = math.tan(math.radians(P[ANG_O])) + math.tan(math.radians(P[ANG_F]))
939 if np.isfinite(tan_sum) and tan_sum > 1e-9 and dr > 0.0:
940 L_imp = dr/tan_sum
941 rho_gas = max(Pc/(P[SP_GASR]*P[SP_GAST]), 1e-6)
942 d_avg = 0.5*(P[DJO] + P[DJF])
943 if np.isfinite(L_imp) and L_imp > 0 and d_avg > 0 and rho_gas > 1e-4:
944 L_b = _clip(d_avg**2/(4.0*L_imp)*math.sqrt(0.5*(P[RHO_O] + P[RHO_F])/rho_gas),
945 0.0, 20.0*d_avg)
946 else:
947 L_b = 0.0
948 # impinging.rupe_mixing_ratio on the bulk jet velocities
949 vO = mO/(P[RHO_O]*sol[14]) if (P[RHO_O] > 0 and sol[14] > 0) else np.nan
950 vF = mF/(P[RHO_F]*sol[15]) if (P[RHO_F] > 0 and sol[15] > 0) else np.nan
951 den = P[RHO_F]*vF**2*P[DJF]
952 if np.isfinite(den) and den > 0 and np.isfinite(vO) and P[RHO_O] > 0 and P[DJO] > 0:
953 rupe_M = P[RHO_O]*vO**2*P[DJO]/den
954 else:
955 # no impingement geometry: drops leave axially at the mass-averaged injection speed
956 u_ax = w_O*uO + w_F*uF
957
958 # eta_vap
959 model = Q[Q_EFF_MODEL]
960 if model == 0.0:
961 eta_vap = 1.0 - Q[Q_EFF_C]
962 elif model == 1.0:
963 eta_vap = _clip(1.0 - Q[Q_EFF_C]*(1.0 - Q[Q_LSTAR]/1.0), 0.0, 1.0)
964 else:
965 eta_vap = _eta_vap(Q, CH, Pc, Tc, gm, Rg, MR, mdot, D32O, D32F, uO, uF, u_ax, L_imp, L_b,
966 lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi, out)
967 if not np.isfinite(eta_vap):
968 return np.nan
969 eta_mix = _eta_mix(Q, CH, MR, rupe_M, holes_O, holes_F, lnMR, row, MR_lo, MR_hi, r_lo, r_hi,
970 c_lo, c_hi, out)
971 if not np.isfinite(eta_mix):
972 return np.nan
973
974 # heat lost through the wall (combustion_eff.heat_loss_cstar_efficiency)
975 eta_HL = 1.0
976 if Q[Q_USE_COUP] > 0.5 and Q_abl > 0.0:
977 if not (mdot > 0.0 and Tc > 0.0 and Rg > 0.0 and gm > 1.0):
978 return np.nan
979 xq = Q_abl/(mdot*gm*Rg/(gm - 1.0)*Tc)
980 eta_HL = math.sqrt(max(1.0 - xq, 0.0))
981 eta = eta_vap*eta_mix*eta_HL
982 if not (np.isfinite(eta) and eta > 0.0):
983 return np.nan
984 cstar = eta*cs_id
985 kappa = _kappa(Q[Q_CR], gm)
986 num = Pc/kappa*Q[Q_AT]
987 if not (np.isfinite(num) and np.isfinite(cstar)) or abs(cstar) < 1e-12:
988 return np.nan
989 demand = num/cstar
990 if not np.isfinite(demand):
991 return np.nan
992 res = mdot - demand
993 out[R_MR] = MR; out[R_MDOT] = mdot; out[R_MDOT_O] = mO; out[R_MDOT_F] = mF
994 out[R_CSTAR_IDEAL] = cs_id; out[R_CSTAR] = cstar; out[R_ETA] = eta; out[R_ETA_VAP] = eta_vap
995 out[R_ETA_MIX] = eta_mix; out[R_ETA_HL] = eta_HL; out[R_GAMMA] = gm; out[R_R] = Rg
996 out[R_TC_IDEAL] = Tc; out[R_TC_EFF] = Tc_eff; out[R_KAPPA] = kappa; out[R_Q_ABL] = Q_abl
997 out[R_RUPE_M] = rupe_M; out[R_M_MOL] = Mg
998 return res if np.isfinite(res) else np.nan
999
1000
1001@njit(cache=True)
1002def _brentq(P, CH, C, P_O, P_F, a, b, xtol, rtol, maxit, out):
1003 """scipy.optimize.brentq (the C original), on _point."""
1004 eps = CH[0][Q_EPS]
1005 fa = _point(a, P, CH, C, P_O, P_F, eps, out)
1006 fb = _point(b, P, CH, C, P_O, P_F, eps, out)
1007 if not np.isfinite(fa) or not np.isfinite(fb):
1008 return np.nan
1009 if fa == 0.0:
1010 return a
1011 if fb == 0.0:
1012 return b
1013 if _sign(fa) == _sign(fb):
1014 return np.nan
1015 c = a; fc = fa; d = b - a; e = d
1016 for _ in range(maxit):
1017 if _sign(fb) == _sign(fc):
1018 c = a; fc = fa; d = b - a; e = d
1019 if abs(fc) < abs(fb):
1020 a = b; b = c; c = a; fa = fb; fb = fc; fc = fa
1021 tol = 2.0*rtol*abs(b) + 0.5*xtol
1022 m = 0.5*(c - b)
1023 if fb == 0.0 or abs(m) <= tol:
1024 return b
1025 if abs(e) < tol or abs(fa) <= abs(fb):
1026 d = m; e = m
1027 else:
1028 s = fb/fa
1029 if a == c:
1030 p = 2.0*m*s; q = 1.0 - s
1031 else:
1032 qa = fa/fc; r = fb/fc
1033 p = s*(2.0*m*qa*(qa - r) - (b - a)*(r - 1.0))
1034 q = (qa - 1.0)*(r - 1.0)*(s - 1.0)
1035 if p > 0.0:
1036 q = -q
1037 else:
1038 p = -p
1039 if 2.0*p < min(3.0*m*q - abs(tol*q), abs(e*q)):
1040 e = d; d = p/q
1041 else:
1042 d = m; e = m
1043 a = b; fa = fb
1044 if abs(d) > tol:
1045 b += d
1046 else:
1047 b += tol if m > 0.0 else -tol
1048 fb = _point(b, P, CH, C, P_O, P_F, eps, out)
1049 if not np.isfinite(fb):
1050 return np.nan
1051 return b
1052
1053
1054@njit(cache=True)
1055def evaluate_core(P, CH, C, P_O, P_F, Pa):
1056 """Pc root, converged state and delivered thrust -> (ok, result vector).
1057
1058 The window and the top-down bracket scan are chamber_solver.solve's (PC_CHOKE_FLOOR_PA,
1059 PC_MIN_TOTAL_DROP_FRAC, _highest_sign_change); the thrust is nozzle.calculate_thrust."""
1060 Q = CH[0]
1061 res = np.full(NR, np.nan)
1062 scratch = np.full(NR, np.nan)
1063 Pc_min = 2.0*101325.0
1064 Pc_max = min(P_O, P_F)*(1.0 - 0.02)
1065 Pc_min = max(Pc_min, P[SV_PCMIN]); Pc_max = min(Pc_max, P[SV_PCMAX])
1066 if Pc_max <= Pc_min:
1067 return False, res
1068 xtol = P[SV_TOL]; rtol = P[SV_TOL]*1e-3
1069 maxit = int(P[SV_MAXIT]) if P[SV_MAXIT] > 0 else 100
1070 eps = Q[Q_EPS]
1071 rmin = _point(Pc_min, P, CH, C, P_O, P_F, eps, scratch)
1072 rmax = _point(Pc_max, P, CH, C, P_O, P_F, eps, scratch)
1073 if not np.isfinite(rmin) or not np.isfinite(rmax):
1074 return False, res
1075 n = 32
1076 lo = 0.0; hi = 0.0; found = False
1077 pb = Pc_max; rb = rmax
1078 for i in range(n - 1, -1, -1):
1079 pa = Pc_min + (Pc_max - Pc_min)*(i/n)
1080 ra = rmin if i == 0 else _point(pa, P, CH, C, P_O, P_F, eps, scratch)
1081 if np.isfinite(ra) and np.isfinite(rb) and _sign(ra) != _sign(rb):
1082 lo = pa; hi = pb; found = True
1083 break
1084 pb = pa; rb = ra
1085 if not found:
1086 return False, res
1087 Pc = _brentq(P, CH, C, P_O, P_F, lo, hi, xtol, rtol, maxit, scratch)
1088 if not np.isfinite(Pc):
1089 return False, res
1090 # the converged state, CEA at the geometry's own Ae/At as the Python final solve does
1091 r = _point(Pc, P, CH, C, P_O, P_F, Q[Q_EPS_CUR], res)
1092 if not np.isfinite(r):
1093 return False, res
1094 res[R_PC] = Pc
1095 # nozzle.calculate_thrust
1096 MR = res[R_MR]
1097 g1 = cea_eval(C[0], C[1], C[2], C[3], C[4], C[5], C[6], C[7], C[8], C[9], MR, Pc, eps)[3]
1098 kappa = _kappa(Q[Q_CR], g1)
1099 P0 = Pc/kappa
1100 cs2, cf2, tc2, gm2, Rg2, Mg2, cfv2 = cea_eval(C[0], C[1], C[2], C[3], C[4], C[5], C[6], C[7],
1101 C[8], C[9], MR, P0, eps)
1102 if not np.isfinite(cfv2):
1103 return False, res
1104 aux3 = CH[24]
1105 ln_r = _aux3(Q, aux3, 0, MR, P0, eps)
1106 Te = _aux3(Q, aux3, 1, MR, P0, eps)
1107 Me = _aux3(Q, aux3, 2, MR, P0, eps)
1108 P_exit = P0/math.exp(ln_r)
1109 v_exit = cs2*(cfv2 - eps*P_exit/P0)
1110 if not (np.isfinite(P_exit) and P_exit > 0 and np.isfinite(Te) and Te > 0
1111 and Me > 1.0 and v_exit > 0):
1112 return False, res
1113 F = Q[Q_ZETA_N]*cfv2*P0*Q[Q_AT] - Pa*Q[Q_AE]
1114 if not np.isfinite(F):
1115 return False, res
1116 mdot = res[R_MDOT]
1117 thr = 2.0/(gm2 + 1.0)
1118 res[R_F] = F
1119 res[R_ISP] = F/(mdot*G0)
1120 res[R_CF] = F/(P0*Q[Q_AT])
1121 res[R_CF_IDEAL] = cfv2 - Pa*eps/P0 if P0 > 0 else cf2
1122 res[R_CF_VAC] = cfv2
1123 res[R_P_EXIT] = P_exit; res[R_T_EXIT] = Te; res[R_M_EXIT] = Me; res[R_V_EXIT] = v_exit
1124 res[R_T_THROAT] = tc2*thr
1125 res[R_P_THROAT] = P0*thr**(gm2/(gm2 - 1.0))
1126 res[R_P0] = P0
1127 if not (res[R_ISP] >= 0.0):
1128 return False, res
1129 return True, res
1130
1131
1132# =============================================================================================
1133# Config -> the chamber tuple (Python)
1134# =============================================================================================
1135_CONTOUR_MEMO: "OrderedDict" = OrderedDict()
1136
1137
1138def _contour(cg):
1139 from engine.pipeline.thermal.gas_side import contour_for
1140 key = (cg.A_throat, cg.chamber_diameter, cg.volume, cg.A_exit)
1141 c = _CONTOUR_MEMO.get(key)
1142 if c is None:
1143 c = contour_for(cg)
1144 _CONTOUR_MEMO[key] = c
1145 if len(_CONTOUR_MEMO) > 64:
1146 _CONTOUR_MEMO.popitem(last=False)
1147 return c
1148
1149
1150def _aux_arrays(cache):
1151 aux = cache.aux
1152 arr = getattr(aux, "_numba_arrays", None)
1153 if arr is None:
1154 t = aux.t
1155 a2 = np.ascontiguousarray(np.stack([t["mu_c"], t["cp_c"], t["Pr_c"], t["x_H2O_c"],
1156 t["x_CO2_c"]]), np.float64)
1157 a3 = np.ascontiguousarray(np.stack([t["lnPcPe"], t["Te"], t["Me"]]), np.float64)
1158 arr = (a2, a3, (float(aux.MR[0]), float(aux.MR[-1]), len(aux.MR),
1159 float(aux.Pc[0]), float(aux.Pc[-1]), len(aux.Pc),
1160 float(aux.eps[0]), float(aux.eps[-1]), len(aux.eps)))
1161 try:
1162 aux._numba_arrays = arr
1163 except Exception:
1164 pass
1165 return arr
1166
1167
1168def _wide_arrays(cache):
1169 from engine.pipeline.combustion_physics import get_cstar_wide_table
1170 cfg = getattr(cache, "config", None)
1171 if cfg is None or not getattr(cfg, "ox_name", None) or not getattr(cfg, "fuel_name", None):
1172 return None
1173 wide = get_cstar_wide_table(cfg.ox_name, cfg.fuel_name)
1174 if wide is None:
1175 return None
1176 arr = getattr(wide, "_numba_arrays", None)
1177 if arr is None:
1178 arr = (np.ascontiguousarray(wide._lnMR, np.float64), np.ascontiguousarray(wide._lnPc, np.float64),
1179 np.ascontiguousarray(wide.cstar, np.float64),
1180 np.array([float(wide.MR_min), float(wide.MR_max)]))
1181 try:
1182 wide._numba_arrays = arr
1183 except Exception:
1184 pass
1185 return arr
1186
1187
1188def chamber_inputs(config, cache):
1189 """The chamber tuple for evaluate_core, or None when a piece of the Python chamber solve is
1190 not mirrored here (the caller then runs Python)."""
1191 from engine.core.nozzle import contraction_ratio_of
1192 from engine.pipeline.config_schemas import ensure_chamber_geometry
1193 from engine.pipeline.thermal.ablative_cooling import ablation_energy_per_mass
1194
1195 cg = ensure_chamber_geometry(config)
1196 eff = config.combustion.efficiency
1197 wide = _wide_arrays(cache)
1198 if wide is None:
1199 return None # c* would come from the design cache (not mirrored)
1200 dist = str(eff.mixing_distribution)
1201 blowing = str(getattr(eff, "droplet_blowing_model", "abramzon_sirignano"))
1202 if dist not in _UNIT_MAD or blowing not in ("abramzon_sirignano", "none"):
1203 return None
1204 z, wz = _UNIT_MAD[dist]
1205 q = float(eff.spray_size_spread_q)
1206 Q = np.zeros(NQ)
1207 Q[_Q["AT"]] = float(cg.A_throat)
1208 Q[_Q["AE"]] = float(cg.A_exit)
1209 Q[_Q["EPS"]] = float(cg.expansion_ratio)
1210 Q[_Q["EPS_CUR"]] = (float(cg.A_exit / cg.A_throat) if cg.A_throat and cg.A_exit and cg.A_throat > 0
1211 else float(cg.expansion_ratio))
1212 Q[_Q["LSTAR"]] = float(cg.Lstar) if cg.Lstar is not None else float(cg.volume / cg.A_throat)
1213 D = cg.chamber_diameter
1214 if (D is None or D <= 0) and config.regen_cooling is not None and config.regen_cooling.chamber_inner_diameter:
1215 D = config.regen_cooling.chamber_inner_diameter
1216 if D is None or D <= 0:
1217 D = 0.08
1218 D = max(D, 1e-6)
1219 Q[_Q["AC"]] = float(np.pi * (D / 2.0) ** 2)
1220 CR = contraction_ratio_of(cg)
1221 Q[_Q["CR"]] = float(CR) if CR is not None else 0.0
1222 Q[_Q["ZETA_N"]] = float(cg.nozzle_efficiency)
1223 Q[_Q["EFF_MODEL"]] = {"constant": 0.0, "linear": 1.0}.get(str(eff.model), 2.0)
1224 Q[_Q["EFF_C"]] = float(eff.C)
1225 Q[_Q["RR_Q"]] = q
1226 Q[_Q["RR_XSCALE"]] = math.gamma(1.0 - 1.0 / q) if q > 1.0 else np.nan
1227 Q[_Q["BLOW_AS"]] = 1.0 if blowing == "abramzon_sirignano" else 0.0
1228 Q[_Q["EM_OPT"]] = float(eff.rupe_Em_opt)
1229 Q[_Q["M_OPT"]] = float(eff.rupe_M_opt)
1230 Q[_Q["EM_CURV"]] = float(eff.rupe_Em_curvature)
1231 Q[_Q["USE_COUP"]] = 1.0 if eff.use_cooling_coupling else 0.0
1232 Q[_Q["IMP"]] = 1.0 if config.injector.type == "impinging" else 0.0
1233
1234 tables = []
1235 for pre, side in (("SO", "oxidizer"), ("SF", "fuel")):
1236 fl = config.fluids[side]
1237 g = lambda k: getattr(fl, k, None) # noqa: E731
1238 dens, Tb, Lb, MW = g("density"), g("boiling_point"), g("latent_heat"), g("molecular_weight")
1239 ok = bool(dens and Tb and Lb and MW)
1240 Q[_Q[f"{pre}_OK"]] = 1.0 if ok else 0.0
1241 Q[_Q[f"{pre}_RHO"]] = float(dens or 0.0)
1242 Q[_Q[f"{pre}_TB"]] = float(Tb or 0.0)
1243 Q[_Q[f"{pre}_LB"]] = float(Lb or 0.0)
1244 Q[_Q[f"{pre}_MW"]] = float(MW or 0.0)
1245 Tcrit = g("critical_temperature") or 0.0
1246 Q[_Q[f"{pre}_TCRIT"]] = float(Tcrit)
1247 T0 = g("temperature")
1248 T0 = 293.0 if T0 is None else float(T0)
1249 Q[_Q[f"{pre}_T0"]] = T0
1250 cp = g("specific_heat")
1251 Q[_Q[f"{pre}_CP"]] = 2000.0 if cp is None else float(cp)
1252 ht = None
1253 if ok and g("name"):
1254 ht = heatup_tables(str(fl.name), T0, float(Tb), float(Lb), float(MW), float(Tcrit))
1255 Q[_Q[f"{pre}_HT"]] = 1.0 if ht is not None else 0.0
1256 tables.append(ht if ht is not None else _NO_HT)
1257
1258 ab = config.ablative_cooling
1259 xs = rs = dA = np.zeros(0)
1260 if ab is not None and ab.enabled:
1261 contour = _contour(cg)
1262 gr = getattr(config, "graphite_insert", None)
1263 if gr is None or not gr.enabled:
1264 x_end = 0.0
1265 else:
1266 x_end = -float(gr.axial_half_length or gr.axial_half_length_ratio * 2.0 * contour.R_t)
1267 lined = (contour.x >= contour.x_face - 1e-12) & (contour.x <= x_end + 1e-12)
1268 xs = np.ascontiguousarray(contour.x[lined], np.float64)
1269 rs = np.ascontiguousarray(contour.r[lined], np.float64)
1270 dA = np.ascontiguousarray(contour.area_elements()[lined]
1271 * float(np.clip(ab.coverage_fraction, 0.0, 1.0)), np.float64)
1272 Ts = float(ab.ablation_surface_temperature)
1273 Q[_Q["AB_ON"]] = 1.0
1274 Q[_Q["AB_TS"]] = Ts
1275 Q[_Q["AB_EW"]] = float(ab.surface_emissivity)
1276 Q[_Q["AB_E"]] = float(ablation_energy_per_mass(ab, Ts))
1277 Q[_Q["AB_BELOW"]] = 1.0 if Ts < ab.pyrolysis_temperature else 0.0
1278 Q[_Q["AB_PHYS"]] = 1.0 if ab.use_physics_based_blowing else 0.0
1279 Q[_Q["AB_BC"]] = float(ab.blowing_coefficient)
1280 Q[_Q["AB_BMIN"]] = float(ab.blowing_min_reduction_factor)
1281 Q[_Q["AB_BEFF"]] = float(np.clip(ab.blowing_efficiency, 0.0, 1.0))
1282 Q[_Q["AB_RT"]] = float(contour.R_t)
1283 Q[_Q["AB_LB"]] = float(contour.beam_length)
1284
1285 a2, a3, g = _aux_arrays(cache)
1286 for k, v in zip(("AX_MR0", "AX_MR1", "AX_NMR", "AX_PC0", "AX_PC1", "AX_NPC", "AX_E0", "AX_E1",
1287 "AX_NE"), g):
1288 Q[_Q[k]] = float(v)
1289
1290 lnMR, lnPc, cs, mrlim = wide
1291 ypow = np.ascontiguousarray(_RR_NODES ** (1.0 / q) if q > 1.0 else _RR_NODES, np.float64)
1292 (hOs, hOc, hOo, tOs, tOc, tOo), (hFs, hFc, hFo, tFs, tFc, tFo) = tables
1293 return (Q, a2, mrlim, lnMR, lnPc, cs, xs, rs, dA,
1294 np.ascontiguousarray(z, np.float64), np.ascontiguousarray(wz, np.float64), ypow,
1295 hOs, hOc, hOo, tOs, tOc, tOo, hFs, hFc, hFo, tFs, tFc, tFo, a3)
1296
1297
1299 """Load (or compile) evaluate_core for the argument types every config produces, so a worker
1300 pool does not pay it inside its first generation. Never raises."""
1301 try:
1302 from engine.accel.params import NP
1303 z1 = np.zeros(2)
1304 CH = (np.zeros(NQ), np.zeros((5, 2, 2)), z1, z1, z1, np.zeros((2, 2)), z1, z1, z1, z1, z1,
1305 z1, _NO_HT[0], _NO_HT[1], _NO_HT[2], _NO_HT[3], _NO_HT[4], _NO_HT[5],
1306 _NO_HT[0], _NO_HT[1], _NO_HT[2], _NO_HT[3], _NO_HT[4], _NO_HT[5],
1307 np.zeros((3, 2, 2, 2)))
1308 t3 = np.ones((2, 2, 2))
1309 C = (z1, z1, z1, t3, t3, t3, t3, t3, t3, t3)
1310 evaluate_core(np.zeros(NP), CH, C, 1.0, 1.0, 1.0)
1311 return True
1312 except Exception:
1313 return False
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
_piecewise(segments, fn, nf, tol_rel, tol_abs)
Definition chamber.py:161
_lk_overlap(pH, pC, L_cm)
Definition chamber.py:787
_interp(x, xp, fp)
Definition chamber.py:464
_mach(ar, gamma, sup)
Definition chamber.py:357
_eta_mix(Q, CH, MR, rupe_M, holes_O, holes_F, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi, out)
Definition chamber.py:688
_aux2(Q, tab, k, MR, Pc)
Definition chamber.py:417
_sat_T(P, Tb, Lb, MW, Tcrit)
Definition chamber.py:91
_spline_block(xs, ys)
Definition chamber.py:97
_brentq(P, CH, C, P_O, P_F, a, b, xtol, rtol, maxit, out)
Definition chamber.py:1002
_cs_of_r(r, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi)
Definition chamber.py:477
_kappa(CR, gamma)
Definition chamber.py:393
_aux3(Q, tab, k, MR, Pc, eps)
Definition chamber.py:426
_eta_vap(Q, CH, Pc, Tc, gamma, R, MR, mdot, D32_O, D32_F, u_O, u_F, u_ax, L_imp, L_b, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi, out)
Definition chamber.py:548
_lk_eps0(c, t, paL)
Definition chamber.py:751
_area_ratio(M, gamma)
Definition chamber.py:351
_lk_species(h2o, T, P_bar, pa_bar, L_cm)
Definition chamber.py:763
evaluate_core(P, CH, C, P_O, P_F, Pa)
Definition chamber.py:1055
_ablative_Q(Q, CH, T0, P0, gamma, G, mu, cp, Pr, xH, xC, gas_mdot)
Definition chamber.py:796
_axis(lo, hi, n, x, log)
Definition chamber.py:403
_wide_row(lnPc, cs, Pc, row)
Definition chamber.py:446
heatup_tables(name, T0, Tb, Lb, MW, Tcrit)
Definition chamber.py:202
_saturation_state(P, Tb, Lb, MW, Tcrit)
Definition chamber.py:327
_pw_find(segs, ok, x)
Definition chamber.py:293
_tab_range(lo, hi, flag, h_target, fn, nf, tol_rel, tol_abs, depth)
Definition chamber.py:109
_cs_of_MR(MR, lnMR, row, MR_lo, MR_hi, r_lo, r_hi, c_lo, c_hi)
Definition chamber.py:489
_rupe_Em_at_M(M, Em_opt, M_opt, curv)
Definition chamber.py:496
_roots(f, a, b, n=800)
Definition chamber.py:181
_heatup(Q, b, HTs, HTc, HTo, TLs, TLc, TLo, Pc, T0, Ts, Tc)
Definition chamber.py:511
_point(Pc, P, CH, C, P_O, P_F, eps_cea, out)
Definition chamber.py:877
chamber_inputs(config, cache)
Definition chamber.py:1188
_pw_val(coef, k, j, dx)
Definition chamber.py:311
static ArmGate g(kMe)