211 geometry: TankGeometry,
213 collapse: CollapseModel |
None =
None,
214 wall_mass: float = 0.0,
215 wall_capacity: float = 0.0,
216 wall_conductance: float = 0.0,
217 vapour: VapourModel |
None =
None,
218 wetted_conductance: float = 0.0,
219 ambient_conductance: float = 0.0,
220 ambient_temperature: float = 293.15,
221 wall_boiling: bool =
False,
222 nucleate_conductance: float = 0.0,
223 leidenfrost_superheat: float = float(
"inf"),
224 boiling_onset: float = 0.0,
225 surface_layer: float = 0.0,
226 surface_mixing: float = 0.0,
227 wall_by_level: bool =
False,
277 self.
_ullage_memo: tuple[TankState |
None, dict[str, float]] = (
None, {})
484 liquid_temperature: float,
485 gas_temperature: float |
None =
None,
486 contact_time: float = 0.0,
487 split_wall: bool =
False,
489 """Build a state from what is actually known before a test.
491 A propellant load, a liquid temperature, a tank pressure, and the
492 pressurant temperature. Everything else follows.
494 ``contact_time`` is how long the interface has existed [s]; a tank that
495 was loaded minutes ago is not one whose surface appeared this instant,
496 and the collapse flux goes as ``1/sqrt(t)``. ``split_wall`` gives the
497 wall under the liquid its own temperature -- the liquid's -- while the
498 wall above stays at the gas temperature. Off, a single lump at the gas
499 temperature, exactly as before.
501 rho_l = self.
liquid.get(
"rho", T=liquid_temperature, q=0.0)
502 volume = self.
geometry.total_volume - liquid_mass / rho_l
505 f
"{liquid_mass:.4g} kg of {self.liquid.name} at "
506 f
"{liquid_temperature:.4g} K does not fit in a "
507 f
"{self.geometry.total_volume * 1e3:.4g} L tank"
509 T_gas = liquid_temperature
if gas_temperature
is None else gas_temperature
521 and contact_time > 0.0
522 and not isinstance(self.
vapour, NoVapour)
525 p_sat = self.
liquid.get(
"p", T=liquid_temperature, q=0.0)
526 except (ValueError, PropertyError):
528 if 0.0 < p_sat < 0.5 * pressure:
530 p_gas = pressure - p_vapour
531 phase = self.
gas.phase(p=p_gas, T=T_gas)
532 if phase.is_liquid_like:
534 f
"pressurant {self.gas.name} at {say(p_gas, 1)} and "
535 f
"{T_gas:.4g} K is a liquid ({phase.value}); an ullage holds gas"
537 rho_g = self.
gas.get(
"rho", p=p_gas, T=T_gas)
538 u_g = self.
gas.get(
"u", p=p_gas, T=T_gas)
547 energy=u_g * rho_g * volume,
548 wall_temperature=T_gas,
550 liquid_mass=liquid_mass,
551 liquid_temperature=liquid_temperature,
552 contact_time=
max(contact_time, 0.0),
553 vapour_mass=vapour_mass,
554 wetted_wall_temperature=liquid_temperature
if split_wall
else None,
555 surface_temperature=(
596 mdot_liquid_out: float = 0.0,
597 mdot_gas_in: float = 0.0,
598 enthalpy_gas_in: float = 0.0,
599 heat_in: float = 0.0,
600 stirring: float = 1.0,
601 mdot_vapour_out: float = 0.0,
603 """Time derivatives, for an integrator to march.
606 mdot_liquid_out: Propellant leaving [kg/s].
607 mdot_gas_in: Pressurant arriving [kg/s].
608 enthalpy_gas_in: Specific enthalpy of arriving pressurant [J/kg].
609 Warm gas into a cold ullage is a heat source and the number
610 that makes helium behave differently from nitrogen.
611 heat_in: External heat into the ullage [W].
612 stirring: Multiplier on the ullage-to-wall conductance, 1 by
613 default; see :meth:`GasVolume.rates`.
614 mdot_vapour_out: Propellant vapour leaving through a vent [kg/s].
615 A well-mixed ullage vents pressurant and vapour in proportion,
616 and the caller knows the split. Without this a venting LOX
617 tank lost only its pressurant: the vapour it boiled stayed,
618 and a tank with a 3/8 in vent wide open climbed to 500 psig. Wall exchange and
619 interfacial loss are computed here and must not be included.
626 layered = self.
surface_layer > 0.0
and state.surface_temperature
is not None
628 state.surface_temperature
629 if layered
and state.surface_temperature
is not None
630 else state.liquid_temperature
633 two_walls = state.wetted_wall_temperature
is not None
635 state.wetted_wall_temperature
636 if state.wetted_wall_temperature
is not None
637 else state.ullage.wall_temperature
645 conductance *
max(stirring, 0.0) * (state.ullage.wall_temperature - T_gas)
650 gas_temperature=T_gas,
651 liquid_temperature=T_surface,
653 elapsed=state.contact_time,
660 dV_dt = mdot_liquid_out / rho_l
if rho_l > 0.0
else 0.0
661 expansion_power = pressure * dV_dt
675 T_sat_total = float(
"inf")
676 wants_sat = state.liquid_mass > 0.0
and not isinstance(self.
vapour, NoVapour)
677 if wants_sat
and not (
679 or (self.
surface_layer > 0.0
and state.surface_temperature
is not None)
684 T_sat_total = self.
liquid.get(
"T", p=pressure, q=0.0)
685 except (ValueError, PropertyError):
686 T_sat_total = float(
"inf")
696 superheat = T_wetted - T_sat_total
702 q_wetted = h * wetted * (T_wetted - state.liquid_temperature)
715 and state.liquid_mass > 0.0
716 and not isinstance(self.
vapour, NoVapour)
719 h_fg = latent_heat(self.
liquid, state.liquid_temperature)
720 except (ValueError, PropertyError):
722 if T_wetted > T_sat_total + self.
boiling_onset and h_fg > 0.0:
724 wall_boil = q_boil / h_fg
741 m_surface = m_bulk = 0.0
742 q_wall_conv = q_wetted - q_boil
746 m_bulk = state.liquid_mass - m_surface
747 if m_surface > 0.0
and m_bulk > 1e-6:
758 span = T_wetted - state.liquid_temperature
760 min(
max((T_wetted - T_surface) / span, 0.0), 1.0)
764 to_bulk = q_wall_conv * (1.0 - reach)
765 q_interface = q_liquid + q_wetted - q_boil - to_bulk
768 liquid_temperature=T_surface,
769 ullage_pressure=pressure,
772 heat_to_interface=q_interface,
778 cp_l = self.
liquid.get(
"cp", T=state.liquid_temperature, q=0.0)
779 heat_capacity_l = state.liquid_mass * cp_l
780 sensible = q_interface - exchange.latent_power
781 d_T_liquid = sensible / heat_capacity_l
if heat_capacity_l > 0.0
else 0.0
794 * (T_surface - state.liquid_temperature)
796 if m_surface > 0.0
and m_bulk > 1e-6:
797 net = sensible - q_mix
806 cap =
min(T_sat_total, T_crit - 2.0)
807 if net > 0.0
and T_surface >= cap:
816 p_sat_cap = self.
liquid.get(
"p", T=
min(T_surface, cap), q=0.0)
817 h_fg_s = latent_heat(self.
liquid,
min(T_surface, cap))
818 except (ValueError, PropertyError):
819 p_sat_cap, h_fg_s = 0.0, 0.0
822 and pressure < 0.97 * p_crit
826 wall_boil += net / h_fg_s
831 d_T_surface = net / (m_surface * cp_l)
832 d_T_liquid = (to_bulk + q_mix) / (m_bulk * cp_l)
833 elif heat_capacity_l > 0.0:
836 d_T_liquid = sensible / heat_capacity_l
837 d_T_surface = d_T_liquid + (state.liquid_temperature - T_surface)
858 mdot_gas_in * enthalpy_gas_in
872 q_ambient_ullage = q_ambient_wetted = 0.0
882 * (total_area - wetted_area)
901 wet_frac = wetted_area / total_area
if total_area > 0.0
else 0.0
902 wet_frac =
min(
max(wet_frac, 1e-3), 1.0 - 1e-3)
903 d_wall = (q_ambient_ullage - q_wall) / (capacity * (1.0 - wet_frac))
904 d_wetted_wall = (q_ambient_wetted - q_wetted) / (capacity * wet_frac)
906 d_wall = (q_ambient_ullage - q_wall - q_wetted) / capacity
912 wall_temperature=d_wall,
913 heat_from_wall=q_wall,
918 liquid_mass=-mdot_liquid_out - exchange.mdot - wall_boil,
919 liquid_temperature=d_T_liquid,
921 vapour_mass=exchange.mdot + wall_boil -
max(mdot_vapour_out, 0.0),
922 evaporation=exchange.mdot,
923 latent_power=exchange.latent_power,
924 heat_to_wetted_wall=q_wetted,
925 heat_to_liquid=q_liquid,
926 expansion_power=expansion_power,
927 wetted_wall_temperature=d_wetted_wall,
928 surface_temperature=d_T_surface,
931 def step(self, state: TankState, rates: TankRates, dt: float) -> TankState:
932 """One explicit Euler step.
934 Explicit on purpose, and stable only because the caller makes it so:
935 the session sub-steps the vessels, sizes its coupling from the
936 regulator-ullage time constant and the ullage's mass, and closes the
937 press path implicitly inside the network solve (ullage storage).
939 Rebuilt with ``replace``: a field added to :class:`TankState` later is
940 carried through the step rather than reset to its default every step,
941 which is how ``vapour_mass`` was once zeroed (docs/PHYSICS-BENCHMARK.md
948 mass=state.ullage.mass + rates.ullage.mass * dt,
949 energy=state.ullage.energy + rates.ullage.energy * dt,
950 wall_temperature=state.ullage.wall_temperature
951 + rates.ullage.wall_temperature * dt,
953 liquid_mass=state.liquid_mass + rates.liquid_mass * dt,
954 liquid_temperature=state.liquid_temperature + rates.liquid_temperature * dt,
955 contact_time=state.contact_time + rates.contact_time * dt,
956 vapour_mass=
max(state.vapour_mass + rates.vapour_mass * dt, 0.0),
957 wetted_wall_temperature=(
959 if state.wetted_wall_temperature
is None
960 else state.wetted_wall_temperature + rates.wetted_wall_temperature * dt
962 surface_temperature=(
964 if state.surface_temperature
is None
965 else state.surface_temperature + rates.surface_temperature * dt