1"""Ullage collapse: what warm pressurant loses to cold liquid.
3Pressurant enters a LOX tank at roughly ambient temperature and meets a surface
4at 90 K. It cools. The pressure it was supplying falls with it, so more
5pressurant is demanded, so the bottle drains faster -- and because the regulator
6is holding tank pressure, **the effect never shows up as a pressure sag at all.
7It shows up as pressurant consumption.** That is the trap: a tank pressure trace
8can look perfect while the COPV empties 20% faster than predicted.
10Why the model is transient, not a heat transfer coefficient
11-----------------------------------------------------------
12Heat does not enter the liquid uniformly; it diffuses into a thermal layer that
13grows from the surface. The layer's depth goes as ``sqrt(pi alpha t)``, which for
14LOX is about **1.1 mm after 5 s** and 3.8 mm after a minute. A steady coefficient
15``h`` cannot represent that: whatever value reproduces a five-second burn is far
16too small for a two-minute hold, and whatever reproduces the hold is far too
17small at ignition. So the model is the transient conduction solution for a
18semi-infinite solid, whose instantaneous flux
22 q(t) = A . dT . sqrt(k rho c / (pi t))
24integrates to ``Q(t) = 2 A dT sqrt(k rho c t / pi)`` -- the familiar
25square-root-of-time behaviour, and the reason a repressurisation immediately
26before firing is worth doing: it resets ``t``.
28That reset is why elapsed contact time is part of the tank's *state* rather than
29a wall-clock reading. A scenario that repressurises at T-10 s starts the clock
30again; one that has been sitting on the pad for an hour does not, and the two
31consume materially different amounts of pressurant over the same burn.
35Pure conduction into quiescent liquid is a **lower** bound. Real ullages have
36free convection, sloshing, and a spray of incoming gas that scrubs the interface,
37and nitrogen over LOX can condense outright, which moves an order of magnitude
38more energy per unit area than sensible cooling does. The
39:data:`registered_collapse_models` registry exists so those can be added as
40named models with their own assumptions rather than as a fudge factor on this
41one. What this module will not do is pretend a single tuned coefficient covers
45from __future__
import annotations
48from dataclasses
import dataclass
49from typing
import Callable, Protocol, runtime_checkable
55MIN_CONTACT_TIME = 1e-3
58@dataclass(frozen=True, slots=True)
60 """The three liquid properties a conduction model needs.
62 Bundled because they are always fetched together and always at the same
63 state, and passed in rather than looked up so the model can be checked
64 against a textbook with three floats.
68 """Thermal conductivity k [W/(m.K)]."""
78 """``sqrt(k rho c)`` [J/(m^2.K.s^0.5)] -- how greedily a surface pulls heat.
80 The single grouping that transient surface conduction depends on. Worth
81 naming: it is why LOX is a much better heat sink than its conductivity
88 """``alpha = k / (rho c)`` [m^2/s] -- how fast the thermal layer grows."""
89 return self.conductivity / (self.density * self.
heat_capacity)
92 """``sqrt(pi alpha t)`` [m]. How deep the liquid has noticed."""
98 """How much heat the ullage loses to the liquid surface, and when."""
106 gas_temperature: float,
107 liquid_temperature: float,
108 liquid: LiquidThermal,
111 """Heat leaving the ullage [W]. Positive means the gas is cooling.
114 area: Interfacial area [m^2] -- the tank's cross-section at the
115 surface, which a head geometry makes a function of fill.
116 gas_temperature: Bulk ullage temperature [K].
117 liquid_temperature: Bulk liquid temperature [K].
118 liquid: Liquid thermal properties.
119 elapsed: Contact time since the interface was last reset [s].
124@dataclass(frozen=True, slots=True)
126 """Ullage exchanges nothing with the liquid.
128 An honest choice for a short blowdown of a warm-gas system, and the right
129 default only when someone has decided it is. It is a *named* model rather
130 than a missing one so a run report can say which assumption produced the
139 gas_temperature: float,
140 liquid_temperature: float,
141 liquid: LiquidThermal,
147@dataclass(frozen=True, slots=True)
149 """Transient conduction into quiescent liquid. A lower bound, and the default.
152 enhancement: Multiplier on the conduction flux, 1.0 for pure quiescent
153 conduction. Free convection and interfacial motion make the real
154 value larger; this is where a *fitted* number from a cold-flow test
155 goes, and it is deliberately a single dimensionless factor with an
156 obvious meaning rather than a tuned coefficient with none.
159 enhancement: float = 1.0
160 name: str =
"conduction"
165 gas_temperature: float,
166 liquid_temperature: float,
167 liquid: LiquidThermal,
170 dt = gas_temperature - liquid_temperature
171 if area <= 0.0
or dt == 0.0:
173 t =
max(elapsed, MIN_CONTACT_TIME)
174 flux = liquid.effusivity / math.sqrt(math.pi * t)
180 gas_temperature: float,
181 liquid_temperature: float,
182 liquid: LiquidThermal,
185 """Cumulative heat [J] after ``elapsed`` at constant area and ΔT.
187 The closed-form integral of :meth:`heat_rate`. Not used by the
188 integrator -- area and temperatures both move -- but it is what the
189 model is validated against, and what answers "roughly how much does
190 this cost me over a 5 s burn" without running anything.
192 dt = gas_temperature - liquid_temperature
199 * math.sqrt(
max(elapsed, 0.0) / math.pi)
204CollapseFactory = Callable[..., CollapseModel]
206_MODELS: dict[str, CollapseFactory] = {
213 _MODELS[name] = factory
217 if name
not in _MODELS:
219 f
"unknown ullage collapse model {name!r}; registered: "
220 f
"{', '.join(sorted(_MODELS))}"
222 return _MODELS[name](**kwargs)
226 return sorted(_MODELS)
float heat_rate(self, float area, float gas_temperature, float liquid_temperature, LiquidThermal liquid, float elapsed)
float heat_rate(self, float area, float gas_temperature, float liquid_temperature, LiquidThermal liquid, float elapsed)
float heat_total(self, float area, float gas_temperature, float liquid_temperature, LiquidThermal liquid, float elapsed)
float penetration_depth(self, float elapsed)
float heat_rate(self, float area, float gas_temperature, float liquid_temperature, LiquidThermal liquid, float elapsed)
CollapseModel build_collapse_model(str name, **object kwargs)
list[str] registered_collapse_models()
None register_collapse_model(str name, CollapseFactory factory)