STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
params.py
Go to the documentation of this file.
1"""Config -> flat scalar state, in pure Python (no C dependency).
2
3WHY THIS EXISTS
4 The Numba kernels used to read their inputs out of the C `EdEngineState`
5 struct (numba_eval.extract_params called native_injector.build_state), so
6 Numba could not run without the C library loaded. That made the C port
7 impossible to delete. This module reproduces that reconciliation with no
8 ctypes involved.
9
10WHY IT IS A TRANSCRIPTION, NOT A REWRITE
11 `native_injector.build_state` encodes decisions that are NOT recoverable by
12 reading the config schema -- see the inline notes on smd_model and
13 we_corr_max below. Re-deriving "what the config means" from the YAML would
14 silently produce different numbers on some configs. So each field here is a
15 line-for-line transcription of the corresponding `_fill_*` helper, and
16 tests/test_accel_params_match_native_state.py asserts exact equality against
17 the real EdEngineState for every committed config while the C port still
18 exists to compare against.
19
20The returned object mirrors EdEngineState's attribute paths exactly
21(`st.fluid_O.density`, `st.injector.imp_O.d_jet`, ...) so the existing
22param-vector mapping reads from either interchangeably.
23"""
24from __future__ import annotations
25
26import numpy as np
27from operator import attrgetter as _attrgetter
28from types import SimpleNamespace
29
30from engine.core.injectors.layout import effective_discharge
31
32# Enum mappings -- mirror native_injector._PHI / _INJ / _EFF_MODEL.
33_PHI = {"none": 0, "sqrtP": 1, "logP": 2}
34_INJ = {"pintle": 0, "impinging": 1, "coaxial": 2}
35_EFF_MODEL = {"constant": 0, "linear": 1, "exponential": 2}
36
37
38def _ns(**kw):
39 return SimpleNamespace(**kw)
40
41
43 # (resolved by engine.core.injectors.layout.effective_discharge before it gets here)
44 """Mirrors native_injector._fill_discharge.
45
46 When the config declares an orifice INLET geometry, resolve it here and hand the kernel
47 the finished Cd_inf with the diameter scaling switched OFF. The kernel has no notion of
48 inlet treatment, so passing the raw Cd_inf would leave it on the old diameter path while
49 Python used the inlet value -- a fresh Python/kernel divergence of exactly the kind that
50 the x_star evaporation split already cost this codebase 9.7% of thrust.
51 """
52 from engine.core.discharge import cd_inf_from_inlet_geometry, lichtarowicz_re_inputs
53 _cd_inlet = cd_inf_from_inlet_geometry(c)
54 _lich = lichtarowicz_re_inputs(c)
55 _lod, _beta, _cdu = (0.0, 0.0, 0.0) if _lich is None else (
56 _lich[1], float(_lich[2] or 0.0), _lich[0])
57 _cd_inf = float(_cd_inlet) if _cd_inlet is not None else float(c.Cd_inf)
58 _use_geom = 0 if _cd_inlet is not None else int(bool(c.use_geometry_cd))
59 return _ns(
60 Cd_inf=_cd_inf, a_Re=float(c.a_Re), Cd_min=float(c.Cd_min),
61 use_geometry_cd=_use_geom,
62 d_ref_m=float(c.d_ref_m), d_min_m=float(c.d_min_m),
63 cd_small_hole_exponent=float(c.cd_small_hole_exponent),
64 cd_large_hole_log_gain=float(c.cd_large_hole_log_gain),
65 cd_inf_max=float(c.cd_inf_max), cd_inf_min_geom=float(c.cd_inf_min_geom),
66 use_pressure_correction=int(bool(c.use_pressure_correction)),
67 P_ref=float(c.P_ref), a_P=float(c.a_P),
68 use_temperature_correction=int(bool(c.use_temperature_correction)),
69 T_ref=float(c.T_ref), a_T=float(c.a_T),
70 lich_l_over_d=_lod, lich_beta=_beta, lich_cd_u=_cdu,
71 )
72
73
75 from engine.pipeline.feed_loss import fittings_K
76 return fittings_K(c)
77
78
79def _feed(c):
80 """Mirrors native_injector._fill_feed."""
81 return _ns(
82 d_inlet=float(getattr(c, "d_inlet", 0.0) or 0.0),
83 A_hydraulic=float(getattr(c, "A_hydraulic", 0.0) or 0.0),
84 # Itemised fittings add to K0 on the line velocity head (feed_loss.fittings_K, same
85 # summation order); no fittings => K0 exactly. roughness_m configs never get here
86 # (accel.can_handle hands them to Python).
87 K0=(float(c.K0) + _feed_fittings_K(c)) if getattr(c, "fittings", None) else float(c.K0),
88 K1=float(c.K1), phi_type=_PHI[c.phi_type],
89 # Exit dump into the manifold (feed_loss.delta_p_feed): same falsy reads as Python.
90 K_exit=float(getattr(c, "K_exit", 0.0) or 0.0),
91 d_exit=float(getattr(c, "d_exit", None) or 0.0),
92 )
93
94
95def _cav(config):
96 """Vena-contracta Cc per stream for the Nurick cavitation limit (discharge.contraction_coefficient)."""
97 from engine.core.discharge import contraction_coefficient, inlet_radius_ratio_of
98 from engine.core.injectors.layout import effective_discharge
99 return _ns(cc_O=contraction_coefficient(inlet_radius_ratio_of(effective_discharge(config, "oxidizer"))),
100 cc_F=contraction_coefficient(inlet_radius_ratio_of(effective_discharge(config, "fuel"))))
101
102
103def _fluid(f):
104 """Mirrors native_injector._fill_fluid.
105
106 Note the `or` defaults: a missing OR zero temperature becomes 0.0, and a
107 missing OR zero latent_heat becomes 300e3. Falsy checks, not `is None`.
108 """
109 return _ns(
110 density=float(f.density), viscosity=float(f.viscosity),
111 surface_tension=float(f.surface_tension),
112 temperature=float(getattr(f, "temperature", 0.0) or 0.0),
113 latent_heat=float(getattr(f, "latent_heat", 300e3) or 300e3),
114 # Boiling point feeds the Spalding transfer number B_M in the derived evaporation
115 # constant. 0 => the kernel falls back to the legacy fixed-K path for that stream.
116 boiling_point=float(getattr(f, "boiling_point", 0.0) or 0.0),
117 # Cavitation limit (impinging._stream_flow): vapour pressure; NaN => no limit.
118 vapor_pressure=(float(f.vapor_pressure) if getattr(f, "vapor_pressure", None) is not None
119 else float("nan")),
120 )
121
122
123def _comb(eff):
124 """Mirrors native_injector._fill_comb."""
125 floor = getattr(eff, "tau_Tc_floor_K", None)
126 ropt = getattr(eff, "R_opt", None)
127 return _ns(
128 model=_EFF_MODEL.get(eff.model, 2),
129 C=float(eff.C), K=float(eff.K),
130 cooling_efficiency_floor=float(eff.cooling_efficiency_floor),
131 use_cooling_coupling=int(bool(eff.use_cooling_coupling)),
132 tau_ref=float(eff.tau_ref), tau_ref_P=float(eff.tau_ref_P),
133 tau_ref_T=float(eff.tau_ref_T), n_pressure=float(eff.n_pressure),
134 T_star_fuel_cap_K=float(getattr(eff, "T_star_fuel_cap_K", 1000.0)),
135 has_tau_Tc_floor=int(floor is not None),
136 tau_Tc_floor=float(floor or 0.0),
137 # Rupe momentum-ratio mixing; R_opt<=0 => derive from impingement angles.
138 Em_peak=float(getattr(eff, "Em_peak", 0.96)),
139 mixing_sigma=float(getattr(eff, "mixing_sigma", 1.5)),
140 R_opt=float(ropt) if ropt is not None else 0.0,
141 )
142
143
144def _cooling(cfg):
145 """Mirrors native_injector._fill_cooling.
146
147 The C struct is zero-initialised, so fields guarded by `if rg is not None` /
148 `if ab is not None` stay 0.0 when that config block is absent. The defaults
149 below reproduce that -- do not "improve" them into schema defaults.
150 """
151 rg, ab = cfg.regen_cooling, cfg.ablative_cooling
152 fc = getattr(cfg, "film_cooling", None)
153 eff = cfg.combustion.efficiency
154 c = _ns(
155 regen_enabled=int(bool(rg and rg.enabled)),
156 film_enabled=int(bool(fc and fc.enabled)),
157 ablative_enabled=int(bool(ab and ab.enabled)),
158 graphite_enabled=int(bool(getattr(cfg, "graphite_insert", None)
159 and cfg.graphite_insert.enabled)),
160 use_cooling_coupling=int(bool(eff.use_cooling_coupling)),
161 cooling_efficiency_floor=float(eff.cooling_efficiency_floor),
162 # zero-init defaults for the two optional blocks
163 hot_gas_viscosity=0.0, hot_gas_thermal_conductivity=0.0,
164 hot_gas_prandtl=0.0, gas_turbulence_intensity=0.0, recovery_factor=0.0,
165 radiation_emissivity_hot=0.0, radiation_view_factor=0.0,
166 regen_chamber_inner_diameter=0.0,
167 ablative_coverage_fraction=0.0, ablative_surface_temperature_limit=0.0,
168 ablative_material_density=0.0, ablative_heat_of_ablation=0.0,
169 ablative_specific_heat=0.0, ablative_pyrolysis_temperature=0.0,
170 ablative_use_physics_based_blowing=0, ablative_blowing_efficiency=0.0,
171 ablative_blowing_coefficient=0.0, ablative_blowing_min_reduction_factor=0.0,
172 ablative_turbulence_reference_intensity=0.0, ablative_turbulence_sensitivity=0.0,
173 ablative_turbulence_exponent=0.0, ablative_turbulence_max_multiplier=0.0,
174 ablative_surface_emissivity=0.0, ablative_ambient_temperature=0.0,
175 ablative_radiative_sink_minimum_threshold=0.0,
176 ablative_radiative_sink_fallback_temperature=0.0,
177 )
178 if rg is not None:
179 c.hot_gas_viscosity = float(rg.hot_gas_viscosity)
180 c.hot_gas_thermal_conductivity = float(rg.hot_gas_thermal_conductivity)
181 c.hot_gas_prandtl = float(rg.hot_gas_prandtl)
182 c.gas_turbulence_intensity = float(rg.gas_turbulence_intensity)
183 c.recovery_factor = float(rg.recovery_factor) if rg.recovery_factor is not None else 0.94
184 c.radiation_emissivity_hot = float(rg.radiation_emissivity_hot)
185 c.radiation_view_factor = float(rg.radiation_view_factor)
186 c.regen_chamber_inner_diameter = float(rg.chamber_inner_diameter or 0.0)
187 if ab is not None:
188 c.ablative_coverage_fraction = float(ab.coverage_fraction)
189 c.ablative_surface_temperature_limit = float(ab.surface_temperature_limit)
190 c.ablative_material_density = float(ab.material_density)
191 c.ablative_heat_of_ablation = float(ab.heat_of_ablation)
192 c.ablative_specific_heat = float(ab.specific_heat)
193 c.ablative_pyrolysis_temperature = float(ab.pyrolysis_temperature)
194 c.ablative_use_physics_based_blowing = int(bool(ab.use_physics_based_blowing))
195 c.ablative_blowing_efficiency = float(ab.blowing_efficiency)
196 c.ablative_blowing_coefficient = float(ab.blowing_coefficient)
197 c.ablative_blowing_min_reduction_factor = float(ab.blowing_min_reduction_factor)
198 c.ablative_turbulence_reference_intensity = float(ab.turbulence_reference_intensity)
199 c.ablative_turbulence_sensitivity = float(ab.turbulence_sensitivity)
200 c.ablative_turbulence_exponent = float(ab.turbulence_exponent)
201 c.ablative_turbulence_max_multiplier = float(ab.turbulence_max_multiplier)
202 c.ablative_surface_emissivity = float(ab.surface_emissivity)
203 c.ablative_ambient_temperature = float(ab.ambient_temperature)
204 c.ablative_radiative_sink_minimum_threshold = float(ab.radiative_sink_minimum_threshold)
205 c.ablative_radiative_sink_fallback_temperature = float(ab.radiative_sink_fallback_temperature)
206 return c
207
208
209def _geom(cg):
210 """Mirrors native_injector._fill_geom. `cg` must come from ensure_chamber_geometry."""
211 return _ns(
212 A_throat=float(cg.A_throat), A_exit=float(cg.A_exit), volume=float(cg.volume),
213 Lstar=float(cg.Lstar) if cg.Lstar else 0.0,
214 length=float(cg.length),
215 length_cylindrical=float(cg.length_cylindrical or 0.0),
216 length_contraction=float(cg.length_contraction or 0.0),
217 chamber_diameter=float(cg.chamber_diameter or 0.0),
218 exit_diameter=float(cg.exit_diameter or 0.0),
219 expansion_ratio=float(cg.expansion_ratio),
220 nozzle_efficiency=float(cg.nozzle_efficiency),
221 Cf=float(cg.Cf or 0.0),
222 design_pressure=float(cg.design_pressure or 0.0),
223 )
224
225
226def _pintle(geom):
227 """Pintle geometry, flattened.
228
229 The area/hydraulic-diameter maths lives in engine.core.geometry and is called
230 HERE, at the boundary, so the kernel sees scalars and the two implementations
231 cannot drift on geometry.
232 """
233 from engine.core.geometry import get_effective_areas, get_hydraulic_diameters
234 A_O, A_F = get_effective_areas(geom)
235 dh_O, dh_F = get_hydraulic_diameters(geom)
236 return _ns(A_O=float(A_O), A_F=float(A_F),
237 d_hyd_O=float(dh_O), d_hyd_F=float(dh_F),
238 d_orifice=float(geom.lox.d_orifice),
239 h_gap=float(geom.fuel.h_gap),
240 d_pintle_tip=float(geom.fuel.d_pintle_tip))
241
242
243_PIN_ZERO = _ns(A_O=0.0, A_F=0.0, d_hyd_O=0.0, d_hyd_F=0.0, d_orifice=0.0, h_gap=0.0,
244 d_pintle_tip=0.0)
245
246
247def _imp(b):
248 """Mirrors native_injector._fill_imp."""
249 return _ns(
250 n_elements=int(b.n_elements), d_jet=float(b.d_jet),
251 impingement_angle=float(b.impingement_angle),
252 spacing=float(getattr(b, "spacing", 0.0) or 0.0),
253 )
254
255
256_NET_ZERO = _ns(enabled=0, n_ports=0, h=0, A_ch=0.0, D_h=0.0, s=0.0, eps=0.0, scale=0.0,
257 A_hole=0.0, d_hole=0.0, K_ent=0.0, C_R=0.0)
258
259
260def _net(config, side):
261 """The stream's back-channel ring (impinging._RingManifold), read off the very object the
262 Python injector builds, so the kernel marches the same branch; zeros for a plenum."""
263 from engine.core.injectors.impinging import _ring_manifold_for
264 fl = config.fluids[side]
265 b = getattr(config.injector.geometry, side)
266 net = _ring_manifold_for(config, side, float(fl.density), float(fl.viscosity),
267 b.n_elements, b.d_jet)
268 if net is None:
269 return _NET_ZERO
270 return _ns(enabled=1, n_ports=int(net.n_ports), h=int(net.h), A_ch=float(net.A_ch),
271 D_h=float(net.D_h), s=float(net.s), eps=float(net.eps), scale=float(net.scale),
272 A_hole=float(net.A_hole), d_hole=float(net.d_hole), K_ent=float(net.K_ent),
273 C_R=float(net.C_R))
274
275
276def build_state(config):
277 """Pure-Python equivalent of native_injector.build_state.
278
279 Returns a namespace with EdEngineState's attribute paths. Raises the same way
280 build_state does on a config it cannot map (e.g. a pintle config, whose
281 injector geometry has no .oxidizer/.fuel impinging branches).
282 """
283 from engine.pipeline.config_schemas import ensure_chamber_geometry
284
285 g = config.injector.geometry
286 sp = config.spray
287 inj_type = _INJ[config.injector.type]
288
289 # Only one geometry block is populated; the other stays zero. The kernels
290 # branch on injector.type, so the unused half is never read.
291 if inj_type == _INJ["impinging"]:
292 imp_O, imp_F, pin = _imp(g.oxidizer), _imp(g.fuel), _PIN_ZERO
293 net_O, net_F = _net(config, "oxidizer"), _net(config, "fuel")
294 elif inj_type == _INJ["pintle"]:
295 _z = _ns(n_elements=0, d_jet=0.0, impingement_angle=0.0, spacing=0.0)
296 imp_O, imp_F, pin = _z, _z, _pintle(g)
297 net_O = net_F = _NET_ZERO
298 else:
299 raise NotImplementedError(f"injector type {config.injector.type!r} not ported")
300
301 spray = _ns(
302 # Impinging atomization is ALWAYS Ingebo (see impinging.py); a stale
303 # `lefebvre` in YAML is deliberately ignored. Reading spray.smd.model
304 # here instead would change results on configs that carry the stale key.
305 smd_model=1,
306 smd_C=float(sp.smd.C), smd_m=float(sp.smd.m), smd_p=float(sp.smd.p),
307 smd_scale=float(getattr(sp.smd, "smd_scale", 1.0) or 1.0),
308 smd_property_scaling=float(bool(getattr(sp.smd, "smd_property_scaling", True))),
309 # Falsy check, not `is None`: we_corr_max of 0.0 and null both -> 0.0.
310 smd_we_corr_max=float(getattr(sp.smd, "we_corr_max", None))
311 if getattr(sp.smd, "we_corr_max", None) else 0.0,
312 use_turbulence_corrections=int(bool(getattr(sp, "use_turbulence_corrections", False))),
313 turbulence_penetration_gain=float(getattr(sp, "turbulence_penetration_gain", 0.0) or 0.0),
314 pintle_C=float(sp.pintle.C), pintle_B=float(sp.pintle.B),
315 pintle_n=float(sp.pintle.n), pintle_p=float(sp.pintle.p),
316 chamber_gas_R=float(sp.smd.chamber_gas_R),
317 chamber_gas_T=float(sp.smd.chamber_gas_T),
318 spray_angle_model=0 if sp.spray_angle.model == "J" else 1,
319 spray_angle_k=float(sp.spray_angle.k), spray_angle_n=float(sp.spray_angle.n),
320 we_min=float(sp.weber.get("We_min", 15.0)),
321 evap_K=float(sp.evaporation.K),
322 evap_x_star_limit=float(sp.evaporation.x_star_limit),
323 evap_use_constraint=int(bool(sp.evaporation.use_constraint)),
324 evap_model_code=1 if getattr(sp.evaporation, "model", "derived") == "derived" else 0,
325 evap_C_evap=float(getattr(sp.evaporation, "C_evap", 1.562)),
326 evap_cp_gas=float(getattr(sp.evaporation, "cp_gas", 2200.0)),
327 evap_apply_tau_res=1.0 if getattr(sp.evaporation, "apply_tau_res_correction", False) else 0.0,
328 # measurements.d32_*_um (impinging.py replaces the TN 4222 D32 where it was measured)
329 d32_measured_O=float(getattr(sp.smd, "d32_measured_O", None) or 0.0),
330 d32_measured_F=float(getattr(sp.smd, "d32_measured_F", None) or 0.0),
331 )
332
333 solver = _ns(
334 closure_max_iterations=int(config.solver.closure.max_iterations),
335 closure_Cd_reduction_factor=float(config.solver.closure.Cd_reduction_factor),
336 Pc_min_bound=float(config.solver.Pc_bounds[0]),
337 Pc_max_bound=float(config.solver.Pc_bounds[1]),
338 tolerance=float(config.solver.tolerance),
339 max_iterations=int(config.solver.max_iterations),
340 )
341
342 return _ns(
343 injector=_ns(type=inj_type, imp_O=imp_O, imp_F=imp_F),
344 pin=pin,
345 net_O=net_O, net_F=net_F,
346 # Same resolution the Python injector uses, so Cd cannot diverge between the paths.
347 discharge_O=_discharge(effective_discharge(config, "oxidizer")),
348 discharge_F=_discharge(effective_discharge(config, "fuel")),
349 cav=_cav(config),
350 feed_O=_feed(config.feed_system["oxidizer"]),
351 feed_F=_feed(config.feed_system["fuel"]),
352 fluid_O=_fluid(config.fluids["oxidizer"]),
353 fluid_F=_fluid(config.fluids["fuel"]),
354 spray=spray,
355 solver=solver,
356 comb=_comb(config.combustion.efficiency),
357 cooling=_cooling(config),
358 geom=_geom(ensure_chamber_geometry(config)),
359 )
360
361
362# ---------------------------------------------------------------------------
363# Parameter-vector layout
364#
365# The kernels take one flat float64 array rather than a struct, so every scalar
366# needs a stable index. _NAMES is that layout and the single source of truth for
367# it; kernels.py pulls these names into its module globals so @njit code can use
368# them as compile-time constants (P[G_AT] and friends).
369#
370# APPEND ONLY. Inserting a name shifts every index after it, which silently
371# invalidates the cached .nbc artifacts compiled against the old layout.
372# ---------------------------------------------------------------------------
373# ---- parameter vector layout (mirrors the fields the C residual reads) -------
374_NAMES = [
375 # fluids
376 "RHO_O", "MU_O", "SIG_O", "T_O",
377 "RHO_F", "MU_F", "SIG_F", "T_F", "LAT_F",
378 # injector geom
379 "DJO", "DJF", "NO", "NF", "ANG_O", "ANG_F",
380 # discharge O (16)
381 "DO_CDINF", "DO_ARE", "DO_CDMIN", "DO_GEOM", "DO_DREF", "DO_DMIN", "DO_EXPS",
382 "DO_LOGG", "DO_CDMAX", "DO_CDFLOOR", "DO_UPC", "DO_PREF", "DO_AP", "DO_UTC", "DO_TREF", "DO_AT",
383 # discharge F (16)
384 "DF_CDINF", "DF_ARE", "DF_CDMIN", "DF_GEOM", "DF_DREF", "DF_DMIN", "DF_EXPS",
385 "DF_LOGG", "DF_CDMAX", "DF_CDFLOOR", "DF_UPC", "DF_PREF", "DF_AP", "DF_UTC", "DF_TREF", "DF_AT",
386 # feed O / F
387 "FO_DIN", "FO_AH", "FO_K0", "FO_K1", "FO_PHI",
388 "FF_DIN", "FF_AH", "FF_K0", "FF_K1", "FF_PHI",
389 # spray
390 "SP_SMDMODEL", "SP_SMDC", "SP_SMDM", "SP_SMDP", "SP_SMDSCALE", "SP_SMDWECORR",
391 "SP_GASR", "SP_GAST", "SP_ANGMODEL", "SP_ANGK", "SP_ANGN", "SP_WEMIN",
392 "SP_EVAPK", "SP_EVAPXLIM", "SP_EVAPUSE",
393 # solver
394 "SV_CLMAX", "SV_CLCDRED", "SV_PCMIN", "SV_PCMAX", "SV_TOL", "SV_MAXIT",
395 # geom
396 "G_EPS", "G_AT", "G_AE", "G_VOL", "G_LSTAR", "G_DCHAM", "G_NOZZEFF",
397 # combustion
398 "C_MODEL", "C_C", "C_TAUREF", "C_TAUREFP", "C_TAUREFT", "C_NPRESS", "C_TSTARCAP",
399 "C_HASFLOOR", "C_TAUFLOOR", "C_EMPEAK", "C_SIGMA", "C_ROPT",
400 # chamber lengths (ablative wetted area only)
401 "G_LEN", "G_LCYL", "G_LCONTR",
402 # cooling gates + hot-gas block (ed_cooling.c)
403 "K_ABLEN", "K_FILMEN", "K_REGENEN", "K_USECOUP", "K_EFFFLOOR",
404 "K_HGMU", "K_HGK", "K_HGPR", "K_TI", "K_RECOV", "K_EMISHOT", "K_VIEWF", "K_DREGEN",
405 # ablative material / blowing / turbulence / radiative sink
406 "AB_COV", "AB_TSURF", "AB_TPYRO", "AB_HABL", "AB_CP", "AB_USEPHYS",
407 "AB_BLOWEFF", "AB_BLOWC", "AB_BLOWMIN",
408 "AB_TIREF", "AB_TISENS", "AB_TIEXP", "AB_TIMAX",
409 "AB_EMIS", "AB_TAMB", "AB_SINKMIN", "AB_SINKFB",
410 # injector-type discriminator (0=pintle, 1=impinging) -- kernels branch on it
411 "INJ_TYPE",
412 # pintle injector (zero for impinging configs)
413 "PIN_AO", "PIN_AF", "PIN_DHO", "PIN_DHF", "PIN_DORIF", "PIN_HGAP",
414 "PIN_SMDC", "PIN_SMDB", "PIN_SMDN", "PIN_SMDP", "PIN_DTIP",
415 # spray turbulence corrections (used by the pintle path)
416 "SP_USETURB", "SP_PENGAIN",
417 # --- spray-length / evaporation block -------------------------------------------------
418 # Orifice SPACING was extracted into the namespace but never made it into this vector, so
419 # the kernel could not compute the doublet ring offset and therefore could not compute
420 # L_imp, the spray length, or an effective residence time. Everything downstream of the
421 # impingement point was invisible to the accelerated path -- which is what Layer 1 runs on.
422 # Appended at the END: every pre-existing index is unchanged.
423 "SPO", "SPF",
424 "RHO_O_BOIL", "RHO_F_BOIL", # boiling points, for the Spalding transfer number
425 "LAT_O", # oxidiser latent heat (LAT_F already present)
426 "EV_MODEL", "EV_CEVAP", "EV_CPGAS", "EV_APPLY_TAURES",
427 # feed-line exit dump (K_exit on the d_exit bore; feed_loss.delta_p_feed)
428 "FO_KX", "FO_DEX", "FF_KX", "FF_DEX",
429 # NACA TN 4222 property transfer on/off (spray.smd.smd_property_scaling)
430 "SP_SMDPROP",
431 # Lichtarowicz (1965) Re law (discharge.cd_from_re): L/d (0 => legacy a_Re form), the
432 # counterbore approach beta (0 => plenum-fed), and the orifice's pre-approach Cd_u.
433 "DO_LOD", "DO_BETA", "DO_CDU", "DF_LOD", "DF_BETA", "DF_CDU",
434 # Nurick cavitation limit: vapour pressure (NaN => none) and contraction coefficient
435 "VP_O", "VP_F", "CC_O", "CC_F",
436 # back-channel ring manifold per stream (impinging._RingManifold; NET_x = 0 => plenum)
437 "NET_O", "NET_O_PORTS", "NET_O_H", "NET_O_ACH", "NET_O_DH", "NET_O_S", "NET_O_EPS",
438 "NET_O_SCALE", "NET_O_AHOLE", "NET_O_DHOLE", "NET_O_KENT", "NET_O_CR",
439 "NET_F", "NET_F_PORTS", "NET_F_H", "NET_F_ACH", "NET_F_DH", "NET_F_S", "NET_F_EPS",
440 "NET_F_SCALE", "NET_F_AHOLE", "NET_F_DHOLE", "NET_F_KENT", "NET_F_CR",
441 # measured D32 [m] (0 => the TN 4222 correlation)
442 "D32M_O", "D32M_F",
443]
444_IDX = {n: i for i, n in enumerate(_NAMES)}
445globals().update(_IDX) # module-level int constants for njit
446NP = len(_NAMES)
447
448
449def _assert_supported(st):
450 """Guard the assumptions that let this port skip cooling / use the impinging path."""
451 assert int(st.injector.type) in (0, 1), "injector type not ported (coaxial)"
452 # Ablative IS ported (see _cooling_evaluate). Film/regen are not -- C refuses
453 # them too (ed_cooling.c:147), so they stay a Python fallback.
454 assert int(getattr(st.cooling, "film_enabled")) == 0 and int(getattr(st.cooling, "regen_enabled")) == 0
455 # No graphite gate, deliberately: C does not check it either (ed_cooling.c
456 # refuses only film/regen at :147), because graphite never enters the chamber
457 # residual -- it lives in the burn/recession path (runner.py), and
458 # chamber_solver.py references it zero times. Gating on it here would reject
459 # configs/canonical/impinging.yaml, which C handles fine.
460
461
462def extract_params(config):
463 """Flatten the config scalars this port needs into a float64 vec.
464
465 Delegates the field mapping to _params_from_state so there is ONE table of
466 field names; the two used to carry independent copies of the whole mapping,
467 which would drift the moment a field was added on one side only.
468 """
469 st = build_state(config) # pure Python -- no C library required
470 _assert_supported(st)
471 return _params_from_state(st)
472
473
474
475# ---------------------------------------------------------------------------
476# state -> param vector
477#
478# The (index, attribute-path) mapping is STATIC, so it is built once at import
479# and frozen into attrgetters. It used to be walked per call with str.split(".")
480# and f-string key lookups, which cost 96 us per extraction -- 6x the cost of
481# building the state itself, and paid on EVERY residual iteration of the fallback
482# path via accel.solve. Same field list, same order; only the lookup is hoisted.
483# ---------------------------------------------------------------------------
484def _build_path_table():
485 paths = {
486 "RHO_O": "fluid_O.density", "MU_O": "fluid_O.viscosity",
487 "SIG_O": "fluid_O.surface_tension", "T_O": "fluid_O.temperature",
488 "RHO_F": "fluid_F.density", "MU_F": "fluid_F.viscosity",
489 "SIG_F": "fluid_F.surface_tension", "T_F": "fluid_F.temperature",
490 "LAT_F": "fluid_F.latent_heat",
491 "VP_O": "fluid_O.vapor_pressure", "VP_F": "fluid_F.vapor_pressure",
492 "CC_O": "cav.cc_O", "CC_F": "cav.cc_F",
493 "LAT_O": "fluid_O.latent_heat",
494 "RHO_O_BOIL": "fluid_O.boiling_point",
495 "RHO_F_BOIL": "fluid_F.boiling_point",
496 "SPO": "injector.imp_O.spacing", "SPF": "injector.imp_F.spacing",
497 "EV_MODEL": "spray.evap_model_code",
498 "EV_CEVAP": "spray.evap_C_evap",
499 "EV_CPGAS": "spray.evap_cp_gas",
500 "EV_APPLY_TAURES": "spray.evap_apply_tau_res",
501 "DJO": "injector.imp_O.d_jet", "DJF": "injector.imp_F.d_jet",
502 "NO": "injector.imp_O.n_elements", "NF": "injector.imp_F.n_elements",
503 "ANG_O": "injector.imp_O.impingement_angle",
504 "ANG_F": "injector.imp_F.impingement_angle",
505 }
506 for pre, side in (("DO", "discharge_O"), ("DF", "discharge_F")):
507 for suf, fld in (("CDINF","Cd_inf"),("ARE","a_Re"),("CDMIN","Cd_min"),("GEOM","use_geometry_cd"),
508 ("DREF","d_ref_m"),("DMIN","d_min_m"),("EXPS","cd_small_hole_exponent"),
509 ("LOGG","cd_large_hole_log_gain"),("CDMAX","cd_inf_max"),("CDFLOOR","cd_inf_min_geom"),
510 ("UPC","use_pressure_correction"),("PREF","P_ref"),("AP","a_P"),
511 ("UTC","use_temperature_correction"),("TREF","T_ref"),("AT","a_T"),
512 ("LOD","lich_l_over_d"),("BETA","lich_beta"),("CDU","lich_cd_u")):
513 paths[f"{pre}_{suf}"] = f"{side}.{fld}"
514 for pre, side in (("FO", "feed_O"), ("FF", "feed_F")):
515 for suf, fld in (("DIN","d_inlet"),("AH","A_hydraulic"),("K0","K0"),("K1","K1"),("PHI","phi_type"),
516 ("KX","K_exit"),("DEX","d_exit")):
517 paths[f"{pre}_{suf}"] = f"{side}.{fld}"
518 for suf, fld in (("SMDMODEL","smd_model"),("SMDC","smd_C"),("SMDM","smd_m"),("SMDP","smd_p"),
519 ("SMDSCALE","smd_scale"),("SMDWECORR","smd_we_corr_max"),("GASR","chamber_gas_R"),
520 ("GAST","chamber_gas_T"),("ANGMODEL","spray_angle_model"),("ANGK","spray_angle_k"),
521 ("ANGN","spray_angle_n"),("WEMIN","we_min"),("EVAPK","evap_K"),
522 ("EVAPXLIM","evap_x_star_limit"),("EVAPUSE","evap_use_constraint")):
523 paths[f"SP_{suf}"] = f"spray.{fld}"
524 for suf, fld in (("CLMAX","closure_max_iterations"),("CLCDRED","closure_Cd_reduction_factor"),
525 ("PCMIN","Pc_min_bound"),("PCMAX","Pc_max_bound"),("TOL","tolerance"),
526 ("MAXIT","max_iterations")):
527 paths[f"SV_{suf}"] = f"solver.{fld}"
528 for suf, fld in (("EPS","expansion_ratio"),("AT","A_throat"),("AE","A_exit"),("VOL","volume"),
529 ("LSTAR","Lstar"),("DCHAM","chamber_diameter"),("NOZZEFF","nozzle_efficiency"),
530 ("LEN","length"),("LCYL","length_cylindrical"),("LCONTR","length_contraction")):
531 paths[f"G_{suf}"] = f"geom.{fld}"
532 for suf, fld in (("MODEL","model"),("C","C"),("TAUREF","tau_ref"),("TAUREFP","tau_ref_P"),
533 ("TAUREFT","tau_ref_T"),("NPRESS","n_pressure"),("TSTARCAP","T_star_fuel_cap_K"),
534 ("HASFLOOR","has_tau_Tc_floor"),("TAUFLOOR","tau_Tc_floor"),("EMPEAK","Em_peak"),
535 ("SIGMA","mixing_sigma"),("ROPT","R_opt")):
536 paths[f"C_{suf}"] = f"comb.{fld}"
537 for name, fld in (("K_ABLEN","ablative_enabled"),("K_FILMEN","film_enabled"),
538 ("K_REGENEN","regen_enabled"),("K_USECOUP","use_cooling_coupling"),
539 ("K_EFFFLOOR","cooling_efficiency_floor"),("K_HGMU","hot_gas_viscosity"),
540 ("K_HGK","hot_gas_thermal_conductivity"),("K_HGPR","hot_gas_prandtl"),
541 ("K_TI","gas_turbulence_intensity"),("K_RECOV","recovery_factor"),
542 ("K_EMISHOT","radiation_emissivity_hot"),("K_VIEWF","radiation_view_factor"),
543 ("K_DREGEN","regen_chamber_inner_diameter"),
544 ("AB_COV","ablative_coverage_fraction"),
545 ("AB_TSURF","ablative_surface_temperature_limit"),
546 ("AB_TPYRO","ablative_pyrolysis_temperature"),
547 ("AB_HABL","ablative_heat_of_ablation"),("AB_CP","ablative_specific_heat"),
548 ("AB_USEPHYS","ablative_use_physics_based_blowing"),
549 ("AB_BLOWEFF","ablative_blowing_efficiency"),
550 ("AB_BLOWC","ablative_blowing_coefficient"),
551 ("AB_BLOWMIN","ablative_blowing_min_reduction_factor"),
552 ("AB_TIREF","ablative_turbulence_reference_intensity"),
553 ("AB_TISENS","ablative_turbulence_sensitivity"),
554 ("AB_TIEXP","ablative_turbulence_exponent"),
555 ("AB_TIMAX","ablative_turbulence_max_multiplier"),
556 ("AB_EMIS","ablative_surface_emissivity"),
557 ("AB_TAMB","ablative_ambient_temperature"),
558 ("AB_SINKMIN","ablative_radiative_sink_minimum_threshold"),
559 ("AB_SINKFB","ablative_radiative_sink_fallback_temperature")):
560 paths[name] = f"cooling.{fld}"
561 paths["INJ_TYPE"] = "injector.type"
562 for suf, fld in (("AO","A_O"),("AF","A_F"),("DHO","d_hyd_O"),("DHF","d_hyd_F"),
563 ("DORIF","d_orifice"),("HGAP","h_gap"),
564 ("DTIP","d_pintle_tip")):
565 paths[f"PIN_{suf}"] = f"pin.{fld}"
566 for name, fld in (("PIN_SMDC","pintle_C"),("PIN_SMDB","pintle_B"),
567 ("PIN_SMDN","pintle_n"),("PIN_SMDP","pintle_p"),
568 ("SP_USETURB","use_turbulence_corrections"),
569 ("SP_PENGAIN","turbulence_penetration_gain"),
570 ("SP_SMDPROP","smd_property_scaling")):
571 paths[name] = f"spray.{fld}"
572 for pre, side in (("NET_O", "net_O"), ("NET_F", "net_F")):
573 paths[pre] = f"{side}.enabled"
574 for suf, fld in (("PORTS", "n_ports"), ("H", "h"), ("ACH", "A_ch"), ("DH", "D_h"),
575 ("S", "s"), ("EPS", "eps"), ("SCALE", "scale"), ("AHOLE", "A_hole"),
576 ("DHOLE", "d_hole"), ("KENT", "K_ent"), ("CR", "C_R")):
577 paths[f"{pre}_{suf}"] = f"{side}.{fld}"
578 paths["D32M_O"] = "spray.d32_measured_O"
579 paths["D32M_F"] = "spray.d32_measured_F"
580 missing = set(_NAMES) - set(paths)
581 assert not missing, f"param(s) with no source path: {sorted(missing)}"
582 return paths
583
584
585_PATHS = _build_path_table()
586_GETTERS = tuple((_IDX[n], _attrgetter(_PATHS[n])) for n in _NAMES)
587
588
589def _params_from_state(st):
590 """Flatten a state namespace (or EdEngineState) into the float64 param vector."""
591 P = np.zeros(NP)
592 for i, get in _GETTERS:
593 P[i] = get(st)
594 return P
_net(config, side)
Definition params.py:260
build_state(config)
Definition params.py:276