1"""Hot-gas side of the chamber and nozzle wall.
3Convection is Bartz, station by station along the real contour (Huzel & Huang eq. 4-13,
4Sutton & Biblarz eq. 8-23), with CEA frozen-composition transport at the stagnation state,
5the exact isentropic area-Mach number and the adiabatic wall temperature of a turbulent
6boundary layer, recovery factor r = Pr^(1/3).
8Radiation is the H2O and CO2 of the burnt gas: a gray gas in a gray enclosure. Gas
9emissivity is Leckner's correlation (Modest, Radiative Heat Transfer, 3rd ed., sec. 11.10,
10Table 11.3), absorptivity at the wall temperature by Hottel's scaling, and the mean beam
11length is 3.6 V/A of the chamber (Modest sec. 10.x) capped at 0.95 D of the local bore.
13No turbulence multiplier and no laminar switch: Bartz is a fit to turbulent rocket-nozzle
14data and already carries the chamber's free-stream turbulence.
17from __future__
import annotations
19from dataclasses
import dataclass
20from typing
import Dict, Optional
30THROAT_RC_OVER_RT = 0.5 * (1.5 + 0.382)
36BEAM_LENGTH_COEFF = 3.6
37CYLINDER_BEAM_LENGTH_OVER_D = 0.95
44 M = np.asarray(M, dtype=float)
45 return (1.0 / M) * ((2.0 / (g + 1.0)) * (1.0 + 0.5 * (g - 1.0) * M * M)) ** ((g + 1.0) / (2.0 * (g - 1.0)))
49 """``mach_from_area_ratio``'s bisection as a scalar loop, for compilation (same 64 halvings)."""
50 out = np.empty(ar.size)
51 for i
in range(ar.size):
52 lo, hi = (1.0, 50.0)
if sup[i]
else (1e-9, 1.0)
55 f = (1.0 / mid) * ((2.0 / (gamma + 1.0)) * (1.0 + 0.5 * (gamma - 1.0) * mid * mid)) ** (
56 (gamma + 1.0) / (2.0 * (gamma - 1.0))) - ar[i]
57 if (f < 0.0)
if sup[i]
else (f > 0.0):
61 out[i] = 1.0
if ar[i] <= 1.0 + 1e-12
else 0.5 * (lo + hi)
66 from numba
import njit
as _njit
67 _mach_bisect = _njit(cache=
True)(_mach_bisect_py)
71_USE_COMPILED_BISECT = _mach_bisect
is not None
75 """Exact inversion of the area-Mach relation on the requested branch (vectorised bisection;
76 A/A* is monotone on each branch). Scalars in, scalar out."""
77 ar = np.atleast_1d(np.asarray(area_ratio, dtype=float))
78 sup = np.broadcast_to(np.atleast_1d(np.asarray(supersonic, dtype=bool)), ar.shape)
79 if _USE_COMPILED_BISECT:
80 M =
_mach_bisect(np.ascontiguousarray(ar.ravel()), np.ascontiguousarray(sup.ravel()),
81 float(gamma)).reshape(ar.shape)
82 return float(M[0])
if np.ndim(area_ratio) == 0
else M
83 lo = np.where(sup, 1.0, 1e-9)
84 hi = np.where(sup, 50.0, 1.0)
89 go_up = np.where(sup, f < 0.0, f > 0.0)
90 lo = np.where(go_up, mid, lo)
91 hi = np.where(go_up, hi, mid)
92 M = np.where(ar <= 1.0 + 1e-12, 1.0, 0.5 * (lo + hi))
93 return float(M[0])
if np.ndim(area_ratio) == 0
else M
97 """(T/T0, P/P0) at Mach M."""
98 t = 1.0 / (1.0 + 0.5 * (gamma - 1.0) * M * M)
99 return t, t ** (gamma / (gamma - 1.0))
105 """Adiabatic wall temperature, turbulent recovery factor r = Pr^(1/3) (H&H eq. 4-10/4-11)."""
106 k = 0.5 * (gamma - 1.0) * M * M
107 r = Pr ** (1.0 / 3.0)
108 return T0 * (1.0 + r * k) / (1.0 + k)
111def bartz_sigma(Tw, T0: float, gamma: float, M, omega: float = BARTZ_OMEGA):
112 """Bartz property-variation correction (H&H eq. 4-14)."""
113 k = 1.0 + 0.5 * (gamma - 1.0) * M * M
114 return 1.0 / ((0.5 * (Tw / T0) * k + 0.5) ** (0.8 - omega / 5.0) * k ** (omega / 5.0))
117def bartz_h(Dt: float, rc: float, mu: float, cp: float, Pr: float, mass_flux_throat: float,
119 """Bartz gas-side coefficient [W/(m^2 K)], SI form of H&H eq. 4-13.
121 ``mass_flux_throat`` is Pc/c* = mdot/At; transport properties at the stagnation state.
123 return (0.026 / Dt ** 0.2 * (mu ** 0.2 * cp / Pr ** 0.6) * mass_flux_throat ** 0.8
124 * (Dt / rc) ** 0.1 * (1.0 / area_ratio) ** 0.9 * sigma)
129_LECKNER_H2O = np.array([[-2.2118, -1.1987, 0.035596],
130 [0.85667, 0.93048, -0.14391],
131 [-0.10838, -0.17156, 0.045915]])
132_LECKNER_CO2 = np.array([[-3.9893, 2.7669, -2.1081, 0.39163],
133 [1.2710, -1.1090, 1.0195, -0.21897],
134 [-0.23678, 0.19731, -0.19544, 0.044644]])
138 """Zero-pressure emissivity, exp(sum_i a_i x^i), a_i = sum_j c_ij t^j, x = log10(paL/1 bar cm)."""
139 t = np.asarray(t, dtype=float)
140 x = np.log10(np.maximum(paL_barcm, 1e-12))
141 a = [sum(c[i, j] * t ** j
for j
in range(c.shape[1]))
for i
in range(3)]
142 return np.exp(a[0] + a[1] * x + a[2] * x * x)
146 t = np.asarray(T, dtype=float) / 1000.0
147 tc = np.minimum(np.asarray(T, dtype=float), LECKNER_T_MAX) / 1000.0
148 paL = np.asarray(pa_bar, dtype=float) * L_cm
151 PE = P_bar + 2.56 * pa_bar / np.sqrt(tc)
152 a = np.where(tc > 0.75, 1.888 - 2.053 * np.log10(tc), 2.144)
155 paL_m = 13.2 * tc * tc
158 PE = P_bar + 0.28 * pa_bar
159 a = 1.0 + 0.1 / tc ** 1.45
162 paL_m = np.where(tc < 0.7, 0.054 / (tc * tc), 0.225 * tc * tc)
163 ratio = 1.0 - (a - 1.0) * (1.0 - PE) / (a + b - 1.0 + PE) * np.exp(
164 -cc * np.log10(paL_m / np.maximum(paL, 1e-12)) ** 2)
165 return np.where(paL > 0.0, e0 * ratio, 0.0)
169 pH = np.asarray(p_H2O_bar, dtype=float)
170 pC = np.asarray(p_CO2_bar, dtype=float)
171 tot = np.maximum(pH + pC, 1e-30)
173 x = np.log10(np.maximum(tot * L_cm, 1e-30))
174 d = (zeta / (10.7 + 101.0 * zeta) - 0.0089 * zeta ** 10.4) * np.maximum(x, 0.0) ** 2.76
175 return np.where((pH > 0) & (pC > 0), d, 0.0)
179 """Total emissivity of an H2O-CO2 mixture (Modest eqs. 11.139-11.143, Table 11.3).
181 Pressures in bar, path length in m. Returns eps, the two species' contributions, the
182 overlap correction and whether the pressure corrections were held at LECKNER_T_MAX.
185 L_cm = np.asarray(L_m, dtype=float) * 100.0
189 return {
"eps": np.maximum(eH + eC - d, 0.0),
"eps_H2O": eH,
"eps_CO2": eC,
"overlap": d,
190 "pressure_correction_clamped": np.asarray(T) > LECKNER_T_MAX}
194 """Absorptivity for wall emission at Tw (Hottel's scaling; Modest eq. 11.144):
195 alpha_i = (Tg/Tw)^n_i eps_i(Tw, p_i L Tw/Tg), n = 0.45 H2O, 0.65 CO2."""
196 s = np.asarray(Tw, dtype=float) / np.asarray(Tg, dtype=float)
197 L_cm = np.asarray(L_m, dtype=float) * 100.0
200 d =
_leckner_overlap(np.asarray(p_H2O_bar) * s, np.asarray(p_CO2_bar) * s, L_cm)
201 return np.maximum((1.0 / s) ** 0.45 * eH + (1.0 / s) ** 0.65 * eC - d, 0.0)
205 """Net gas-to-wall radiation of an isothermal gray gas in a gray enclosure [W/m^2]."""
206 f = eps_w / (1.0 - (1.0 - eps_w) * (1.0 - alpha_g))
207 return f * STEFAN_BOLTZMANN_W_M2_K4 * (eps_g * Tg ** 4 - alpha_g * Tw ** 4)
214 """Gas-side wall radius r(x), throat at x = 0, face at x_face < 0."""
226 """Lateral area of the frustum between consecutive stations, per station (half each side)."""
227 dA_seg = np.pi * (self.
r[1:] + self.
r[:-1]) * np.hypot(np.diff(self.
x), np.diff(self.
r))
228 dA = np.zeros_like(self.
r)
229 dA[:-1] += 0.5 * dA_seg
230 dA[1:] += 0.5 * dA_seg
234def wall_contour(A_throat: float, chamber_diameter: float, volume: float,
235 A_exit: Optional[float] =
None, n_chamber: int = 160, n_nozzle: int = 60,
236 theta: float = np.pi / 4.0) -> WallContour:
237 """The drawn contour: barrel, cone at half-angle theta, 1.5 Rt entrance arc, Rao bell.
239 The barrel length is the one that closes the declared volume (chamber_length_calc), the
240 same construction the geometry generator draws; it is not read from a stored length.
244 R_t = float(np.sqrt(A_throat / np.pi))
245 R_c = 0.5 * float(chamber_diameter)
246 CR = (R_c / R_t) ** 2
247 L_cyl =
max(float(chamber_length_calc(volume, A_throat, CR, theta)), 0.0)
248 r_tan = R_t * (1.0 + 1.5 * (1.0 - np.cos(theta)))
249 x_arc = -1.5 * R_t * np.sin(theta)
250 x_cone = x_arc -
max(R_c - r_tan, 0.0) / np.tan(theta)
251 x_face = x_cone - L_cyl
253 n_cyl =
max(int(n_chamber * 0.5), 8)
254 n_cone =
max(int(n_chamber * 0.25), 6)
255 n_arc =
max(n_chamber - n_cyl - n_cone, 6)
256 xc = np.linspace(x_face, x_cone, n_cyl, endpoint=
False)
257 rc = np.full_like(xc, R_c)
258 xk = np.linspace(x_cone, x_arc, n_cone, endpoint=
False)
259 rk = R_c - (xk - x_cone) * np.tan(theta)
260 ta = np.linspace(-(np.pi / 2.0 + theta), -np.pi / 2.0, n_arc)
261 xa = 1.5 * R_t * np.cos(ta)
262 ra = 1.5 * R_t * np.sin(ta) + 2.5 * R_t
263 x = np.concatenate([xc, xk, xa])
264 r = np.concatenate([rc, rk, ra])
265 if A_exit
is not None and A_exit > A_throat:
266 pts = generate_nozzle(A_throat, A_exit, steps=
max(n_nozzle, 10), theta=theta)[0]
267 keep = pts[:, 0] > 1e-12
268 x = np.concatenate([x, pts[keep, 0]])
269 r = np.concatenate([r, pts[keep, 1]])
270 order = np.argsort(x, kind=
"stable")
271 x, r = x[order], r[order]
272 uniq = np.concatenate([[
True], np.diff(x) > 1e-12])
273 x, r = x[uniq], r[uniq]
277 dA = np.pi * (r[ch][1:] + r[ch][:-1]) * np.hypot(np.diff(x[ch]), np.diff(r[ch]))
278 A_enc = float(dA.sum()) + np.pi * R_c ** 2 + A_throat
279 L_e = BEAM_LENGTH_COEFF * float(volume) / A_enc
280 return WallContour(x=x, r=r, x_face=float(x_face), x_cone_start=float(x_cone),
281 x_arc_start=float(x_arc), R_t=R_t, R_c=R_c, volume=float(volume),
282 beam_length=float(L_e))
286 """wall_contour for a ChamberGeometryConfig."""
287 return wall_contour(cg.A_throat, cg.chamber_diameter, cg.volume, A_exit=cg.A_exit)
294 """Everything the wall sees of the gas: stagnation state, throat mass flux, CEA transport
295 (frozen, chamber) and composition (equilibrium, chamber)."""
299 mass_flux_throat: float
307def hot_gas_state(cea_cache, MR: float, Pc: float, T0: float, gamma: float, mdot: float,
308 A_throat: float) -> HotGasState:
309 """HotGasState from the CEA auxiliary tables at (MR, Pc)."""
311 tr = aux.transport(MR, Pc,
"chamber")
312 comp = aux.composition(MR, Pc,
"chamber")
313 return HotGasState(T0=float(T0), P0=float(Pc), gamma=float(gamma),
314 mass_flux_throat=float(mdot) / float(A_throat), mu=tr[
"mu"], cp=tr[
"cp"],
315 Pr=tr[
"Pr"], x_H2O=comp[
"H2O"], x_CO2=comp[
"CO2"])
318def profile(gas: HotGasState, contour: WallContour, Tw, eps_wall, mask=
None) -> Dict[str, np.ndarray]:
319 """Convective and radiative flux into the wall at every contour station (or ``mask``ed
320 ones), wall at Tw with emissivity eps_wall (scalars or per-station)."""
321 x = contour.x
if mask
is None else contour.x[mask]
322 r = contour.r
if mask
is None else contour.r[mask]
323 Tw = np.broadcast_to(np.asarray(Tw, dtype=float), x.shape).astype(float)
324 ew = np.broadcast_to(np.asarray(eps_wall, dtype=float), x.shape).astype(float)
325 ar = np.maximum((r / contour.R_t) ** 2, 1.0)
327 k = 1.0 + 0.5 * (gas.gamma - 1.0) * M * M
329 tP = tT ** (gas.gamma / (gas.gamma - 1.0))
332 Dt = 2.0 * contour.R_t
333 h =
bartz_h(Dt, THROAT_RC_OVER_RT * contour.R_t, gas.mu, gas.cp, gas.Pr,
334 gas.mass_flux_throat, ar, sigma)
336 P_bar = gas.P0 * tP / 1e5
337 L = np.minimum(contour.beam_length, CYLINDER_BEAM_LENGTH_OVER_D * 2.0 * r)
338 pH, pC = gas.x_H2O * P_bar, gas.x_CO2 * P_bar
342 return {
"x": x,
"r": r,
"area_ratio": ar,
"M": M,
"h": h,
"Taw": Taw,
343 "q_conv": h * (Taw - Tw),
"q_rad": q_rad,
"eps_gas": le[
"eps"],
"alpha_gas": alpha,
344 "T_static": Tg,
"P_static": gas.P0 * tP,
"beam_length": L,
345 "leckner_clamped": le[
"pressure_correction_clamped"]}
348def station_flux(gas: HotGasState, contour: WallContour, x: float, Tw: float, eps_wall: float) -> Dict[str, float]:
349 """profile() at one axial position, radius interpolated on the contour."""
350 r = float(np.interp(x, contour.x, contour.r))
351 one =
WallContour(x=np.array([float(x)]), r=np.array([r]), x_face=contour.x_face,
352 x_cone_start=contour.x_cone_start, x_arc_start=contour.x_arc_start,
353 R_t=contour.R_t, R_c=contour.R_c, volume=contour.volume,
354 beam_length=contour.beam_length)
355 p =
profile(gas, one, Tw, eps_wall)
356 return {k: (float(v[0])
if np.ndim(v)
else v)
for k, v
in p.items()}
np.ndarray area_elements(self)
bartz_h(float Dt, float rc, float mu, float cp, float Pr, float mass_flux_throat, area_ratio, sigma)
_leckner_species(str sp, T, P_bar, pa_bar, L_cm)
Dict[str, np.ndarray] profile(HotGasState gas, WallContour contour, Tw, eps_wall, mask=None)
_leckner_overlap(p_H2O_bar, p_CO2_bar, L_cm)
area_ratio_of_mach(M, gamma)
Dict[str, np.ndarray] leckner_emissivity(T, P_bar, p_H2O_bar, p_CO2_bar, L_m)
gray_enclosure_flux(eps_g, alpha_g, eps_w, Tg, Tw)
_mach_bisect_py(ar, sup, gamma)
recovery_temperature(float T0, float gamma, M, float Pr)
mach_from_area_ratio(area_ratio, float gamma, supersonic)
Dict[str, float] station_flux(HotGasState gas, WallContour contour, float x, float Tw, float eps_wall)
_leckner_eps0(np.ndarray c, t, paL_barcm)
WallContour wall_contour(float A_throat, float chamber_diameter, float volume, Optional[float] A_exit=None, int n_chamber=160, int n_nozzle=60, float theta=np.pi/4.0)
HotGasState hot_gas_state(cea_cache, float MR, float Pc, float T0, float gamma, float mdot, float A_throat)
bartz_sigma(Tw, float T0, float gamma, M, float omega=BARTZ_OMEGA)
gas_absorptivity(Tg, Tw, P_bar, p_H2O_bar, p_CO2_bar, L_m)
static_ratios(M, float gamma)
WallContour contour_for(cg)