STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
pintle.py
Go to the documentation of this file.
1"""Pintle injector model implementation."""
2
3from __future__ import annotations
4
5from typing import Tuple, Dict, Any
6
7import numpy as np
8
9from engine.pipeline.config_schemas import PintleEngineConfig, PintleInjectorConfig
10from engine.pipeline.feed_loss import delta_p_feed
11from engine.pipeline.thermal.regen_cooling import delta_p_regen_channels
12from engine.core.discharge import cd_from_re, calculate_reynolds_number
13from engine.core.geometry import get_effective_areas, get_hydraulic_diameters
14from engine.core.spray import (
15 momentum_flux_ratio,
16 thrust_momentum_ratio,
17 spray_angle_from_J,
18 spray_angle_from_TMR,
19 weber_number,
20 ohnesorge_number,
21 smd_lefebvre,
22 smd_pintle,
23 tau_evap,
24 xstar,
25 check_spray_constraints,
26)
27
28from . import InjectorModel
30 effective_flow_areas_from_cd,
31 merge_effective_area_warnings,
32)
33
34
36 """Physically accurate pintle injector solver."""
37
38 injector_config: PintleInjectorConfig
39
40 def __init__(self, engine_config: PintleEngineConfig):
41 super().__init__(engine_config)
42 injector_cfg = engine_config.injector
43 if not isinstance(injector_cfg, PintleInjectorConfig):
44 raise TypeError("PintleInjector requires PintleInjectorConfig")
45 self.injector_config = injector_cfg
46
47 def solve(
48 self,
49 P_tank_O: float,
50 P_tank_F: float,
51 Pc: float,
52 ) -> Tuple[float, float, Dict[str, Any]]:
53 config = self.engine_config
54 injector_geom = self.injector_config.geometry
55
56 discharge_O = config.discharge["oxidizer"]
57 discharge_F = config.discharge["fuel"]
58 feed_O = config.feed_system["oxidizer"]
59 feed_F = config.feed_system["fuel"]
60 spray_cfg = config.spray
61 fluids = config.fluids
62
63 rho_O = fluids["oxidizer"].density
64 mu_O = fluids["oxidizer"].viscosity
65 sigma_O = fluids["oxidizer"].surface_tension
66
67 rho_F = fluids["fuel"].density
68 mu_F = fluids["fuel"].viscosity
69 sigma_F = fluids["fuel"].surface_tension
70
71 A_LOX, A_fuel = get_effective_areas(injector_geom)
72 d_hyd_O, d_hyd_F = get_hydraulic_diameters(injector_geom)
73
74 mdot_O_guess = 0.1
75 mdot_F_guess = 0.1
76
77 max_iter = config.solver.closure.max_iterations
78 Cd_reduction = config.solver.closure.Cd_reduction_factor
79
80 Cd_O_eff = discharge_O.Cd_inf
81 Cd_F_eff = discharge_F.Cd_inf
82
83 diagnostics = {
84 "iterations": 0,
85 "constraints_satisfied": False,
86 "violations": [],
87 "J": None,
88 "TMR": None,
89 "theta": None,
90 "We_O": None,
91 "We_F": None,
92 "D32_O": None,
93 "D32_F": None,
94 "x_star": None,
95 "injector_type": "pintle",
96 "turbulence_intensity_O": None,
97 "turbulence_intensity_F": None,
98 "turbulence_length_O": None,
99 "turbulence_length_F": None,
100 "turbulence_intensity_mix": None,
101 }
102
103 mdot_O = mdot_O_guess
104 mdot_F = mdot_F_guess
105
106 for iteration in range(max_iter):
107 if iteration == 0:
108 mdot_O = mdot_O_guess
109 mdot_F = mdot_F_guess
110
111 # Initialize feed losses (will be updated in feed_iter loop)
112 delta_p_feed_O = 0.0
113 delta_p_feed_F = 0.0
114
115 for feed_iter in range(3):
116 # Calculate feed losses with current mass flows
117 # CRITICAL: Recalculate on each iteration to ensure consistency
118 delta_p_feed_O = delta_p_feed(mdot_O, rho_O, feed_O, P_tank_O, mu=mu_O)
119 delta_p_feed_F_base = delta_p_feed(mdot_F, rho_F, feed_F, P_tank_F, mu=mu_F)
120
121 # CRITICAL: Ensure feed loss is calculated - if it's still 0.0 with non-zero flow, something is wrong
122 if delta_p_feed_O == 0.0 and mdot_O > 0.01:
123 import warnings
124 K0_val = feed_O.get('K0', 'N/A') if isinstance(feed_O, dict) else getattr(feed_O, 'K0', 'N/A')
125 warnings.warn(f"[WARNING] LOX feed loss is 0.0 with mdot_O={mdot_O:.4f} kg/s. Check feed system config (K0={K0_val}, K_eff should be > 0).")
126
127 # CRITICAL: Ensure final feed losses are stored (recalculate one more time after loop)
128 delta_p_feed_O = delta_p_feed(mdot_O, rho_O, feed_O, P_tank_O, mu=mu_O)
129 delta_p_feed_F_base = delta_p_feed(mdot_F, rho_F, feed_F, P_tank_F, mu=mu_F)
130 if config.regen_cooling is not None and config.regen_cooling.enabled:
131 delta_p_regen = delta_p_regen_channels(
132 mdot_F,
133 rho_F,
134 mu_F,
135 config.regen_cooling,
136 P_tank_F,
137 )
138 delta_p_feed_F = delta_p_feed_F_base + delta_p_regen
139 else:
140 delta_p_feed_F = delta_p_feed_F_base
141
142 # CRITICAL: Calculate injector pressures AFTER feed loss calculation (always, not just in else block)
143 P_inj_O = P_tank_O - delta_p_feed_O
144 P_inj_F = P_tank_F - delta_p_feed_F
145
146 if feed_iter < 2:
147 delta_p_inj_O = max(0.0, P_inj_O - Pc)
148 delta_p_inj_F = max(0.0, P_inj_F - Pc)
149
150 u_O_quick = np.sqrt(2 * delta_p_inj_O / rho_O) if delta_p_inj_O > 0 else 0.0
151 u_F_quick = np.sqrt(2 * delta_p_inj_F / rho_F) if delta_p_inj_F > 0 else 0.0
152
153 Re_O_quick = calculate_reynolds_number(rho_O, u_O_quick, d_hyd_O, mu_O)
154 Re_F_quick = calculate_reynolds_number(rho_F, u_F_quick, d_hyd_F, mu_F)
155
156 # CRITICAL FIX: Remove hardcoded temperatures - should come from config or fluid properties
157 # LOX is typically at saturation temperature ~90K, RP-1 at ambient ~300K
158 # But these should be configurable, not hardcoded
159 T_tank_O = getattr(fluids["oxidizer"], 'temperature', 90.0) # Use config if available
160 T_tank_F = getattr(fluids["fuel"], 'temperature', 300.0) # Use config if available
161 Cd_O_quick_base = cd_from_re(Re_O_quick, discharge_O, P_inlet=P_inj_O, T_inlet=T_tank_O)
162 Cd_F_quick_base = cd_from_re(Re_F_quick, discharge_F, P_inlet=P_inj_F, T_inlet=T_tank_F)
163 Cd_O_quick = min(Cd_O_quick_base, Cd_O_eff)
164 Cd_F_quick = min(Cd_F_quick_base, Cd_F_eff)
165
166 mdot_O = Cd_O_quick * A_LOX * np.sqrt(2 * rho_O * delta_p_inj_O)
167 mdot_F = Cd_F_quick * A_fuel * np.sqrt(2 * rho_F * delta_p_inj_F)
168
169 delta_p_inj_O = max(0.0, P_inj_O - Pc)
170 delta_p_inj_F = max(0.0, P_inj_F - Pc)
171
172 if P_inj_F < Pc:
173 mdot_F = 0.0
174 if P_inj_O < Pc:
175 mdot_O = 0.0
176
177 u_O = mdot_O / (rho_O * A_LOX) if A_LOX > 0 else 0.0
178 u_F = mdot_F / (rho_F * A_fuel) if A_fuel > 0 else 0.0
179
180 Re_O = calculate_reynolds_number(rho_O, u_O, d_hyd_O, mu_O)
181 Re_F = calculate_reynolds_number(rho_F, u_F, d_hyd_F, mu_F)
182
183 # CRITICAL FIX: Use same temperature values as above, not hardcoded
184 T_tank_O = getattr(fluids["oxidizer"], 'temperature', 90.0)
185 T_tank_F = getattr(fluids["fuel"], 'temperature', 300.0)
186 Cd_O_base = cd_from_re(Re_O, discharge_O, P_inlet=P_inj_O, T_inlet=T_tank_O)
187 Cd_F_base = cd_from_re(Re_F, discharge_F, P_inlet=P_inj_F, T_inlet=T_tank_F)
188 Cd_O = min(Cd_O_base, Cd_O_eff)
189 Cd_F = min(Cd_F_base, Cd_F_eff)
190
191 if delta_p_inj_O > 0:
192 mdot_O = Cd_O * A_LOX * np.sqrt(2 * rho_O * delta_p_inj_O)
193 else:
194 mdot_O = 0.0
195
196 if delta_p_inj_F > 0:
197 mdot_F = Cd_F * A_fuel * np.sqrt(2 * rho_F * delta_p_inj_F)
198 else:
199 mdot_F = 0.0
200
201 u_O = mdot_O / (rho_O * A_LOX) if A_LOX > 0 else 0.0
202 u_F = mdot_F / (rho_F * A_fuel) if A_fuel > 0 else 0.0
203
204 J = momentum_flux_ratio(rho_O, u_O, rho_F, u_F)
205 MR = mdot_O / mdot_F if mdot_F > 0 else np.inf
206 TMR = thrust_momentum_ratio(J, MR)
207
208 if spray_cfg.spray_angle.model == "J":
209 theta = spray_angle_from_J(J, spray_cfg.spray_angle.k, spray_cfg.spray_angle.n)
210 else:
211 theta = spray_angle_from_TMR(TMR)
212
213 We_O = weber_number(rho_O, u_O, injector_geom.lox.d_orifice, sigma_O)
214 We_F = weber_number(rho_F, u_F, d_hyd_F, sigma_F)
215
216 Oh_O = ohnesorge_number(mu_O, rho_O, sigma_O, injector_geom.lox.d_orifice)
217 Oh_F = ohnesorge_number(mu_F, rho_F, sigma_F, d_hyd_F)
218
219 turbulence_intensity_O = 0.16 * (Re_O ** -0.125) if Re_O > 0 else 0.1
220 turbulence_intensity_F = 0.16 * (Re_F ** -0.125) if Re_F > 0 else 0.1
221 turbulence_intensity_O = float(np.clip(turbulence_intensity_O, 0.02, 0.3))
222 turbulence_intensity_F = float(np.clip(turbulence_intensity_F, 0.02, 0.3))
223 turbulence_length_O = 0.07 * d_hyd_O
224 turbulence_length_F = 0.07 * d_hyd_F
225 velocity_total = max(u_O + u_F, 1e-6)
226 turbulence_intensity_mix = (
227 (turbulence_intensity_O * u_O + turbulence_intensity_F * u_F) / velocity_total
228 )
229 turbulence_intensity_mix = float(np.clip(turbulence_intensity_mix, 0.02, 0.35))
230
231 # PHYSICS-BASED PINTLE SMD: Use relative velocity and gap height
232 # ----------------------------------------------------------------
233 # V_rel = sqrt(u_O^2 + u_F^2) (orthogonal 90 deg impingement)
234 # L_open = h_gap
235 V_rel = float(np.sqrt(u_O**2 + u_F**2)) # Magnitude of relative velocity vector
236 L_open = injector_geom.fuel.h_gap
237
238 # Use physics-based correlation: SMD = C * L_open * We_rel^(-n) * (1 + B * Oh_f)^p
239 # We_rel and Oh_f are computed inside smd_pintle using fuel properties (sheet)
240 D32 = smd_pintle(
241 L_open,
242 V_rel,
243 rho_F,
244 mu_F,
245 sigma_F,
246 spray_cfg.pintle.C,
247 spray_cfg.pintle.B,
248 spray_cfg.pintle.n,
249 spray_cfg.pintle.p,
250 )
251
252 # Assign coupled SMD to both streams
253 D32_O = D32
254 D32_F = D32
255
256 if spray_cfg.use_turbulence_corrections:
257 # For pintle physics mode, we typically RELY on V_rel (shear) as the primary mechanism
258 # Turbulence gains might double-count or be less relevant than shear.
259 # Per plan: Bypass or cap generic turbulence corrections for pintle SMD.
260 # We log it but do not apply it to SMD.
261 breakup_multiplier = 1.0 # Disabled for pintle mode
262 else:
263 breakup_multiplier = 1.0
264
265 U_rel = np.sqrt(u_O ** 2 + u_F ** 2)
266 tau_evap_O = tau_evap(D32_O, spray_cfg.evaporation.K)
267 x_star_O = xstar(U_rel, tau_evap_O)
268 x_star_combined = max(x_star_O, xstar(U_rel, tau_evap(D32_F, spray_cfg.evaporation.K)))
269
270 if spray_cfg.use_turbulence_corrections:
271 penetration_multiplier = 1.0 / (1.0 + spray_cfg.turbulence_penetration_gain * turbulence_intensity_mix)
272 penetration_multiplier = float(np.clip(penetration_multiplier, 0.3, 1.0))
273 x_star_combined *= penetration_multiplier
274 else:
275 penetration_multiplier = 1.0
276
277 constraints_ok, violations = check_spray_constraints(We_O, We_F, x_star_combined, spray_cfg)
278
279 # CRITICAL: Recalculate feed losses one final time with converged mass flows
280 # to ensure diagnostics have the correct final values
281 delta_p_feed_O_final = delta_p_feed(mdot_O, rho_O, feed_O, P_tank_O, mu=mu_O)
282 delta_p_feed_F_base_final = delta_p_feed(mdot_F, rho_F, feed_F, P_tank_F, mu=mu_F)
283 if config.regen_cooling is not None and config.regen_cooling.enabled:
284 delta_p_regen_final = delta_p_regen_channels(
285 mdot_F,
286 rho_F,
287 mu_F,
288 config.regen_cooling,
289 P_tank_F,
290 )
291 delta_p_feed_F_final = delta_p_feed_F_base_final + delta_p_regen_final
292 else:
293 delta_p_feed_F_final = delta_p_feed_F_base_final
294
295 diagnostics.update(
296 {
297 "iterations": iteration + 1,
298 "constraints_satisfied": constraints_ok,
299 "violations": violations,
300 "J": J,
301 "TMR": TMR,
302 "theta": theta,
303 "We_O": We_O,
304 "We_F": We_F,
305 "V_rel": V_rel,
306 "L_open": L_open,
307 "D32_O": D32_O,
308 "D32_F": D32_F,
309 "x_star": x_star_combined,
310 "turbulence_intensity_O": turbulence_intensity_O,
311 "turbulence_intensity_F": turbulence_intensity_F,
312 "turbulence_length_O": turbulence_length_O,
313 "turbulence_length_F": turbulence_length_F,
314 "turbulence_intensity_mix": turbulence_intensity_mix,
315 "breakup_multiplier": breakup_multiplier,
316 "penetration_multiplier": penetration_multiplier,
317 # Injector velocities
318 "u_O": float(u_O),
319 "u_F": float(u_F),
320 # Injector pressure diagnostics
321 "P_injector_O": float(P_inj_O),
322 "P_injector_F": float(P_inj_F),
323 "delta_p_injector_O": float(delta_p_inj_O),
324 "delta_p_injector_F": float(delta_p_inj_F),
325 # CRITICAL: Use final calculated feed losses, not loop values
326 "delta_p_feed_O": float(delta_p_feed_O_final),
327 "delta_p_feed_F": float(delta_p_feed_F_final),
328 # Discharge coefficients
329 "Cd_O": float(Cd_O),
330 "Cd_F": float(Cd_F),
331 }
332 )
333
334 if constraints_ok:
335 break
336
337 Cd_O_eff *= Cd_reduction
338 Cd_F_eff *= Cd_reduction
339 Cd_O_eff = max(Cd_O_eff, discharge_O.Cd_min)
340 Cd_F_eff = max(Cd_F_eff, discharge_F.Cd_min)
341
342 A_eff_O, A_eff_F, eff_warns = effective_flow_areas_from_cd(diagnostics, A_LOX, A_fuel)
343 diagnostics["A_geom_O"] = float(A_LOX)
344 diagnostics["A_geom_F"] = float(A_fuel)
345 diagnostics["A_eff_O"] = float(A_eff_O)
346 diagnostics["A_eff_F"] = float(A_eff_F)
347 if eff_warns:
348 merge_effective_area_warnings(diagnostics, eff_warns)
349
350 return mdot_O, mdot_F, diagnostics
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Tuple[float, float, Dict[str, Any]] solve(self, float P_tank_O, float P_tank_F, float Pc)
Definition pintle.py:52
__init__(self, PintleEngineConfig engine_config)
Definition pintle.py:40