69 total_propellant_kg: Optional[float] =
None,
70 of_ratio: Optional[float] =
None,
71 lox_tank_volume_m3: Optional[float] =
None,
72 lox_ullage_volume_m3: Optional[float] =
None,
73 fuel_tank_volume_m3: Optional[float] =
None,
74 fuel_ullage_volume_m3: Optional[float] =
None,
75 lox_propellant_mass_kg: Optional[float] =
None,
76 fuel_propellant_mass_kg: Optional[float] =
None,
77) -> Tuple[float, float, float, float, float, float]:
78 """Resolve LOX/fuel (volume_m3, mass_kg, density) for blowdown simulation.
80 Falls back to config tank fields when overrides are omitted.
82 rho_lox = float(config.fluids[
"oxidizer"].density)
83 rho_fuel = float(config.fluids[
"fuel"].density)
85 if lox_propellant_mass_kg
is not None and fuel_propellant_mass_kg
is not None:
86 m_lox = float(lox_propellant_mass_kg)
87 m_fuel = float(fuel_propellant_mass_kg)
88 elif total_propellant_kg
is not None:
90 dr = getattr(config,
"design_requirements",
None)
91 of_ratio = float(getattr(dr,
"optimal_of_ratio",
None)
or 2.5)
94 lox_tank = getattr(config,
"lox_tank",
None)
95 fuel_tank = getattr(config,
"fuel_tank",
None)
96 m_lox = float(getattr(lox_tank,
"mass",
None)
or 0.0)
97 m_fuel = float(getattr(fuel_tank,
"mass",
None)
or 0.0)
98 if m_lox <= 0
or m_fuel <= 0:
100 "Blowdown requires total_propellant_kg or positive lox_tank.mass and fuel_tank.mass in config"
103 if lox_tank_volume_m3
is not None or lox_ullage_volume_m3
is not None:
105 tank_volume_m3=lox_tank_volume_m3,
106 ullage_volume_m3=lox_ullage_volume_m3,
107 propellant_mass_kg=m_lox,
108 density_kg_m3=rho_lox,
109 tank_name=
"LOX tank",
114 if fuel_tank_volume_m3
is not None or fuel_ullage_volume_m3
is not None:
116 tank_volume_m3=fuel_tank_volume_m3,
117 ullage_volume_m3=fuel_ullage_volume_m3,
118 propellant_mass_kg=m_fuel,
119 density_kg_m3=rho_fuel,
120 tank_name=
"Fuel tank",
125 return V_lox, m_lox, rho_lox, V_fuel, m_fuel, rho_fuel
346 evaluate_engine_fn: Callable[[float, float], Tuple[float, float]],
347 P_lox_initial_Pa: float,
348 P_fuel_initial_Pa: float,
351 R_pressurant: float = 296.803,
352 T_lox_gas_K: float = 250.0,
353 T_fuel_gas_K: float = 293.0,
354 n_polytropic: float = 1.2,
355 use_real_gas: bool =
True,
356 n2_Z_csv: str =
"n2_Z_lookup.csv",
357 total_propellant_kg: Optional[float] =
None,
358 of_ratio: Optional[float] =
None,
359 lox_tank_volume_m3: Optional[float] =
None,
360 lox_ullage_volume_m3: Optional[float] =
None,
361 fuel_tank_volume_m3: Optional[float] =
None,
362 fuel_ullage_volume_m3: Optional[float] =
None,
363 lox_propellant_mass_kg: Optional[float] =
None,
364 fuel_propellant_mass_kg: Optional[float] =
None,
365) -> Dict[str, Dict[str, np.ndarray]]:
367 Simulate coupled blowdown for both LOX and fuel tanks.
373 evaluate_engine_fn : function
374 Callback (P_lox_Pa, P_fuel_Pa) -> (mdot_lox_kg_s, mdot_fuel_kg_s)
375 P_lox_initial_Pa, P_fuel_initial_Pa : float
376 Initial tank pressures
377 config : EngineConfig
378 Configuration object with tank geometry
382 dict with full time-series traces for both tanks
390 def get_tank_params(fluid_key, tank_attr, h_attr, r_attr):
391 rho = float(config.fluids[fluid_key].density)
393 tank = getattr(config, tank_attr,
None)
394 if tank
and hasattr(tank,
'tank_volume_m3')
and tank.tank_volume_m3
is not None:
395 V = float(tank.tank_volume_m3)
396 elif tank
and hasattr(tank, h_attr)
and hasattr(tank, r_attr):
397 V = float(np.pi * getattr(tank, r_attr)**2 * getattr(tank, h_attr))
398 elif hasattr(config,
'propellant')
and hasattr(config.propellant,
'tank_volume_m3'):
399 V = float(config.propellant.tank_volume_m3)
403 if tank
and hasattr(tank,
'mass'):
405 elif hasattr(config,
'propellant')
and hasattr(config.propellant,
'initial_mass_kg'):
406 m = float(config.propellant.initial_mass_kg)
412 has_tank_overrides = any(
417 lox_ullage_volume_m3,
419 fuel_ullage_volume_m3,
420 lox_propellant_mass_kg,
421 fuel_propellant_mass_kg,
424 if has_tank_overrides:
427 total_propellant_kg=total_propellant_kg,
429 lox_tank_volume_m3=lox_tank_volume_m3,
430 lox_ullage_volume_m3=lox_ullage_volume_m3,
431 fuel_tank_volume_m3=fuel_tank_volume_m3,
432 fuel_ullage_volume_m3=fuel_ullage_volume_m3,
433 lox_propellant_mass_kg=lox_propellant_mass_kg,
434 fuel_propellant_mass_kg=fuel_propellant_mass_kg,
437 rho_lox, V_lox, m_lox = get_tank_params(
'oxidizer',
'lox_tank',
'lox_h',
'lox_radius')
438 rho_fuel, V_fuel, m_fuel = get_tank_params(
'fuel',
'fuel_tank',
'rp1_h',
'rp1_radius')
443 GAS_VENTING_AREA_MULTIPLIER = 10.0
445 def get_injector_area(fluid_type: str, for_gas_venting: bool =
False) -> float:
447 inj = config.injector
449 if inj.type ==
'pintle':
450 if fluid_type ==
'oxidizer':
452 liquid_area = float(geom.lox.n_orifices * geom.lox.A_entry)
456 liquid_area = float(np.pi * geom.fuel.d_pintle_tip * geom.fuel.h_gap)
457 elif inj.type ==
'coaxial':
458 if fluid_type ==
'oxidizer':
460 liquid_area = float(geom.core.n_ports * np.pi * (geom.core.d_port/2)**2)
463 liquid_area = float(geom.core.n_ports * np.pi * ((geom.annulus.inner_diameter + 2*geom.annulus.gap_thickness)/2)**2 - np.pi*(geom.annulus.inner_diameter/2)**2)
470 return max(liquid_area * GAS_VENTING_AREA_MULTIPLIER, 1e-3)
471 return max(liquid_area, 1e-5)
473 return 1e-3
if for_gas_venting
else 1e-4
475 A_inj_lox = get_injector_area(
'oxidizer')
476 A_inj_fuel = get_injector_area(
'fuel')
479 lox_tank =
TankState(V_lox, m_lox, rho_lox, P_lox_initial_Pa, T_lox_gas_K, R_pressurant, Z_interp, n_polytropic)
480 fuel_tank =
TankState(V_fuel, m_fuel, rho_fuel, P_fuel_initial_Pa, T_fuel_gas_K, R_pressurant, Z_interp, n_polytropic)
485 'lox': {k: np.zeros(N)
for k
in [
'P_Pa',
'T_K',
'V_ullage_m3',
'mdot_kg_s',
'm_prop_kg']},
486 'fuel': {k: np.zeros(N)
for k
in [
'P_Pa',
'T_K',
'V_ullage_m3',
'mdot_kg_s',
'm_prop_kg']}
489 history[
'lox'][
'is_depleted'] = np.zeros(N, dtype=bool)
490 history[
'fuel'][
'is_depleted'] = np.zeros(N, dtype=bool)
494 mdot_lox_0, mdot_fuel_0 = evaluate_engine_fn(P_lox_initial_Pa, P_fuel_initial_Pa)
497 history[
'lox'][
'P_Pa'][0] = lox_tank.P_gas
498 history[
'lox'][
'T_K'][0] = lox_tank.T_gas
499 history[
'lox'][
'V_ullage_m3'][0] = lox_tank.V_ullage
500 history[
'lox'][
'mdot_kg_s'][0] = mdot_lox_0
501 history[
'lox'][
'm_prop_kg'][0] = lox_tank.m_prop
503 history[
'fuel'][
'P_Pa'][0] = fuel_tank.P_gas
504 history[
'fuel'][
'T_K'][0] = fuel_tank.T_gas
505 history[
'fuel'][
'V_ullage_m3'][0] = fuel_tank.V_ullage
506 history[
'fuel'][
'mdot_kg_s'][0] = mdot_fuel_0
507 history[
'fuel'][
'm_prop_kg'][0] = fuel_tank.m_prop
514 for i
in range(1, N):
515 dt = times[i] - times[i-1]
518 mdot_lox_liquid_prev = history[
'lox'][
'mdot_kg_s'][i-1]
519 mdot_fuel_liquid_prev = history[
'fuel'][
'mdot_kg_s'][i-1]
523 lox_depleted = lox_tank.m_prop <= 1e-4
525 mdot_lox_liquid = 0.0
527 A_vent_lox = get_injector_area(
'oxidizer', for_gas_venting=
True)
529 lox_tank.P_gas, P_amb, lox_tank.T_gas, A_vent_lox, gamma_gas, R_pressurant
532 mdot_lox_liquid = mdot_lox_liquid_prev
536 fuel_depleted = fuel_tank.m_prop <= 1e-4
538 mdot_fuel_liquid = 0.0
540 A_vent_fuel = get_injector_area(
'fuel', for_gas_venting=
True)
542 fuel_tank.P_gas, P_amb, fuel_tank.T_gas, A_vent_fuel, gamma_gas, R_pressurant
545 mdot_fuel_liquid = mdot_fuel_liquid_prev
549 P_lox_new = lox_tank.step(mdot_lox_liquid, dt, mdot_lox_gas)
550 P_fuel_new = fuel_tank.step(mdot_fuel_liquid, dt, mdot_fuel_gas)
554 if lox_depleted
or fuel_depleted:
558 elif lox_tank.m_prop > 0
and fuel_tank.m_prop > 0:
559 mdot_lox_new, mdot_fuel_new = evaluate_engine_fn(P_lox_new, P_fuel_new)
566 history[
'lox'][
'P_Pa'][i] = P_lox_new
567 history[
'lox'][
'T_K'][i] = lox_tank.T_gas
568 history[
'lox'][
'V_ullage_m3'][i] = lox_tank.V_ullage
569 history[
'lox'][
'mdot_kg_s'][i] = mdot_lox_new
570 history[
'lox'][
'm_prop_kg'][i] = lox_tank.m_prop
571 history[
'lox'][
'is_depleted'][i] = lox_depleted
573 history[
'fuel'][
'P_Pa'][i] = P_fuel_new
574 history[
'fuel'][
'T_K'][i] = fuel_tank.T_gas
575 history[
'fuel'][
'V_ullage_m3'][i] = fuel_tank.V_ullage
576 history[
'fuel'][
'mdot_kg_s'][i] = mdot_fuel_new
577 history[
'fuel'][
'm_prop_kg'][i] = fuel_tank.m_prop
578 history[
'fuel'][
'is_depleted'][i] = fuel_depleted
Dict[str, Dict[str, np.ndarray]] simulate_coupled_blowdown(np.ndarray times, Callable[[float, float], Tuple[float, float]] evaluate_engine_fn, float P_lox_initial_Pa, float P_fuel_initial_Pa, config, *, float R_pressurant=296.803, float T_lox_gas_K=250.0, float T_fuel_gas_K=293.0, float n_polytropic=1.2, bool use_real_gas=True, str n2_Z_csv="n2_Z_lookup.csv", Optional[float] total_propellant_kg=None, Optional[float] of_ratio=None, Optional[float] lox_tank_volume_m3=None, Optional[float] lox_ullage_volume_m3=None, Optional[float] fuel_tank_volume_m3=None, Optional[float] fuel_ullage_volume_m3=None, Optional[float] lox_propellant_mass_kg=None, Optional[float] fuel_propellant_mass_kg=None)