STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
graphite_cooling.py
Go to the documentation of this file.
1"""Graphite throat insert: surface chemistry and recession.
2
3The throat oxidisers of a LOX/hydrocarbon engine are H2O, CO2 and OH; their reactions with
4carbon are endothermic. See carbon_oxidation.
5"""
6
7from __future__ import annotations
8
9from typing import Dict, Optional
10import numpy as np
11from engine.pipeline.config_schemas import GraphiteInsertConfig
12
13R_GAS = 8.314462618 # J/(mol K)
14MW_C = 12.0107 # kg/kmol
15ATM = 101325.0
16
17# Carbon consumed per mole of oxidiser (products CO, H2, H) and the reaction enthalpy per kg
18# of carbon at 298 K from JANAF heats of formation; positive is absorbed at the surface.
19# C + H2O -> CO + H2 +131.3 kJ/mol C + CO2 -> 2 CO +172.5 kJ/mol
20# C + OH -> CO + H +68.1 kJ/mol 2C + O2 -> 2 CO -221.1 kJ/mol
21# C + O -> CO -359.7 kJ/mol
22CARBON_OXIDISERS = {
23 "H2O": (1.0, +10.93e6),
24 "CO2": (1.0, +14.36e6),
25 "OH": (1.0, +5.67e6),
26 "O2": (2.0, -9.20e6),
27 "O": (1.0, -29.95e6),
28}
29
30
31def carbon_oxidation(T_s: float, P_static: float, composition: Dict[str, float], MW_mix: float,
32 g0: float, cfg: GraphiteInsertConfig) -> Dict[str, object]:
33 """Carbon mass flux [kg/(m^2 s)] off a graphite surface at T_s and the heat it absorbs.
34
35 Each oxidiser attacks at 1/(1/m_kin + 1/m_diff). Diffusion limit (unit Lewis number,
36 film theory): m_diff = g * MW_C nu X / MW_mix with g the mass-transfer conductance h/cp,
37 reduced by blowing g = g0 ln(1+B)/B, B = m_C/g0. Kinetics (Bradley et al. 1984, via
38 Thakre & Yang, J. Propulsion Power 24(4), 2008): A T^b exp(-E/RT) p^n, p in atm;
39 O2 and O are diffusion-limited.
40 """
41 rates = {"H2O": cfg.oxidation_H2O, "CO2": cfg.oxidation_CO2, "OH": cfg.oxidation_OH}
42 m_kin = {}
43 for sp in CARBON_OXIDISERS:
44 x = max(float(composition.get(sp, 0.0)), 0.0)
45 if sp in rates and x > 0:
46 r = rates[sp]
47 p_atm = x * P_static / ATM
48 m_kin[sp] = r.A * T_s ** r.T_exponent * np.exp(-r.E / (R_GAS * T_s)) * p_atm ** r.n
49 else:
50 m_kin[sp] = np.inf
51 m_C = 0.0
52 species: Dict[str, float] = {}
53 for _ in range(100):
54 B = m_C / g0 if g0 > 0 else 0.0
55 g = g0 * (np.log1p(B) / B if B > 1e-12 else 1.0)
56 species = {}
57 for sp, (nu, _dH) in CARBON_OXIDISERS.items():
58 x = max(float(composition.get(sp, 0.0)), 0.0)
59 m_diff = g * MW_C * nu * x / MW_mix
60 mk = m_kin[sp]
61 species[sp] = 0.0 if m_diff <= 0 or mk <= 0 else 1.0 / (1.0 / mk + 1.0 / m_diff)
62 m_new = sum(species.values())
63 if abs(m_new - m_C) <= 1e-12 + 1e-10 * m_new:
64 m_C = m_new
65 break
66 m_C = m_new
67 B = m_C / g0 if g0 > 0 else 0.0
68 q_chem = sum(species[sp] * CARBON_OXIDISERS[sp][1] for sp in species)
69 return {"mass_flux": float(m_C), "species": species, "q_chem": float(q_chem), "B": float(B),
70 "blowing_factor": float(np.log1p(B) / B) if B > 1e-12 else 1.0}
71
72
73def graphite_surface_state(T_s: float, gas, contour, composition: Dict[str, float],
74 cfg: GraphiteInsertConfig) -> Dict[str, object]:
75 """Gas-side balance at the throat for a graphite surface at T_s: Bartz convection (blown),
76 gas radiation, and the heat the surface chemistry absorbs. q_net goes into the insert."""
77 from engine.pipeline.thermal.gas_side import station_flux
78
79 eps_w = cfg.emissivity if cfg.emissivity is not None else 0.8
80 st = station_flux(gas, contour, 0.0, T_s, eps_w)
81 g0 = st["h"] / gas.cp
82 ox = carbon_oxidation(T_s, st["P_static"], composition, composition["MW"], g0, cfg)
83 q_conv = ox["blowing_factor"] * st["q_conv"]
84 q_net = q_conv + st["q_rad"] - ox["q_chem"]
85 return {"q_conv": q_conv, "q_conv_unblown": st["q_conv"], "q_rad": st["q_rad"],
86 "q_chem": ox["q_chem"], "q_net": q_net, "mass_flux": ox["mass_flux"],
87 "species": ox["species"], "B": ox["B"], "h": st["h"], "Taw": st["Taw"],
88 "P_static": st["P_static"]}
89
90
92 chamber_pressure: float,
93 chamber_velocity: float,
94 throat_velocity: float,
95 chamber_heat_flux: float,
96 gamma: float = 1.2,
97) -> float:
98 """
99 Calculate throat recession multiplier based on local flow conditions using a heuristic scaling.
100
101 Throat recession is typically 1.2-2.5x higher than chamber due to:
102 1. Higher velocity → Higher convective heat transfer
103 2. Sonic conditions → Maximum heat flux
104 3. Pressure gradient → Enhanced mass transfer
105 4. Turbulence amplification near throat
106
107 Heuristic scaling for heat flux ratio:
108 q_throat / q_chamber ∝ (V_throat / V_chamber)^0.8 × (P_throat / P_chamber)^0.2
109
110 WARNING: This is not a formal Bartz correlation. Real throat heat transfer depends on
111 geometry (D_t, curvature), viscosity/Pr, and boundary layer state. If
112 heat_transfer_coefficient at the throat is already available from a CFD or
113 boundary-layer code, this heuristic multiplier should not be used.
114
115 Parameters:
116 -----------
117 chamber_pressure : float
118 Chamber pressure [Pa]
119 chamber_velocity : float
120 Chamber gas velocity [m/s]
121 throat_velocity : float
122 Throat gas velocity (sonic) [m/s]
123 chamber_heat_flux : float
124 Chamber wall heat flux [W/m²] (used for validation, not in calculation)
125 gamma : float
126 Specific heat ratio
127
128 Returns:
129 --------
130 multiplier : float
131 Throat recession multiplier (typically 1.2-2.5)
132 """
133 if chamber_velocity <= 0 or throat_velocity <= 0:
134 return 1.3 # Default fallback
135
136 # Velocity ratio effect (dominant factor)
137 velocity_ratio = throat_velocity / chamber_velocity
138 velocity_factor = velocity_ratio ** 0.8
139
140 # Pressure ratio effect (throat is at critical pressure)
141 # P_throat / P_chamber ≈ (2/(γ+1))^(γ/(γ-1))
142 pressure_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
143 pressure_factor = pressure_ratio ** 0.2
144
145 # Heuristic heat flux ratio
146 heat_flux_ratio = velocity_factor * pressure_factor
147
148 # Recession rate is proportional to heat flux
149 # Add a base factor for enhanced turbulence at throat
150 turbulence_enhancement = 1.1
151
152 multiplier = heat_flux_ratio * turbulence_enhancement
153
154 # Clamp to reasonable bounds (1.2 to 2.5)
155 multiplier = float(np.clip(multiplier, 1.2, 2.5))
156
157 return multiplier
158
159
161 net_heat_flux: float,
162 throat_temperature: float,
163 gas_temperature: float,
164 graphite_config: GraphiteInsertConfig,
165 throat_area: float,
166 pressure: float,
167 gas_density: Optional[float] = None,
168 gas_viscosity: Optional[float] = None,
169 oxygen_mass_fraction: Optional[float] = None,
170 characteristic_length: Optional[float] = None,
171 gas_velocity: Optional[float] = None,
172 heat_transfer_coefficient: Optional[float] = None,
173 backside_temperature: Optional[float] = None,
174 effective_thickness: Optional[float] = None,
175 gas_state=None,
176 contour=None,
177 throat_composition: Optional[Dict[str, float]] = None,
178) -> Dict[str, float]:
179 """Carbon recession of the graphite throat at the surface temperature ``throat_temperature``.
180
181 The surface temperature is an INPUT: it comes from the insert's transient conduction
182 (engine.pipeline.thermal.wall_conduction), not from a guess. Chemistry is
183 graphite_surface_state: H2O, CO2, OH, O2 and O from the CEA throat composition, each at
184 the lesser of its kinetic and diffusion-limited rate. ``gas_state`` is a
185 gas_side.HotGasState, ``contour`` a gas_side.WallContour and ``throat_composition`` the
186 CEA throat mole fractions with the throat 'MW'. The older arguments (net_heat_flux,
187 gas_density, oxygen_mass_fraction, ...) are accepted and unused. With
188 ``simplified_graphite_oxidation`` the configured constant rate is returned instead.
189 """
190 if not graphite_config.enabled or throat_area <= 0:
191 return {
192 "enabled": False,
193 "recession_rate": 0.0,
194 "mass_flux": 0.0,
195 "surface_temperature": throat_temperature,
196 "heat_removed": 0.0,
197 "oxidation_rate": 0.0,
198 "oxidation_mass_flux": 0.0,
199 "thermal_mass_flux": 0.0,
200 "feedback_fraction": 0.0,
201 "q_feedback": 0.0,
202 "q_radiation": 0.0,
203 "q_conduction": 0.0,
204 }
205
206 # Graphite throats absolutely can recede, especially under high heat flux
207 # and oxidizing species. sizing_only_mode allows suppressing this recession
208 # for initial design phases where only thermal soak is being evaluated.
209 sizing_only_mode = getattr(graphite_config, "sizing_only_mode", False)
210 # If sizing_only_mode is True, we calculate physics but return zero recession_rate.
211 # Otherwise (default), we return the physical recession rate.
212
213 # Simplified oxidation mode: constant 0.01 mm/s radial recession
214 simplified_mode = getattr(graphite_config, "simplified_graphite_oxidation", False)
215 if simplified_mode:
216 # Configured constant rate (default 1e-5 m/s = 0.01 mm/s, previously hardcoded here).
217 _simp_rate = float(getattr(graphite_config, "simplified_oxidation_rate", 1.0e-5))
218 m_dot_ox_simple = _simp_rate * graphite_config.material_density
219
220 # Return simplified metrics immediately
221 # Still calculate basic thermal metrics if needed, but for simplified mode we skip the complex loop
222 recession_rate_report = 0.0 if sizing_only_mode else _simp_rate
223 return {
224 "enabled": True,
225 "recession_rate": float(recession_rate_report),
226 "recession_rate_calculated": _simp_rate,
227 "mass_flux": float(0.0 if sizing_only_mode else m_dot_ox_simple),
228 "mass_flux_calculated": float(m_dot_ox_simple),
229 "surface_temperature": float(throat_temperature),
230 "effective_heat_flux": float(net_heat_flux),
231 "radiative_relief": 0.0,
232 "conduction_loss": 0.0,
233 "heat_removed": 0.0,
234 "oxidation_rate": 1e-5,
235 "oxidation_mass_flux": float(m_dot_ox_simple),
236 "thermal_mass_flux": 0.0,
237 "recession_rate_thermal": 0.0,
238 "mass_flux_thermal": 0.0,
239 "coverage_area": float(throat_area * graphite_config.coverage_fraction),
240 "feedback_fraction": 0.0,
241 "q_feedback": 0.0,
242 "q_radiation": 0.0,
243 "q_conduction": 0.0,
244 "q_convective": float(net_heat_flux),
245 "damkohler_number": 0.0,
246 "blowing_parameter": 0.0,
247 "sizing_only_mode": sizing_only_mode,
248 "simplified_mode": True,
249 }
250
251 if gas_state is None or contour is None or throat_composition is None:
252 raise ValueError(
253 "compute_graphite_recession needs gas_state, contour and throat_composition (the CEA "
254 "throat mole fractions); set 'simplified_graphite_oxidation: true' for a constant rate")
255 st = graphite_surface_state(float(throat_temperature), gas_state, contour, throat_composition,
256 graphite_config)
257 rate = st["mass_flux"] / graphite_config.material_density
258 return {
259 "enabled": True,
260 "recession_rate": 0.0 if sizing_only_mode else float(rate),
261 "recession_rate_calculated": float(rate),
262 "mass_flux": 0.0 if sizing_only_mode else float(st["mass_flux"]),
263 "surface_temperature": float(throat_temperature),
264 "oxidation_rate": float(rate),
265 "oxidation_mass_flux": float(st["mass_flux"]),
266 "recession_rate_thermal": 0.0,
267 "thermal_mass_flux": 0.0,
268 "q_convective": float(st["q_conv"]),
269 "q_radiation": float(st["q_rad"]),
270 "q_chemical": float(st["q_chem"]),
271 "q_net": float(st["q_net"]),
272 "blowing_parameter": float(st["B"]),
273 "species_mass_flux": st["species"],
274 "heat_transfer_coefficient": float(st["h"]),
275 "coverage_area": float(throat_area * graphite_config.coverage_fraction),
276 "sizing_only_mode": sizing_only_mode,
277 "simplified_mode": False,
278 }
279
#define max(a, b)
Definition Arduino.h:43
Dict[str, float] compute_graphite_recession(float net_heat_flux, float throat_temperature, float gas_temperature, GraphiteInsertConfig graphite_config, float throat_area, float pressure, Optional[float] gas_density=None, Optional[float] gas_viscosity=None, Optional[float] oxygen_mass_fraction=None, Optional[float] characteristic_length=None, Optional[float] gas_velocity=None, Optional[float] heat_transfer_coefficient=None, Optional[float] backside_temperature=None, Optional[float] effective_thickness=None, gas_state=None, contour=None, Optional[Dict[str, float]] throat_composition=None)
float calculate_throat_heuristic_multiplier(float chamber_pressure, float chamber_velocity, float throat_velocity, float chamber_heat_flux, float gamma=1.2)
Dict[str, object] graphite_surface_state(float T_s, gas, contour, Dict[str, float] composition, GraphiteInsertConfig cfg)
Dict[str, object] carbon_oxidation(float T_s, float P_static, Dict[str, float] composition, float MW_mix, float g0, GraphiteInsertConfig cfg)