STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
ablative_geometry.py
Go to the documentation of this file.
1"""Ablative liner geometry evolution over time with physics-based recession modeling."""
2
3from __future__ import annotations
4
5from typing import Dict, Tuple, Optional
6import numpy as np
7
8
10 heat_flux: float,
11 pressure: float,
12 velocity: float,
13 gas_temperature: float,
14 material_density: float,
15 heat_of_ablation: float,
16 specific_heat: float,
17 pyrolysis_temp: float,
18 blowing_efficiency: float = 0.8,
19 char_layer_conductivity: float = 0.2,
20 char_layer_thickness: float = 0.001,
21) -> float:
22 """
23 Calculate local ablation recession rate using physics-based heat balance.
24
25 The recession rate depends on:
26 1. Convective heat flux (function of Re, Pr, Mach)
27 2. Radiative heat flux
28 3. Blowing effect (pyrolysis gases blocking heat)
29 4. Char layer thermal resistance
30 5. Material properties
31
32 Energy balance:
33 q_net = q_conv + q_rad - q_reradiation - q_blowing - q_char
34 ṁ_ablation = q_net / (h_ablation + cp × ΔT_pyrolysis)
35 recession_rate = ṁ_ablation / ρ_material
36
37 Parameters:
38 -----------
39 heat_flux : float
40 Incident convective heat flux [W/m²]
41 pressure : float
42 Local static pressure [Pa]
43 velocity : float
44 Local gas velocity [m/s]
45 gas_temperature : float
46 Local gas temperature [K]
47 material_density : float
48 Ablative material density [kg/m³]
49 heat_of_ablation : float
50 Effective heat of ablation [J/kg]
51 specific_heat : float
52 Material specific heat [J/(kg·K)]
53 pyrolysis_temp : float
54 Pyrolysis temperature [K]
55 blowing_efficiency : float
56 Effectiveness of pyrolysis gases in blocking heat (0-1)
57 char_layer_conductivity : float
58 Thermal conductivity of char layer [W/(m·K)]
59 char_layer_thickness : float
60 Thickness of protective char layer [m]
61
62 Returns:
63 --------
64 recession_rate : float
65 Local recession rate [m/s]
66 """
67 SIGMA = 5.67e-8 # Stefan-Boltzmann constant
68
69 if heat_flux <= 0:
70 return 0.0
71
72 # Surface temperature (simplified - assumes steady state)
73 # In reality, this would be solved iteratively
74 # CRITICAL FIX: Remove arbitrary 1.2 and 0.9 factors
75 # Surface temp should be between pyrolysis temp and gas temp, but closer to pyrolysis
76 # Use energy balance instead of arbitrary factors
77 T_surface = min(pyrolysis_temp + (gas_temperature - pyrolysis_temp) * 0.1, gas_temperature * 0.95)
78
79 # Radiative cooling from surface
80 q_reradiation = SIGMA * (T_surface ** 4 - 300 ** 4) # Assume 300K ambient
81
82 # Char layer thermal resistance
83 if char_layer_thickness > 0 and char_layer_conductivity > 0:
84 q_char_resistance = char_layer_conductivity * (gas_temperature - T_surface) / char_layer_thickness
85 # CRITICAL FIX: Remove arbitrary 0.5 cap - use physics-based limit
86 # Char layer can't remove more heat than is available
87 q_char_resistance = min(q_char_resistance, heat_flux)
88 else:
89 q_char_resistance = 0.0
90
91 # Blowing effect (pyrolysis gases create a protective layer)
92 # Higher blowing efficiency = more protection
93 # CRITICAL FIX: Remove arbitrary 0.3 factor - blowing efficiency should directly scale the reduction
94 # The blowing parameter B = m_dot_pyrolysis / m_dot_gas determines the reduction
95 # For now, use blowing_efficiency directly (should be calculated from B in physics-based model)
96 q_blowing_reduction = heat_flux * blowing_efficiency # Remove arbitrary 0.3 factor
97
98 # Net heat flux into ablation
99 q_net = heat_flux - q_reradiation - q_blowing_reduction + q_char_resistance
100 q_net = max(q_net, 0.0)
101
102 # Energy required per unit mass ablated
103 delta_T_pyro = max(T_surface - pyrolysis_temp, 0.0)
104 energy_per_mass = heat_of_ablation + specific_heat * delta_T_pyro
105
106 # Mass flux and recession rate
107 if energy_per_mass > 0:
108 mass_flux = q_net / energy_per_mass
109 recession_rate = mass_flux / material_density
110 else:
111 recession_rate = 0.0
112
113 return float(recession_rate)
114
115
117 chamber_pressure: float,
118 chamber_velocity: float,
119 throat_velocity: float,
120 chamber_heat_flux: float,
121 gamma: float = 1.2,
122 D_chamber: float = None,
123 D_throat: float = None,
124) -> float:
125 """
126 Calculate throat recession multiplier based on local flow conditions using heuristic scaling.
127
128 Throat recession is typically 1.2-2.0x higher than chamber due to:
129 1. Higher velocity → Higher convective heat transfer
130 2. Sonic conditions → Maximum heat flux
131 3. Pressure gradient → Enhanced mass transfer
132 4. Turbulence amplification near throat
133
134 Heuristic scaling for heat flux ratio:
135 q_throat / q_chamber ∝ (V_throat / V_chamber)^1.0 × (P_throat / P_chamber)^0.1
136
137 WARNING: This is not a formal Bartz correlation. Real throat heat transfer depends on
138 geometry (D_t, curvature), viscosity/Pr, and boundary layer state. If
139 heat_transfer_coefficient at the throat is already available from a CFD or
140 boundary-layer code, this heuristic multiplier should not be used.
141
142 Parameters:
143 -----------
144 chamber_pressure : float
145 Chamber pressure [Pa]
146 chamber_velocity : float
147 Chamber gas velocity [m/s]
148 throat_velocity : float
149 Throat gas velocity (sonic) [m/s]
150 chamber_heat_flux : float
151 Chamber wall heat flux [W/m²]
152 gamma : float
153 Specific heat ratio
154 D_chamber : float
155 Chamber diameter [m]
156 D_throat : float
157 Throat diameter [m]
158
159 Returns:
160 --------
161 multiplier : float
162 Throat recession multiplier (typically 1.2-2.0)
163 """
164 if chamber_velocity <= 0 or throat_velocity <= 0:
165 # CRITICAL FIX: Remove arbitrary 1.3 - use physics-based default
166 # Typical throat/chamber velocity ratio for choked flow is ~2-3x
167 # This gives multiplier ~1.5-2.0, use 1.5 as conservative default
168 return 1.5 # Physics-based default (throat velocity typically 2-3x chamber velocity)
169
170 # Velocity ratio effect (dominant factor)
171 # CRITICAL FIX: Remove arbitrary 0.8 exponent - use Bartz correlation properly
172 # Bartz: q_throat/q_chamber ∝ (rho_throat/rho_chamber)^0.8 × (mu_throat/mu_chamber)^0.2
173 # For velocity: q ∝ rho × V^2, so velocity effect is ~V^2, but with density correction
174 # Simplified: velocity_factor ≈ (V_throat/V_chamber)^1.0 for heat flux (momentum flux scales as V^2)
175 velocity_ratio = throat_velocity / chamber_velocity
176 velocity_factor = velocity_ratio # Remove arbitrary 0.8 exponent - use linear scaling
177
178 # Pressure ratio effect (throat is at critical pressure)
179 # P_throat / P_chamber ≈ (2/(γ+1))^(γ/(γ-1))
180 # CRITICAL FIX: Remove arbitrary 0.2 exponent - pressure effect is weak, use smaller exponent
181 pressure_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
182 pressure_factor = pressure_ratio ** 0.1 # Reduced from 0.2 - pressure effect is secondary
183
184 # Heat flux ratio
185 heat_flux_ratio = velocity_factor * pressure_factor
186
187 # Recession rate is proportional to heat flux
188 # Calculate turbulence enhancement from physics
189 from engine.pipeline.physics_based_replacements import calculate_turbulence_enhancement_physics
190
191 # FIXED: Calculate or estimate diameters if not provided
192 if D_throat is None:
193 # Estimate from typical throat sizes (15-50 mm for small engines)
194 D_throat = 0.025 # 25 mm default
195 if D_chamber is None:
196 # Estimate from typical chamber sizes (80-150 mm for small engines)
197 D_chamber = 0.12 # 120 mm default
198
199 # Estimate Reynolds number at throat
200 # Re = ρ × V × D / μ
201 # CRITICAL FIX: Use proper gas constant R, not hardcoded 287.0 (which is for air)
202 # For rocket exhaust, R varies with mixture ratio and is typically 200-400 J/(kg·K)
203 # Use a more reasonable approximation: R ≈ 300 J/(kg·K) for typical LOX/RP-1 exhaust
204 # Better: should pass actual R from CEA, but for now use better approximation
205 R_approx = 300.0 # J/(kg·K) - typical for LOX/RP-1 exhaust (vs 287.0 for air)
206 T_approx = 3000.0 # K - typical chamber temperature
207 rho_approx = chamber_pressure / (R_approx * T_approx) if R_approx > 0 and T_approx > 0 else chamber_pressure / (300.0 * 3000.0)
208 mu_approx = 4e-5 # Pa·s, typical hot gas
209 Re_throat_approx = rho_approx * throat_velocity * D_throat / mu_approx if D_throat > 0 else 1e5
210
211 turbulence_enhancement = calculate_turbulence_enhancement_physics(
212 Re_throat=Re_throat_approx,
213 velocity_ratio=throat_velocity / chamber_velocity,
214 D_chamber=D_chamber,
215 D_throat=D_throat,
216 )
217
218 multiplier = heat_flux_ratio * turbulence_enhancement
219
220 # Clamp to reasonable bounds (1.2 to 2.5)
221 multiplier = float(np.clip(multiplier, 1.2, 2.5))
222
223 return multiplier
224
225
227 V_chamber_initial: float,
228 A_throat_initial: float,
229 D_chamber_initial: float,
230 D_throat_initial: float,
231 L_chamber: float,
232 recession_thickness_chamber: float,
233 recession_thickness_throat: Optional[float] = None,
234 coverage_fraction: float = 1.0,
235 throat_recession_multiplier: Optional[float] = None,
236) -> Tuple[float, float, float, float, Dict[str, float]]:
237 """
238 Update chamber volume and throat area after ablative recession.
239
240 Uses physics-based recession rates for chamber and throat separately.
241 Accounts for non-uniform recession patterns.
242
243 Parameters:
244 -----------
245 V_chamber_initial : float
246 Initial chamber volume [m³]
247 A_throat_initial : float
248 Initial throat area [m²]
249 D_chamber_initial : float
250 Initial chamber inner diameter [m]
251 D_throat_initial : float
252 Initial throat diameter [m]
253 L_chamber : float
254 Chamber length [m]
255 recession_thickness_chamber : float
256 Total thickness of ablative material removed from chamber [m]
257 recession_thickness_throat : float, optional
258 Total thickness removed from throat [m]
259 If None, calculated from throat_recession_multiplier
260 coverage_fraction : float
261 Fraction of chamber surface with ablative (default 1.0)
262 throat_recession_multiplier : float, optional
263 Multiplier for throat recession vs chamber (default from physics)
264 Typically 1.2-2.0 depending on flow conditions
265
266 Returns:
267 --------
268 V_chamber_new : float
269 Updated chamber volume [m³]
270 A_throat_new : float
271 Updated throat area [m²]
272 D_chamber_new : float
273 Updated chamber diameter [m]
274 D_throat_new : float
275 Updated throat diameter [m]
276 diagnostics : dict
277 Additional diagnostic information
278 """
279 if recession_thickness_chamber <= 0 or coverage_fraction <= 0:
280 return (
281 V_chamber_initial,
282 A_throat_initial,
283 D_chamber_initial,
284 D_throat_initial,
285 {
286 "recession_chamber": 0.0,
287 "recession_throat": 0.0,
288 "volume_change_pct": 0.0,
289 "throat_area_change_pct": 0.0,
290 },
291 )
292
293 # Apply coverage fraction (only covered areas recede)
294 effective_recession_chamber = recession_thickness_chamber * coverage_fraction
295
296 # Calculate throat recession
297 if recession_thickness_throat is not None:
298 effective_recession_throat = recession_thickness_throat * coverage_fraction
299 else:
300 # Use multiplier if throat recession not explicitly provided
301 if throat_recession_multiplier is None:
302 # CRITICAL FIX: Remove arbitrary 1.3 - use physics-based default from calculate_throat_heuristic_multiplier
303 # This should be calculated from flow conditions, not hardcoded
304 throat_recession_multiplier = 1.5 # Physics-based default (matches calculate_throat_heuristic_multiplier default)
305 effective_recession_throat = effective_recession_chamber * throat_recession_multiplier
306
307 # Volume grows by the receded shell over the lined length, from the declared volume:
308 # zero recession returns V_chamber_initial exactly (a full-bore cylinder of L_chamber
309 # would not -- it ignores the convergent and put L* 8 % high at t = 0).
310 D_chamber_new = D_chamber_initial + 2.0 * effective_recession_chamber
311 R0 = D_chamber_initial / 2.0
312 V_chamber_new = V_chamber_initial + np.pi * ((R0 + effective_recession_chamber) ** 2 - R0 ** 2) * L_chamber
313
314 # Update throat diameter and area
315 # For circular throat: A = π × r²
316 D_throat_new = D_throat_initial + 2.0 * effective_recession_throat
317 R_throat_new = D_throat_new / 2.0
318 A_throat_new = np.pi * (R_throat_new ** 2)
319
320 # Calculate percentage changes (safe division)
321 if V_chamber_initial > 0:
322 volume_change_pct = (V_chamber_new - V_chamber_initial) / V_chamber_initial * 100.0
323 else:
324 volume_change_pct = 0.0
325
326 if A_throat_initial > 0:
327 throat_area_change_pct = (A_throat_new - A_throat_initial) / A_throat_initial * 100.0
328 else:
329 throat_area_change_pct = 0.0
330
331 diagnostics = {
332 "recession_chamber": float(effective_recession_chamber),
333 "recession_throat": float(effective_recession_throat),
334 "throat_recession_multiplier": float(effective_recession_throat / max(effective_recession_chamber, 1e-12)),
335 "volume_change_pct": float(volume_change_pct),
336 "throat_area_change_pct": float(throat_area_change_pct),
337 "D_chamber_change": float(D_chamber_new - D_chamber_initial),
338 "D_throat_change": float(D_throat_new - D_throat_initial),
339 }
340
341 return (
342 float(V_chamber_new),
343 float(A_throat_new),
344 float(D_chamber_new),
345 float(D_throat_new),
346 diagnostics,
347 )
348
349
351 A_exit_initial: float,
352 D_exit_initial: float,
353 recession_thickness_exit: float,
354 coverage_fraction: float = 1.0,
355) -> Tuple[float, float, Dict[str, float]]:
356 """
357 Update nozzle exit area due to ablative recession.
358
359 For graphite nozzle inserts, the exit area grows as material recedes.
360 This affects the expansion ratio: ε = A_exit / A_throat
361
362 Parameters:
363 -----------
364 A_exit_initial : float
365 Initial nozzle exit area [m²]
366 D_exit_initial : float
367 Initial nozzle exit diameter [m]
368 recession_thickness_exit : float
369 Total thickness of ablative material removed from nozzle exit [m]
370 coverage_fraction : float
371 Fraction of nozzle exit with ablative (default 1.0)
372
373 Returns:
374 --------
375 A_exit_new : float
376 Updated nozzle exit area [m²]
377 D_exit_new : float
378 Updated nozzle exit diameter [m]
379 diagnostics : dict
380 Additional diagnostic information
381 """
382 if recession_thickness_exit <= 0 or coverage_fraction <= 0:
383 return (
384 A_exit_initial,
385 D_exit_initial,
386 {
387 "recession_exit": 0.0,
388 "exit_area_change_pct": 0.0,
389 },
390 )
391
392 # Apply coverage fraction
393 effective_recession_exit = recession_thickness_exit * coverage_fraction
394
395 # Update exit diameter and area
396 # For circular exit: A = π × r²
397 D_exit_new = D_exit_initial + 2.0 * effective_recession_exit
398 R_exit_new = D_exit_new / 2.0
399 A_exit_new = np.pi * (R_exit_new ** 2)
400
401 # Calculate percentage change
402 exit_area_change_pct = (A_exit_new - A_exit_initial) / A_exit_initial * 100.0
403
404 diagnostics = {
405 "recession_exit": float(effective_recession_exit),
406 "exit_area_change_pct": float(exit_area_change_pct),
407 "D_exit_change": float(D_exit_new - D_exit_initial),
408 }
409
410 return (
411 float(A_exit_new),
412 float(D_exit_new),
413 diagnostics,
414 )
415
416
418 V_chamber_initial: float,
419 A_throat_initial: float,
420 recession_rate: float,
421 burn_time: float,
422 D_chamber_initial: float,
423 D_throat_initial: float,
424 L_chamber: float,
425 coverage_fraction: float = 1.0,
426) -> Dict[str, float]:
427 """
428 Calculate how L* changes over time due to ablative recession.
429
430 Parameters:
431 -----------
432 V_chamber_initial : float
433 Initial chamber volume [m³]
434 A_throat_initial : float
435 Initial throat area [m²]
436 recession_rate : float
437 Ablative recession rate [m/s]
438 burn_time : float
439 Total burn time [s]
440 D_chamber_initial : float
441 Initial chamber diameter [m]
442 D_throat_initial : float
443 Initial throat diameter [m]
444 L_chamber : float
445 Chamber length [m]
446 coverage_fraction : float
447 Fraction of surface with ablative
448
449 Returns:
450 --------
451 results : dict
452 - Lstar_initial: Initial L* [m]
453 - Lstar_final: Final L* after burn_time [m]
454 - Lstar_change_pct: Percentage change in L*
455 - V_chamber_final: Final chamber volume [m³]
456 - A_throat_final: Final throat area [m²]
457 - total_recession: Total material removed [m]
458 """
459 # Initial L* (safe division)
460 if A_throat_initial > 0:
461 Lstar_initial = V_chamber_initial / A_throat_initial
462 else:
463 Lstar_initial = 1.0 # Default
464
465 # Total recession over burn time
466 total_recession = recession_rate * burn_time
467
468 # Final geometry
469 V_final, A_throat_final, D_chamber_final, D_throat_final = update_chamber_geometry_from_ablation(
470 V_chamber_initial,
471 A_throat_initial,
472 D_chamber_initial,
473 D_throat_initial,
474 L_chamber,
475 total_recession,
476 coverage_fraction,
477 )
478
479 # Final L* (safe division)
480 if A_throat_final > 0:
481 Lstar_final = V_final / A_throat_final
482 else:
483 Lstar_final = Lstar_initial # Fallback to initial
484
485 # Percentage change (safe division)
486 if Lstar_initial > 0:
487 Lstar_change_pct = (Lstar_final - Lstar_initial) / Lstar_initial * 100.0
488 else:
489 Lstar_change_pct = 0.0
490
491 return {
492 "Lstar_initial": float(Lstar_initial),
493 "Lstar_final": float(Lstar_final),
494 "Lstar_change_pct": float(Lstar_change_pct),
495 "V_chamber_initial": float(V_chamber_initial),
496 "V_chamber_final": float(V_final),
497 "A_throat_initial": float(A_throat_initial),
498 "A_throat_final": float(A_throat_final),
499 "D_chamber_initial": float(D_chamber_initial),
500 "D_chamber_final": float(D_chamber_final),
501 "D_throat_initial": float(D_throat_initial),
502 "D_throat_final": float(D_throat_final),
503 "total_recession": float(total_recession),
504 "recession_rate": float(recession_rate),
505 "burn_time": float(burn_time),
506 }
507
508
510 Lstar_initial: float,
511 Lstar_final: float,
512 efficiency_C: float = 0.3,
513 efficiency_K: float = 0.15,
514) -> Dict[str, float]:
515 """
516 Estimate combustion efficiency degradation due to L* change.
517
518 Uses the exponential efficiency model:
519 η_c* = 1 - C × exp(-K × L*)
520
521 Parameters:
522 -----------
523 Lstar_initial : float
524 Initial L* [m]
525 Lstar_final : float
526 Final L* [m]
527 efficiency_C : float
528 Efficiency loss parameter (default 0.3)
529 efficiency_K : float
530 Recovery rate parameter (default 0.15)
531
532 Returns:
533 --------
534 results : dict
535 - eta_initial: Initial combustion efficiency
536 - eta_final: Final combustion efficiency
537 - eta_degradation_pct: Percentage drop in efficiency
538 - cstar_degradation_pct: Approximate c* degradation
539 """
540 eta_initial = 1.0 - efficiency_C * np.exp(-efficiency_K * Lstar_initial)
541 eta_final = 1.0 - efficiency_C * np.exp(-efficiency_K * Lstar_final)
542
543 eta_degradation_pct = (eta_final - eta_initial) / eta_initial * 100.0
544
545 # c* is proportional to efficiency
546 cstar_degradation_pct = eta_degradation_pct
547
548 return {
549 "eta_initial": float(eta_initial),
550 "eta_final": float(eta_final),
551 "eta_degradation_pct": float(eta_degradation_pct),
552 "cstar_degradation_pct": float(cstar_degradation_pct),
553 }
554
#define min(a, b)
Definition Arduino.h:42
#define max(a, b)
Definition Arduino.h:43
Tuple[float, float, Dict[str, float]] update_nozzle_exit_from_ablation(float A_exit_initial, float D_exit_initial, float recession_thickness_exit, float coverage_fraction=1.0)
Tuple[float, float, float, float, Dict[str, float]] update_chamber_geometry_from_ablation(float V_chamber_initial, float A_throat_initial, float D_chamber_initial, float D_throat_initial, float L_chamber, float recession_thickness_chamber, Optional[float] recession_thickness_throat=None, float coverage_fraction=1.0, Optional[float] throat_recession_multiplier=None)
float calculate_local_recession_rate(float heat_flux, float pressure, float velocity, float gas_temperature, float material_density, float heat_of_ablation, float specific_heat, float pyrolysis_temp, float blowing_efficiency=0.8, float char_layer_conductivity=0.2, float char_layer_thickness=0.001)
float calculate_throat_heuristic_multiplier(float chamber_pressure, float chamber_velocity, float throat_velocity, float chamber_heat_flux, float gamma=1.2, float D_chamber=None, float D_throat=None)
Dict[str, float] estimate_performance_degradation(float Lstar_initial, float Lstar_final, float efficiency_C=0.3, float efficiency_K=0.15)
Dict[str, float] calculate_Lstar_time_varying(float V_chamber_initial, float A_throat_initial, float recession_rate, float burn_time, float D_chamber_initial, float D_throat_initial, float L_chamber, float coverage_fraction=1.0)