31def _gas_radiation(gas_props: Dict[str, float], Tg: float, P: float, Tw: float, D: float,
32 eps_wall: float) -> float:
33 """Net gas-to-wall radiation of the chamber gas (Leckner gray gas, beam length 0.95 D).
34 Needs the CEA mole fractions x_H2O and x_CO2 in ``gas_props``."""
36 CYLINDER_BEAM_LENGTH_OVER_D, gas_absorptivity, gray_enclosure_flux, leckner_emissivity)
37 if "x_H2O" not in gas_props
or "x_CO2" not in gas_props:
38 raise ValueError(
"gas radiation needs the CEA mole fractions x_H2O and x_CO2")
40 pH, pC = gas_props[
"x_H2O"] * P_bar, gas_props[
"x_CO2"] * P_bar
41 L = CYLINDER_BEAM_LENGTH_OVER_D * D
42 eps_g = float(leckner_emissivity(Tg, P_bar, pH, pC, L)[
"eps"])
43 alpha = float(gas_absorptivity(Tg,
min(Tw, Tg), P_bar, pH, pC, L))
44 return float(gray_enclosure_flux(eps_g, alpha, eps_wall, Tg, Tw))
170 config: RegenCoolingConfig,
174 Calculate pressure drop through regenerative cooling channels.
177 1. Inlet pipe (3/8" diameter)
178 2. Split into N parallel channels
179 3. N channels (each with friction)
183 Pressure drop is a function of tank pressure:
184 - Higher tank pressure → higher mass flow → higher velocity → higher pressure drop
185 - This is already captured through mdot(P_tank), but we add explicit pressure scaling
186 - Channel length should match chamber length (cooling channels go across the length of the chamber)
191 Total mass flow rate [kg/s] (already depends on P_tank)
193 Fluid density [kg/m³]
195 Dynamic viscosity [Pa·s]
196 config : RegenCoolingConfig
197 Regenerative cooling configuration
199 Tank pressure [Pa] - used for pressure-dependent scaling
204 Total pressure drop [Pa]
211 d_inlet = config.d_inlet
212 A_inlet = np.pi * (d_inlet / 2) ** 2
213 u_inlet = mdot / (rho * A_inlet)
214 d_hyd_inlet = d_inlet
215 Re_inlet = (rho * u_inlet * d_hyd_inlet) / mu
220 delta_p_inlet = f_inlet * (config.L_inlet / d_hyd_inlet) * (rho / 2) * u_inlet ** 2
223 delta_p_inlet += K_ENTRANCE_SHARP_ND * (rho / 2) * u_inlet ** 2
227 K_split = config.K_manifold_split
228 delta_p_split = K_split * (rho / 2) * u_inlet ** 2
232 mdot_channel = mdot / config.n_channels
236 config.channel_width,
237 config.channel_height
239 A_channel = config.channel_width * config.channel_height
245 u_channel_estimate = mdot_channel / (rho * A_channel)
246 Re_channel_entrance = (rho * u_channel_estimate * d_hyd_channel) / mu
249 if Re_channel_entrance <= 0:
250 Cd_entrance_dynamic = config.Cd_entrance_min
253 Cd_entrance_dynamic = config.Cd_entrance_inf - config.a_Re_entrance / np.sqrt(
max(Re_channel_entrance, 1e-6))
254 Cd_entrance_dynamic = np.clip(Cd_entrance_dynamic, config.Cd_entrance_min, config.Cd_entrance_inf)
257 A_eff_entrance = Cd_entrance_dynamic * A_channel
258 u_channel_entrance = mdot_channel / (rho * A_eff_entrance)
262 K_entrance_channel = (1.0 / (Cd_entrance_dynamic ** 2) - 1.0)
263 delta_p_entrance_channel = K_entrance_channel * (rho / 2) * u_channel_entrance ** 2
266 u_channel = mdot_channel / (rho * A_channel)
267 Re_channel = (rho * u_channel * d_hyd_channel) / mu
273 delta_p_channel_friction = f_channel * (config.channel_length / d_hyd_channel) * (rho / 2) * u_channel ** 2
282 Cd_exit_dynamic = config.Cd_exit_min
285 Cd_exit_dynamic = config.Cd_exit_inf - config.a_Re_exit / np.sqrt(
max(Re_channel, 1e-6))
286 Cd_exit_dynamic = np.clip(Cd_exit_dynamic, config.Cd_exit_min, config.Cd_exit_inf)
288 K_exit_channel = (1.0 - Cd_exit_dynamic ** 2)
289 delta_p_exit_channel = K_exit_channel * (rho / 2) * u_channel ** 2
292 delta_p_channel = delta_p_entrance_channel + delta_p_channel_friction + delta_p_exit_channel
299 K_merge = config.K_manifold_merge
300 delta_p_merge = K_merge * (rho / 2) * u_channel ** 2
304 d_outlet = config.d_outlet
if config.d_outlet
is not None else d_inlet
305 A_outlet = np.pi * (d_outlet / 2) ** 2
306 u_outlet = mdot / (rho * A_outlet)
307 d_hyd_outlet = d_outlet
308 Re_outlet = (rho * u_outlet * d_hyd_outlet) / mu
311 delta_p_outlet = f_outlet * (config.L_outlet / d_hyd_outlet) * (rho / 2) * u_outlet ** 2
322 return float(delta_p_total)
371 coolant_props: Dict[str, float],
372 gas_props: Dict[str, float],
373 config: RegenCoolingConfig,
375) -> Dict[str, float]:
376 """Calculate heat-transfer coupling for regenerative cooling channels."""
379 "enabled": config.use_heat_transfer,
380 "coolant_outlet_temperature": coolant_props.get(
"temperature", 0.0),
382 "heat_flux_convective": 0.0,
383 "heat_flux_radiative": 0.0,
384 "overall_heat_flux": 0.0,
387 "wall_temperature_hot": gas_props.get(
"Tc", 0.0),
388 "wall_temperature_coolant": coolant_props.get(
"temperature", 0.0),
389 "film_effectiveness": 0.0,
390 "effective_gas_temperature": gas_props.get(
"Tc", 0.0),
391 "coolant_bulk_temperature": coolant_props.get(
"temperature", 0.0),
392 "segment_wall_temperatures": [],
393 "segment_heat_flux": [],
396 if not config.use_heat_transfer
or mdot_coolant <= 0:
399 cp_c =
max(coolant_props.get(
"cp", DEFAULT_COOLANT_SPEC_HEAT_J_KG_K), 1.0)
400 k_c =
max(coolant_props.get(
"thermal_conductivity", DEFAULT_COOLANT_THERMAL_COND_W_M_K), EPSILON_SMALL)
401 mu_c =
max(coolant_props.get(
"viscosity", 1e-4), EPSILON_TINY)
402 rho_c =
max(coolant_props.get(
"density", DEFAULT_COOLANT_DENS_KG_M3), 1.0)
403 T_c_bulk = coolant_props.get(
"temperature", DEFAULT_COOLANT_TEMP_K)
405 channel_area = config.channel_width * config.channel_height
407 mdot_channel = mdot_coolant /
max(config.n_channels, 1)
409 Pc = gas_props.get(
"Pc", 0.0)
410 Tc = gas_props.get(
"Tc", 0.0)
411 gamma = gas_props.get(
"gamma", DEFAULT_GAMMA_ND)
412 R_g = gas_props.get(
"R", DEFAULT_GAS_CONST_J_KG_K)
414 chamber_d_inner = config.chamber_inner_diameter
415 if chamber_d_inner
is None:
416 chamber_area = gas_props.get(
"chamber_area")
417 if chamber_area
is not None and chamber_area > 0:
418 chamber_d_inner = np.sqrt(4.0 * chamber_area / np.pi)
420 throat_area = gas_props.get(
"A_throat", DEFAULT_THROAT_AREA_M2)
421 chamber_d_inner = np.sqrt(4.0 * throat_area / np.pi)
425 chamber_length = gas_props.get(
"chamber_length", config.channel_length)
427 if chamber_length <= 0:
428 chamber_length = config.channel_length
429 circumference = np.pi * chamber_d_inner
430 segment_count =
max(config.n_segments, 1)
431 segment_length = chamber_length / segment_count
432 segment_area_hot = circumference * segment_length
434 A_cross = np.pi * (chamber_d_inner ** 2) / 4.0
435 rho_g =
max(Pc / (R_g *
max(Tc, 1.0)), MIN_DENS_KG_M3)
436 V_g = mdot_total / (rho_g * A_cross)
439 mu_g_config = config.hot_gas_viscosity
442 M = gas_props.get(
"M")
443 if M
is not None and M > 0
and Tc > 0:
446 mu_g_calculated = mu_g_config
449 mu_g = mu_g_calculated
451 k_g = config.hot_gas_thermal_conductivity
452 cp_g = gamma * R_g /
max(gamma - 1.0, EPSILON_SMALL)
453 Pr_g = config.hot_gas_prandtl
if config.hot_gas_prandtl > 0
else (mu_g * cp_g /
max(k_g, EPSILON_SMALL))
455 Re_g = rho_g * V_g * chamber_d_inner /
max(mu_g, EPSILON_TINY)
459 Nu_g = NU_TURBULENT_COEFFICIENT_ND * (Re_g ** NU_TURBULENT_RE_EXPONENT_ND) * (Pr_g ** NU_TURBULENT_PR_EXPONENT_ND)
461 turbulence_boost_g = 1.0 + config.gas_turbulence_intensity
462 h_g_base = Nu_g * k_g / chamber_d_inner * turbulence_boost_g
466 recovery_factor = config.recovery_factor
if config.recovery_factor
is not None else DEFAULT_RECOVERY_FACTOR_ND
467 Taw = Tc * recovery_factor
469 heat_removed_total = 0.0
470 heat_flux_conv_total = 0.0
471 heat_flux_rad_total = 0.0
472 wall_hot_segments: List[float] = []
473 wall_cold_segments: List[float] = []
474 segment_heat_fluxes: List[float] = []
476 for _
in range(segment_count):
477 u_channel = mdot_channel / (rho_c * channel_area)
478 Re_c = rho_c * u_channel * d_hyd_channel /
max(mu_c, 1e-8)
479 Pr_c = mu_c * cp_c /
max(k_c, 1e-4)
484 f_c = (0.79 * np.log(Re_c) - 1.64) ** -2
if Re_c > 2300
else 64.0 /
max(Re_c, 1.0)
485 Nu_c = (f_c / 8.0 * (Re_c - 1000.0) * Pr_c) / (1.0 + 12.7 * np.sqrt(f_c / 8.0) * (Pr_c ** (2.0 / 3.0) - 1.0))
489 turbulence_boost_c = 1.0 + config.coolant_turbulence_intensity
490 h_c = Nu_c * k_c / d_hyd_channel * turbulence_boost_c
493 U_inv = (1.0 /
max(h_g, EPSILON_SMALL)) + (config.wall_thickness /
max(config.wall_thermal_conductivity, EPSILON_SMALL)) + (1.0 /
max(h_c, EPSILON_SMALL))
497 delta_T =
max(Taw - T_c_bulk, 0.0)
498 heat_flux_conv = U * delta_T
501 Tw_rad = Taw - heat_flux_conv /
max(h_g, EPSILON_SMALL)
502 heat_flux_rad = config.radiation_view_factor *
_gas_radiation(gas_props, Tc, Pc, Tw_rad,
504 config.radiation_emissivity_hot)
505 heat_flux_rad =
max(heat_flux_rad, 0.0)
506 heat_flux_total = heat_flux_conv + heat_flux_rad
508 q_segment = heat_flux_total * segment_area_hot
509 heat_removed_total += q_segment
510 heat_flux_conv_total += heat_flux_conv * segment_area_hot
511 heat_flux_rad_total += heat_flux_rad * segment_area_hot
513 T_c_bulk += q_segment /
max(mdot_coolant * cp_c, EPSILON_SMALL)
516 Tw_hot = Taw - heat_flux_conv /
max(h_g, EPSILON_SMALL)
517 Tw_cold = Tw_hot - heat_flux_conv * config.wall_thickness /
max(config.wall_thermal_conductivity, EPSILON_SMALL)
519 wall_hot_segments.append(Tw_hot)
520 wall_cold_segments.append(Tw_cold)
521 segment_heat_fluxes.append(heat_flux_total)
523 avg_heat_flux = heat_removed_total / (circumference * chamber_length)
if chamber_length > 0
else 0.0
527 "coolant_outlet_temperature": float(T_c_bulk),
528 "heat_removed": float(heat_removed_total),
529 "heat_flux_convective": float(heat_flux_conv_total /
max(circumference * chamber_length, EPSILON_SMALL)),
530 "heat_flux_radiative": float(heat_flux_rad_total /
max(circumference * chamber_length, EPSILON_SMALL)),
531 "overall_heat_flux": float(avg_heat_flux),
532 "h_hot": float(h_g_base),
533 "h_coolant": float(h_c),
534 "wall_temperature_hot": float(np.mean(wall_hot_segments)
if wall_hot_segments
else Taw),
535 "wall_temperature_coolant": float(np.mean(wall_cold_segments)
if wall_cold_segments
else T_c_bulk),
536 "gas_viscosity": float(mu_g),
537 "gas_viscosity_config": float(mu_g_config),
538 "gas_viscosity_calculated": float(mu_g_calculated),
539 "effective_gas_temperature": float(Tc),
540 "adiabatic_wall_temperature": float(Taw),
541 "coolant_bulk_temperature": float(T_c_bulk),
542 "segment_wall_temperatures": wall_hot_segments,
543 "segment_coolant_wall_temperatures": wall_cold_segments,
544 "segment_heat_flux": segment_heat_fluxes,
545 "segment_positions": [i * segment_length + segment_length / 2
for i
in range(segment_count)],