STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
reaction_chemistry.py
Go to the documentation of this file.
1"""Finite-rate chemistry and reaction progress modeling for combustion chamber.
2
3This module provides physics-based (not reference-based) models for:
41. Reaction progress tracking through chamber
52. Finite-rate chemistry effects using actual Arrhenius kinetics
63. Species evolution modeling
74. Shifting equilibrium in nozzle based on actual flow conditions
8"""
9
10from __future__ import annotations
11
12from typing import Dict, List, Tuple, Optional, Any
13import numpy as np
14from engine.pipeline.config_schemas import PintleEngineConfig
16 NumericalStability,
17 PhysicalConstraints,
18)
19
20# Universal gas constant [J/(mol·K)]
21R_gas = 8314.462618 # J/(kmol·K) for per-kmol calculations
22
23
24def _fuel_name_for_kinetics(config: PintleEngineConfig) -> str:
25 """Label used for Arrhenius fuel-type branching (matches reaction_rate_constant lists).
26
27 Priority: ``combustion.cea.fuel_name`` -> ``fluids['fuel'].name`` -> generic hydrocarbon ("RP-1").
28 """
29 cea = getattr(getattr(config, "combustion", None), "cea", None)
30 if cea is not None:
31 fn = getattr(cea, "fuel_name", None)
32 if fn:
33 return str(fn)
34 fluids = getattr(config, "fluids", None)
35 if fluids and "fuel" in fluids:
36 fuel = fluids["fuel"]
37 fn = getattr(fuel, "name", None)
38 if fn:
39 return str(fn)
40 return "RP-1"
41
42
43def _fuel_props_for_evaporation(config: PintleEngineConfig) -> Optional[Dict[str, float]]:
44 """Density and boiling point for droplet evaporation time scale."""
45 fluids = getattr(config, "fluids", None)
46 if not fluids or "fuel" not in fluids:
47 return None
48 fuel = fluids["fuel"]
49 bp = getattr(fuel, "boiling_point", None)
50 if bp is None:
51 from engine.pipeline.assumptions import assume
52 bp = assume("kinetics.fuel.boiling_point", 489.0, unit="K",
53 reason=f"fluids.fuel.boiling_point missing for {getattr(fuel, 'name', '?')} (RP-1 value used; set it or load a propellant preset)")
54 return {
55 "density": float(fuel.density),
56 "boiling_point": float(bp),
57 }
58
59
61 residence_time: float,
62 reaction_time_scale: float,
63 initial_progress: float = 0.0,
64) -> float:
65 """
66 Calculate reaction progress based on residence time and reaction kinetics.
67
68 Models chemical reaction progress as first-order kinetics:
69 dX/dt = (1 - X) / τ_chem
70
71 Solution: X(t) = 1 - (1 - X0) × exp(-t / τ)
72
73 Parameters:
74 -----------
75 residence_time : float
76 Time spent in chamber [s] (must be >= 0)
77 reaction_time_scale : float
78 Chemical reaction time scale [s] (must be > 0)
79 initial_progress : float
80 Initial reaction progress (0-1)
81
82 Returns:
83 --------
84 progress : float
85 Reaction progress (0-1), where 1 = complete equilibrium
86
87 Raises:
88 -------
89 ValueError
90 If inputs are invalid (negative time, non-positive time scale, etc.)
91 """
92 # Validate inputs - raise errors rather than clamping
93 if not np.isfinite(residence_time):
94 raise ValueError(f"Non-finite residence_time: {residence_time}")
95 if residence_time < 0:
96 raise ValueError(f"Negative residence_time: {residence_time}")
97
98 if not np.isfinite(reaction_time_scale):
99 raise ValueError(f"Non-finite reaction_time_scale: {reaction_time_scale}")
100 if reaction_time_scale <= 0:
101 raise ValueError(f"Non-positive reaction_time_scale: {reaction_time_scale}")
102
103 if not np.isfinite(initial_progress):
104 raise ValueError(f"Non-finite initial_progress: {initial_progress}")
105 if initial_progress < 0 or initial_progress > 1:
106 raise ValueError(f"Initial_progress out of bounds [0,1]: {initial_progress}")
107
108 # Reaction progress based on first-order kinetics
109 # X = 1 - (1 - X0) × exp(-t / τ)
110 time_ratio = residence_time / reaction_time_scale
111
112 if not np.isfinite(time_ratio):
113 raise ValueError(f"Non-finite time_ratio: {residence_time} / {reaction_time_scale}")
114
115 # Calculate exponential - allow any value (physics will determine)
116 exp_term = np.exp(-time_ratio)
117 if not np.isfinite(exp_term):
118 if time_ratio > 0:
119 exp_term = 0.0 # Complete reaction (t >> τ)
120 else:
121 exp_term = 1.0 # No reaction (t << 0, invalid case)
122
123 progress = 1.0 - (1.0 - initial_progress) * exp_term
124
125 if not np.isfinite(progress):
126 raise ValueError(f"Non-finite progress calculated: residence_time={residence_time}, reaction_time_scale={reaction_time_scale}")
127
128 # Progress is naturally bounded [0, 1] by the exponential form
129 # But don't clamp - if it's outside bounds, that's a physics error
130 if progress < 0 or progress > 1:
131 raise ValueError(f"Progress outside [0,1]: {progress} (this indicates a physics error)")
132
133 return float(progress)
134
135
136_EA_MR_W = 0.10 # half-width [MR] of the smooth blend at the 1.5 / 3.0 boundaries
137
138
139def _ea_mr_factor(MR: float) -> float:
140 """Activation-energy multiplier vs O/F: 1.2 (fuel-rich) -> 1.0 -> 0.9 (oxidizer-rich), C1-smooth."""
141 def smooth(t):
142 t = min(max(t, 0.0), 1.0)
143 return t * t * (3.0 - 2.0 * t)
144 w = _EA_MR_W
145 lo = 1.2 + (1.0 - 1.2) * smooth((MR - (1.5 - w)) / (2.0 * w)) # 1.2 -> 1.0 around 1.5
146 hi = 1.0 + (0.9 - 1.0) * smooth((MR - (3.0 - w)) / (2.0 * w)) # 1.0 -> 0.9 around 3.0
147 return float(lo if MR < 2.25 else hi)
148
149
151 Pc: float,
152 Tc: float,
153 MR: float,
154 fuel_type: str = "RP-1",
155 config: Optional[PintleEngineConfig] = None,
156) -> float:
157 """
158 Calculate chemical reaction rate constant using Arrhenius kinetics.
159
160 Rate constant: k = A × P^n × exp(-Ea / (R_gas × T))
161 Reaction time: τ = 1 / k
162
163 Uses actual Arrhenius parameters for hydrocarbon/LOX combustion:
164 - Pre-exponential factor A depends on fuel type and mixture ratio
165 - Pressure exponent n: typically 0.7-1.0 for gas-phase reactions
166 - Activation energy Ea: depends on fuel complexity
167
168 Parameters:
169 -----------
170 Pc : float
171 Chamber pressure [Pa]
172 Tc : float
173 Chamber temperature [K]
174 MR : float
175 Mixture ratio (O/F)
176 fuel_type : str
177 Fuel type ("RP-1", "Ethanol", "H2", etc.) - affects activation energy
178 config : PintleEngineConfig, optional
179 Engine configuration for kinetics parameters. If None, uses built-in defaults.
180
181 Returns:
182 --------
183 k : float
184 Reaction rate constant [1/s]
185
186 Raises:
187 -------
188 ValueError
189 If inputs are invalid or result is non-physical
190 """
191 # Validate inputs
192 if not np.isfinite(Pc) or Pc <= 0:
193 raise ValueError(f"Invalid pressure: {Pc} Pa")
194 if not np.isfinite(Tc) or Tc <= 0:
195 raise ValueError(f"Invalid temperature: {Tc} K")
196 if not np.isfinite(MR) or MR <= 0:
197 raise ValueError(f"Invalid mixture ratio: {MR}")
198
199 # Get kinetics parameters from config if available, otherwise use built-in defaults
200 if config is not None:
201 eff_cfg = config.combustion.efficiency
202 # Map fuel type to config parameters
203 if fuel_type.upper() in ["RP-1", "KEROSENE", "JP"]:
204 A0 = eff_cfg.A0_hydrocarbon
205 n_pre = eff_cfg.n_pre_hydrocarbon
206 Ea = eff_cfg.Ea_hydrocarbon
207 n_pressure = 0.8
208 elif fuel_type.upper() in ["ETHANOL", "C2H5OH"]:
209 A0 = eff_cfg.A0_ethanol
210 n_pre = eff_cfg.n_pre_ethanol
211 Ea = eff_cfg.Ea_ethanol
212 n_pressure = 0.8
213 elif fuel_type.upper() in ["H2", "HYDROGEN"]:
214 A0 = eff_cfg.A0_hydrogen
215 n_pre = eff_cfg.n_pre_hydrogen
216 Ea = eff_cfg.Ea_hydrogen
217 n_pressure = 0.5
218 elif fuel_type.upper() in ["CH4", "METHANE"]:
219 # Methane: explicit branch (falls back to hydrocarbon kinetics if no methane-specific attrs)
220 A0 = getattr(eff_cfg, "A0_methane", eff_cfg.A0_hydrocarbon)
221 n_pre = getattr(eff_cfg, "n_pre_methane", eff_cfg.n_pre_hydrocarbon)
222 Ea = getattr(eff_cfg, "Ea_methane", eff_cfg.Ea_hydrocarbon)
223 n_pressure = 0.8
224 else:
225 # Default to hydrocarbon
226 A0 = eff_cfg.A0_hydrocarbon
227 n_pre = eff_cfg.n_pre_hydrocarbon
228 Ea = eff_cfg.Ea_hydrocarbon
229 n_pressure = 0.8
230 else:
231 # Built-in defaults (backward compatibility)
232 if fuel_type.upper() in ["RP-1", "KEROSENE", "JP"]:
233 A0 = 1e7
234 n_pre = 0.3
235 Ea = 80000.0
236 n_pressure = 0.8
237 elif fuel_type.upper() in ["ETHANOL", "C2H5OH"]:
238 A0 = 5e7
239 n_pre = 0.25
240 Ea = 140000.0
241 n_pressure = 0.8
242 elif fuel_type.upper() in ["H2", "HYDROGEN"]:
243 A0 = 1e9
244 n_pre = 0.2
245 Ea = 40000.0
246 n_pressure = 0.5
247 elif fuel_type.upper() in ["CH4", "METHANE"]:
248 A0 = 1.5e7
249 n_pre = 0.3
250 Ea = 100000.0
251 n_pressure = 0.8
252 else:
253 # Default: generic hydrocarbon
254 A0 = 1e7
255 n_pre = 0.3
256 Ea = 80000.0
257 n_pressure = 0.8
258
259 # Effective activation energy vs mixture ratio: fuel-rich chemistry is slower (x1.2),
260 # oxidizer-rich faster (x0.9). Blended with a C1 smoothstep across +/-0.10 MR of the 1.5 and
261 # 3.0 boundaries -- the previous hard `if` put a 20% step in Ea at exactly MR = 1.5, which
262 # is inside the ethalox design band, and a step in the objective breaks any gradient-based
263 # or secant refinement that crosses it (same defect as combustion_physics._ea_norm_from_mr).
264 Ea *= _ea_mr_factor(MR)
265
266 # Pre-exponential with pressure dependence
267 # A(P) = A0 × (P / P0)^n_pre, where P0 = 1 MPa reference
268 P0_ref = 1e6 # 1 MPa reference
269 pressure_prefactor = (Pc / P0_ref) ** n_pre
270
271 A = A0 * pressure_prefactor
272
273 if not np.isfinite(A) or A <= 0:
274 raise ValueError(f"Non-physical pre-exponential: A={A}, Pc={Pc}")
275
276 # Pressure term: P^n
277 pressure_term = (Pc / P0_ref) ** n_pressure
278
279 if not np.isfinite(pressure_term) or pressure_term <= 0:
280 raise ValueError(f"Non-physical pressure term: (Pc/P0)^n={pressure_term}, Pc={Pc}")
281
282 # Arrhenius exponential: exp(-Ea / (R × T))
283 arrhenius_exponent = -Ea / (R_gas * Tc)
284
285 if not np.isfinite(arrhenius_exponent):
286 raise ValueError(f"Non-finite Arrhenius exponent: Ea={Ea}, R_gas={R_gas}, Tc={Tc}")
287
288 # Clamp exponent to prevent overflow/underflow, but only for numerical stability
289 # This is a numerical safeguard, not physics clamping
290 arrhenius_exponent = np.clip(arrhenius_exponent, -50.0, 50.0)
291
292 arrhenius_term = np.exp(arrhenius_exponent)
293
294 if not np.isfinite(arrhenius_term) or arrhenius_term <= 0:
295 raise ValueError(f"Non-physical Arrhenius term: exp({arrhenius_exponent}) = {arrhenius_term}")
296
297 # Overall rate constant: k = A × P^n × exp(-Ea/(R×T))
298 k = A * pressure_term * arrhenius_term
299
300 if not np.isfinite(k) or k <= 0:
301 raise ValueError(f"Non-physical rate constant: k={k}")
302
303 return float(k)
304
305
307 Pc: float,
308 Tc: float,
309 MR: float,
310 fuel_type: str = "RP-1",
311 config: Optional[PintleEngineConfig] = None,
312) -> float:
313 """
314 Calculate chemical reaction time scale from Arrhenius rate constant.
315
316 τ = 1 / k, where k is the reaction rate constant.
317
318 Parameters:
319 -----------
320 Pc : float
321 Chamber pressure [Pa]
322 Tc : float
323 Chamber temperature [K]
324 MR : float
325 Mixture ratio (O/F)
326 fuel_type : str
327 Fuel type
328 config : PintleEngineConfig, optional
329 Engine configuration for kinetics parameters
330
331 Returns:
332 --------
333 tau : float
334 Reaction time scale [s]
335 """
336 k = calculate_reaction_rate_constant(Pc, Tc, MR, fuel_type, config=config)
337
338 tau = 1.0 / k
339
340 if not np.isfinite(tau) or tau <= 0:
341 raise ValueError(f"Non-physical reaction time: τ={tau} = 1/k={1/k}")
342
343 return float(tau)
344
345
347 Pc: float,
348 Tc: float,
349 droplet_diameter: float,
350 fuel_props: Optional[Dict] = None,
351 gamma: float = 1.2,
352 R_gas: float = 300.0,
353) -> float:
354 """
355 Calculate droplet evaporation time scale from physics.
356
357 Uses D² law: τ_evap = d² / (8 × D × ln(1 + B))
358 where D is mass diffusivity and B is Spalding number.
359
360 Parameters:
361 -----------
362 Pc : float
363 Chamber pressure [Pa]
364 Tc : float
365 Chamber temperature [K]
366 droplet_diameter : float
367 Droplet diameter [m] (SMD or characteristic size)
368 fuel_props : dict, optional
369 Fuel properties (density, latent heat, etc.)
370 gamma : float
371 Specific heat ratio from CEA
372 R_gas : float
373 Gas constant [J/(kg·K)] from CEA
374
375 Returns:
376 --------
377 tau_evap : float
378 Evaporation time scale [s]
379 """
380 from engine.pipeline.combustion_physics import calculate_gasification_efficiency
381 from engine.pipeline.physics_constants import PRANDTL_DEFAULT
382
383 if not np.isfinite(Pc) or Pc <= 0:
384 raise ValueError(f"Invalid pressure: {Pc} Pa")
385 if not np.isfinite(Tc) or Tc <= 0:
386 raise ValueError(f"Invalid temperature: {Tc} K")
387 if not np.isfinite(droplet_diameter) or droplet_diameter <= 0:
388 raise ValueError(f"Invalid droplet diameter: {droplet_diameter} m")
389
390 # Default fuel properties (RP-1)
391 if fuel_props is None:
392 fuel_props = {}
393
394 # Extract properties with defaults
395 rho_l = fuel_props.get("density", 810.0)
396 L_eff = fuel_props.get("L_vap") or fuel_props.get("latent_heat", 300e3)
397 cp_l = fuel_props.get("specific_heat", 2000.0)
398 T_inj = fuel_props.get("temperature", 293.0)
399
400 # Estimate gas properties
401 # rho_g = P / (R_gas * T)
402 rho_g = Pc / (R_gas * Tc)
403
404 # cp_g = gamma * R / (gamma - 1)
405 if gamma > 1.0:
406 cp_g = gamma * R_gas / (gamma - 1.0)
407 else:
408 cp_g = 2200.0
409
410 mu_g = 7e-5 # Approximation for hot gas
411
412 # Call gasification model
413 # We pass a dummy tau_res because we only need the timescale diagnostics
414 # U_slip is estimated as modest value (convection aids evaporation)
415 try:
416 _, diagnostics = calculate_gasification_efficiency(
417 Tc=Tc,
418 Pc=Pc,
419 tau_res=1.0, # Dummy value
420 SMD=droplet_diameter,
421 rho_l=rho_l,
422 cp_l=cp_l,
423 L_eff=L_eff,
424 T_inj=T_inj,
425 cp_g=cp_g,
426 rho_g=rho_g,
427 mu_g=mu_g,
428 U_slip=20.0, # Assumed slip for reaction progress estimate
429 Pr=PRANDTL_DEFAULT
430 )
431 tau_evap = diagnostics["tau_vap"]
432 except Exception as e:
433 # Fallback if model fails (rare)
434 warnings.warn(f"Gasification model failed in reaction_chemistry: {e}. Using fallback.")
435 tau_evap = 1e-3 # 1 ms default fallback
436
437 return float(tau_evap)
438
439
441 L_chamber: float,
442 u_mean: float,
443 turbulence_intensity: float = 0.1,
444) -> float:
445 """
446 Calculate turbulent mixing time scale from actual flow conditions.
447
448 τ_mix = L / u_turbulent, where u_turbulent = turbulence_intensity × u_mean
449
450 Parameters:
451 -----------
452 L_chamber : float
453 Characteristic mixing length [m] (typically L*)
454 u_mean : float
455 Mean flow velocity [m/s]
456 turbulence_intensity : float
457 Turbulence intensity (typically 0.05-0.2 for rocket chambers)
458
459 Returns:
460 --------
461 tau_mix : float
462 Mixing time scale [s]
463 """
464 if not np.isfinite(L_chamber) or L_chamber <= 0:
465 raise ValueError(f"Invalid chamber length: {L_chamber} m")
466 if not np.isfinite(u_mean) or u_mean <= 0:
467 raise ValueError(f"Invalid mean velocity: {u_mean} m/s")
468 if not np.isfinite(turbulence_intensity) or turbulence_intensity <= 0:
469 raise ValueError(f"Invalid turbulence intensity: {turbulence_intensity}")
470
471 u_turbulent = turbulence_intensity * u_mean
472
473 if not np.isfinite(u_turbulent) or u_turbulent <= 0:
474 raise ValueError(f"Non-physical turbulent velocity: u_turb={u_turbulent}")
475
476 tau_mix = L_chamber / u_turbulent
477
478 if not np.isfinite(tau_mix) or tau_mix <= 0:
479 raise ValueError(f"Non-physical mixing time: τ={tau_mix}")
480
481 return float(tau_mix)
482
483
485 Lstar: float,
486 Pc: float,
487 Tc: float,
488 cstar: float,
489 gamma: float,
490 R: float,
491 MR: float,
492 config: PintleEngineConfig,
493 spray_diagnostics: Optional[Dict] = None,
494 Tc_kinetics: Optional[float] = None,
495) -> Dict[str, float]:
496 """
497 Calculate reaction progress through chamber using actual physics.
498
499 No reference conditions - all calculations based on actual engine state.
500
501 Parameters:
502 -----------
503 Lstar : float
504 Characteristic length [m]
505 Pc : float
506 Chamber pressure [Pa]
507 Tc : float
508 Chamber temperature [K] (Used for residence time - Ideal is conservative)
509 cstar : float
510 Characteristic velocity [m/s]
511 gamma : float
512 Specific heat ratio
513 R : float
514 Gas constant [J/(kg·K)]
515 MR : float
516 Mixture ratio
517 config : PintleEngineConfig
518 Engine configuration
519 spray_diagnostics : dict, optional
520 Spray diagnostics (SMD, velocities, etc.) for evaporation/mixing
521 Tc_kinetics : float, optional
522 Temperature to use for reaction kinetics [K]. If None, uses Tc.
523 (Actual/Effective is conservative)
524
525 Returns:
526 --------
527 results : dict
528 Reaction progress and time scales
529 """
530 # Validate all inputs - raise errors, don't clamp
531 if not all(np.isfinite([Lstar, Pc, Tc, cstar, gamma, R, MR])):
532 raise ValueError("Non-finite input parameters to calculate_chamber_reaction_progress")
533
534 # Use Tc_kinetics for reaction-rate limited processes if provided
535 T_react = Tc_kinetics if Tc_kinetics is not None else Tc
536
537 if Lstar <= 0 or Pc <= 0 or Tc <= 0 or cstar <= 0 or gamma <= 1.0 or R <= 0 or MR <= 0 or T_react <= 0:
538 raise ValueError(f"Invalid physical parameters: L*={Lstar}, Pc={Pc}, Tc={Tc}, T_react={T_react}, c*={cstar}, γ={gamma}, R={R}, MR={MR}")
539
540 # Calculate residence time from actual conditions (Ideal Tc is conservative: shorter time)
541 # τ_res = V_chamber * rho / mdot
542 # Since L* = V_chamber / At and mdot = Pc * At / cstar (choked flow):
543 # τ_res = (Lstar * At) * (Pc / (R * Tc)) / (Pc * At / cstar) = Lstar * cstar / (R * Tc)
544 tau_residence, tau_valid = NumericalStability.safe_divide(Lstar * cstar, R * Tc, None, "tau_residence")
545 if not tau_valid.passed:
546 raise ValueError(f"Residence time calculation failed: L*={Lstar}, c*={cstar}, R={R}, Tc={Tc}")
547
548 fuel_name = _fuel_name_for_kinetics(config)
549
550 # Calculate reaction time scale from Arrhenius kinetics (Actual T_react is conservative: longer time)
551 tau_reaction = calculate_reaction_time_scale(Pc, T_react, MR, fuel_name, config=config)
552
553 # Calculate evaporation time from droplet physics (Actual T_react is conservative: longer time)
554 # Use SMD from spray diagnostics if available
555 if spray_diagnostics and "SMD" in spray_diagnostics:
556 smd = spray_diagnostics["SMD"] # [m]
557 else:
558 # Estimate from spray physics (typical SMD ~ 50-200 microns)
559 smd = 100e-6 # 100 microns default
560
561 fuel_props = _fuel_props_for_evaporation(config)
562
563 tau_evaporation = calculate_evaporation_time_scale(Pc, T_react, smd, fuel_props, gamma=gamma, R_gas=R)
564
565 # Calculate mixing time from actual flow velocities
566 # Mean velocity from characteristic velocity
567 # c* ≈ a* ≈ u* (at throat conditions), so mean velocity ≈ c* / 10-20
568 u_mean = cstar / 15.0 # Approximate mean velocity
569
570 # Turbulence intensity from spray diagnostics if available
571 turbulence_intensity = 0.1 # Default 10%
572 if spray_diagnostics and "turbulence_intensity_mix" in spray_diagnostics:
573 turbulence_intensity = spray_diagnostics["turbulence_intensity_mix"]
574
575 tau_mixing = calculate_mixing_time_scale(Lstar, u_mean, turbulence_intensity)
576
577 # Effective time scale for sequential process
578 # Reactions start during mixing, so combine: τ_eff = sqrt(τ_mix² + τ_reac²) + τ_evap
579 mixing_squared = tau_mixing ** 2
580 reaction_squared = tau_reaction ** 2
581
582 if not (np.isfinite(mixing_squared) and np.isfinite(reaction_squared)):
583 raise ValueError(f"Non-finite time scales: τ_mix={tau_mixing}, τ_reac={tau_reaction}")
584
585 sum_squared = mixing_squared + reaction_squared
586 tau_combined, tau_combined_valid = NumericalStability.safe_sqrt(sum_squared, "sqrt(tau_mix^2 + tau_reac^2)")
587 if not tau_combined_valid.passed:
588 # Fallback to simple sum if sqrt fails
589 tau_combined = tau_mixing + tau_reaction
590
591 tau_effective = tau_evaporation + tau_combined
592
593 if not np.isfinite(tau_effective) or tau_effective <= 0:
594 raise ValueError(f"Non-physical effective time: τ_eff={tau_effective}")
595
596 # Reaction progress at different locations
597 progress_injection = calculate_reaction_progress(0.0, tau_effective, 0.0)
598 progress_mid = calculate_reaction_progress(tau_residence / 2.0, tau_effective, 0.0)
599 progress_throat = calculate_reaction_progress(tau_residence, tau_effective, 0.0)
600
601 return {
602 "progress_injection": float(progress_injection),
603 "progress_mid": float(progress_mid),
604 "progress_throat": float(progress_throat),
605 "tau_residence": float(tau_residence),
606 "tau_evaporation": float(tau_evaporation),
607 "tau_mixing": float(tau_mixing),
608 "tau_reaction": float(tau_reaction),
609 "tau_effective": float(tau_effective),
610 }
611
612
613# --- Retired: shifting-equilibrium nozzle approximation ------------------------
614# calculate_frozen_gamma_from_composition / calculate_shifting_equilibrium_gamma /
615# calculate_shifting_equilibrium_properties were an empirical Damkoehler estimate
616# of the exit composition shift (~700 lines). Superseded by CEA's shifting-
617# equilibrium vacuum thrust coefficient (cea_cache "Cf_vac"). See
618# docs/thrust_efficiency_bug_analysis.md. Removed 2026-06-28.
619
620
621# Keep species evolution function simple - no changes needed
623 progress: float,
624 species_equilibrium: Dict[str, float],
625 species_initial: Optional[Dict[str, float]] = None,
626) -> Dict[str, float]:
627 """
628 Calculate species concentrations based on reaction progress.
629
630 Linear interpolation between initial and equilibrium:
631 X_species = X_initial + progress × (X_equilibrium - X_initial)
632
633 Parameters:
634 -----------
635 progress : float
636 Reaction progress (0-1)
637 species_equilibrium : dict
638 Equilibrium species mole fractions {species_name: fraction}
639 species_initial : dict, optional
640 Initial species mole fractions (default: pure reactants)
641
642 Returns:
643 --------
644 species_current : dict
645 Current species mole fractions
646 """
647 if not np.isfinite(progress) or progress < 0 or progress > 1:
648 raise ValueError(f"Invalid progress: {progress}")
649
650 if species_initial is None:
651 species_initial = {}
652
653 species_current = {}
654
655 # Interpolate between initial and equilibrium
656 for species, X_eq in species_equilibrium.items():
657 if not np.isfinite(X_eq) or X_eq < 0 or X_eq > 1:
658 raise ValueError(f"Invalid equilibrium fraction for {species}: {X_eq}")
659
660 X_init = species_initial.get(species, 0.0)
661 if not np.isfinite(X_init) or X_init < 0 or X_init > 1:
662 raise ValueError(f"Invalid initial fraction for {species}: {X_init}")
663
664 X_current = X_init + progress * (X_eq - X_init)
665
666 # Natural bounds from interpolation, but validate
667 if not np.isfinite(X_current) or X_current < 0 or X_current > 1:
668 raise ValueError(f"Non-physical species fraction for {species}: X={X_current}")
669
670 species_current[species] = float(X_current)
671
672 return species_current
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
str _fuel_name_for_kinetics(PintleEngineConfig config)
Dict[str, float] calculate_species_evolution(float progress, Dict[str, float] species_equilibrium, Optional[Dict[str, float]] species_initial=None)
float calculate_mixing_time_scale(float L_chamber, float u_mean, float turbulence_intensity=0.1)
float calculate_evaporation_time_scale(float Pc, float Tc, float droplet_diameter, Optional[Dict] fuel_props=None, float gamma=1.2, float R_gas=300.0)
float calculate_reaction_progress(float residence_time, float reaction_time_scale, float initial_progress=0.0)
float calculate_reaction_rate_constant(float Pc, float Tc, float MR, str fuel_type="RP-1", Optional[PintleEngineConfig] config=None)
Dict[str, float] calculate_chamber_reaction_progress(float Lstar, float Pc, float Tc, float cstar, float gamma, float R, float MR, PintleEngineConfig config, Optional[Dict] spray_diagnostics=None, Optional[float] Tc_kinetics=None)
Optional[Dict[str, float]] _fuel_props_for_evaporation(PintleEngineConfig config)
float calculate_reaction_time_scale(float Pc, float Tc, float MR, str fuel_type="RP-1", Optional[PintleEngineConfig] config=None)