STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_stratification.py
Go to the documentation of this file.
1"""Where a shut LOX tank's leak heat goes: boiling regimes, the onset
2superheat, and the stratified surface layer.
3
4A lumped liquid gets a shut cryogen tank's climb wrong both ways. Boil the
5whole leak into the ullage and it is ~100 psi/min; warm all fifteen
6kilograms and it is ~0.3 psi/min. The stand shows tens of psi a minute,
7because the leak warms the *surface*. Each closure here is opt-in and, off,
8leaves the tank exactly as it was.
9"""
10
11from __future__ import annotations
12
13from dataclasses import replace
14
15import pytest
16
17from feedtwin.props import Fluid
18from feedtwin.vessels.geometry import CylindricalTank
19from feedtwin.vessels.tank import Tank
20from feedtwin.vessels.vapour import SaturatedVapour
21
22PSI = 6894.757
23
24
25@pytest.fixture(scope="module")
26def parts():
27 return (
28 Fluid("oxygen"),
29 Fluid("nitrogen"),
30 CylindricalTank(diameter=0.1524, barrel_length=0.531),
31 )
32
33
34def build(parts, **kwargs) -> Tank:
35 lox, gn2, geometry = parts
36 return Tank(
37 lox,
38 gn2,
39 geometry,
40 wall_mass=6.9,
41 wall_capacity=900.0,
42 wall_conductance=12.0,
43 vapour=SaturatedVapour(),
44 wetted_conductance=100.0,
45 # An inch of fiberglass under an 8 W/m2K air film: 1.3 W/m2K.
46 ambient_conductance=1.3,
47 wall_boiling=True,
48 **kwargs,
49 )
50
51
52def shut(tank: Tank, parts, *, wall_above: float, pressure: float = 101325.0):
53 """Loaded to 95 %, with the vent just shut, wetted wall ``wall_above``
54 kelvin over the liquid. At more than twice saturation the ullage is
55 seeded with the propellant's own vapour at the liquid's saturation
56 pressure, as a tank that has sat a while holds."""
57 lox, _, geometry = parts
58 state = tank.initial_state(
59 pressure=pressure,
60 liquid_mass=0.95 * geometry.total_volume * lox.get("rho", T=90.0, q=0.0),
61 liquid_temperature=90.0,
62 gas_temperature=95.0,
63 contact_time=300.0,
64 split_wall=True,
65 )
66 return replace(state, wetted_wall_temperature=90.0 + wall_above)
67
68
69# ------------------------------------------------------------ the off switch
70
71
73 old = build(parts)
74 new = build(
75 parts,
76 nucleate_conductance=0.0,
77 leidenfrost_superheat=float("inf"),
78 boiling_onset=0.0,
79 surface_layer=0.0,
80 surface_mixing=0.0,
81 )
82 for above in (2.0, 60.0):
83 a = old.rates(shut(old, parts, wall_above=above))
84 b = new.rates(shut(new, parts, wall_above=above))
85 assert a == b
86
87
88# --------------------------------------------------------------- the regimes
89
90
92 """A wall 100 K above saturation is insulated by its own vapour film; one
93 5 K above is wetted and nucleate boiling pulls thirty times the heat."""
94 film = build(parts)
95 both = build(parts, nucleate_conductance=3000.0, leidenfrost_superheat=40.0)
96 hot_f = film.rates(shut(film, parts, wall_above=100.0)).heat_to_wetted_wall
97 hot_b = both.rates(shut(both, parts, wall_above=100.0)).heat_to_wetted_wall
98 assert hot_b == pytest.approx(hot_f)
99 warm_f = film.rates(shut(film, parts, wall_above=5.0)).heat_to_wetted_wall
100 warm_b = both.rates(shut(both, parts, wall_above=5.0)).heat_to_wetted_wall
101 assert warm_b == pytest.approx(30.0 * warm_f, rel=1e-6)
102
103
105 """Two kelvin of superheat: with no onset the wall boils it all into the
106 ullage; with a 5 K onset the same heat reaches the liquid as sensible."""
107 boils = build(parts)
108 waits = build(parts, boiling_onset=5.0)
109 r_boil = boils.rates(shut(boils, parts, wall_above=2.0))
110 r_wait = waits.rates(shut(waits, parts, wall_above=2.0))
111 # The wall's share: everything the ullage gains beyond what the surface
112 # itself evaporates. (Over a pressurant-only ullage the saturated surface
113 # evaporates in both cases; that is the surface, not the wall.)
114 assert r_boil.vapour_mass - r_boil.evaporation > 0.0
115 assert r_wait.vapour_mass - r_wait.evaporation == pytest.approx(0.0, abs=1e-12)
116 assert r_wait.heat_to_wetted_wall == pytest.approx(r_boil.heat_to_wetted_wall)
117 # What the wall did not boil reached the surface instead.
118 assert r_wait.latent_power > r_boil.latent_power
119 # ...and well over the onset it boils exactly as before.
120 hot = waits.rates(shut(waits, parts, wall_above=60.0))
121 assert hot.vapour_mass == pytest.approx(
122 boils.rates(shut(boils, parts, wall_above=60.0)).vapour_mass
123 )
124
125
126# ---------------------------------------------------------- the surface layer
127
128
130 lox, _, geometry = parts
131 tank = build(parts, boiling_onset=5.0, surface_layer=0.01, surface_mixing=5.0)
132 # Vapour already at saturation over the surface, so the arriving heat is
133 # sensible and has to warm something.
134 state = shut(tank, parts, wall_above=1.0, pressure=2.5e5)
135 assert state.vapour_mass > 0.0
136 assert state.surface_temperature == pytest.approx(90.0)
137 rates = tank.rates(state)
138 m_s = tank.surface_mass(state)
139 assert 0.1 < m_s < 0.4, m_s # a centimetre of LOX over a 6 in bore
140 # The slab takes the heat; the bulk sees only the trickle through the layer.
141 assert rates.surface_temperature > 0.0
142 assert rates.surface_temperature > 20.0 * rates.liquid_temperature
143 stepped = tank.step(state, rates, 1.0)
144 assert stepped.surface_temperature == pytest.approx(
145 90.0 + rates.surface_temperature, rel=1e-9
146 )
147 # No layer on the state: the same tank is well mixed.
148 mixed = replace(state, surface_temperature=None)
149 plain = tank.rates(mixed)
150 assert plain.surface_temperature == 0.0
151 assert plain.liquid_temperature > rates.liquid_temperature
152
153
155 """A wall two kelvin warm holds thirty kilojoules; poured into a
156 two-hundred-gram layer that would be a surface hotter than the metal.
157 Liquid warmed at a wall arrives no hotter than the wall, so once the
158 layer reaches it the rest thickens the layer -- here, warms the bulk."""
159 tank = build(
160 parts,
161 boiling_onset=5.0,
162 surface_layer=0.01,
163 surface_mixing=0.0,
164 nucleate_conductance=3000.0,
165 leidenfrost_superheat=40.0,
166 )
167 state = shut(tank, parts, wall_above=2.0, pressure=2.5e5)
168 state = replace(state, surface_temperature=state.liquid_temperature + 2.0)
169 rates = tank.rates(state)
170 # Nothing from the wall reaches a layer already at the wall's temperature;
171 # what the wall gives goes to the bulk.
172 assert rates.heat_to_wetted_wall > 0.0
173 assert rates.liquid_temperature > 0.0
174 below = replace(state, surface_temperature=state.liquid_temperature + 0.5)
175 r_below = tank.rates(below)
176 assert r_below.surface_temperature > rates.surface_temperature
177 assert r_below.liquid_temperature < rates.liquid_temperature
178
179
181 parts,
182) -> None:
183 """The stand's number is ~20 psi/min (operator). Where it comes from: the
184 dry wall above the liquid is still warm after a load (the liquid never
185 touched it), it warms the ullage, the ullage warms the surface, and the
186 surface sets the pressure. The three closures together land in that
187 decade and keep climbing while the hardware has heat to give; the old
188 closure jumps and stalls."""
189
190 def climb(tank: Tank, dt: float = 0.05) -> tuple[float, float]:
191 """psi/min over the first and the second half-minute."""
192 state = shut(tank, parts, wall_above=0.5)
193 # The wall above the liquid: 200 K after a two-minute load.
194 state = replace(state, ullage=replace(state.ullage, wall_temperature=200.0))
195 marks = [tank.pressure(state)]
196 for half in range(2):
197 for _ in range(int(30.0 / dt)):
198 state = tank.step(state, tank.rates(state), dt)
199 marks.append(tank.pressure(state))
200 return (marks[1] - marks[0]) / PSI * 2.0, (marks[2] - marks[1]) / PSI * 2.0
201
202 stratified = build(
203 parts,
204 nucleate_conductance=3000.0,
205 leidenfrost_superheat=40.0,
206 boiling_onset=5.0,
207 surface_layer=0.01,
208 surface_mixing=5.0,
209 )
210 first, second = climb(stratified)
211 # Tens of psi a minute over the first minute, front-loaded -- the dry
212 # wall gives its heat fastest while it is hottest -- and still climbing
213 # in the second half-minute rather than stalled.
214 assert (
215 10.0 < (first + second) / 2.0 < 80.0
216 ), f"{first:.1f} then {second:.1f} psi/min"
217 assert second > 2.0, f"{first:.1f} then {second:.1f} psi/min"
218 # The old closure -- boil the leak at the wall -- jumps and then stalls:
219 # the rising pressure lifts saturation over the wall, boiling stops, and
220 # the leak warms fifteen kilograms at a fraction of a psi a minute.
221 boiling = build(parts, nucleate_conductance=3000.0, leidenfrost_superheat=40.0)
222 b_first, b_second = climb(boiling)
223 assert b_second < 0.2 * b_first, f"{b_first:.1f} then {b_second:.1f} psi/min"
224
225
227 parts,
228) -> None:
229 """A dry wall at room temperature over a small ullage pours kilowatts into
230 the gas, and the gas into the surface. The layer stops at saturation for
231 the tank's pressure and the heat becomes vapour; it never reaches the
232 critical temperature, where every property call fails and the tank
233 used to freeze at zero."""
234 tank = build(parts, boiling_onset=2.0, surface_layer=0.01, surface_mixing=5.0)
235 state = shut(tank, parts, wall_above=0.5, pressure=2.5e5)
236 state = replace(state, ullage=replace(state.ullage, wall_temperature=293.0))
237 T_crit = float(parts[0].critical_temperature)
238 for _ in range(int(60.0 / 0.02)):
239 rates = tank.rates(state)
240 state = tank.step(state, rates, 0.02)
241 assert state.surface_temperature is not None
242 assert state.surface_temperature < T_crit - 1.0
243 assert tank.pressure(state) > 1.0e5
244 assert state.vapour_mass > 0.0
245 # And the tank stops at the propellant's critical pressure: past the end
246 # of the saturation line there is nothing to boil, so the heat warms the
247 # bulk and the pressure holds.
248 p_crit = float(parts[0].critical_pressure)
249 for _ in range(int(120.0 / 0.02)):
250 state = tank.step(state, tank.rates(state), 0.02)
251 assert tank.pressure(state) < 1.01 * p_crit
252 assert state.liquid_temperature > 90.05
253
254
256 parts,
257) -> None:
258 """A wall at 92 K over 90 K liquid with the layer at 91 K: half the wall's
259 convective heat reaches the surface, half thickens the layer into the bulk.
260 Only the half that arrives can evaporate the surface. The exchange used to
261 price all of it as latent and then debit the layer again for the half the
262 bulk took -- evaporating with heat that never got there."""
263 tank = build(parts, boiling_onset=5.0, surface_layer=0.01, surface_mixing=0.0)
264 state = shut(tank, parts, wall_above=2.0, pressure=2.5e5)
265 state = replace(state, vapour_mass=0.0, surface_temperature=91.0)
266 rates = tank.rates(state)
267 reach = (92.0 - 91.0) / (92.0 - 90.0)
268 assert rates.heat_to_wetted_wall > 0.0 and rates.heat_to_liquid > 0.0
269 arrives = rates.heat_to_liquid + reach * rates.heat_to_wetted_wall
270 assert rates.latent_power == pytest.approx(arrives, rel=1e-9)
271 # The rest warms the bulk, and none of it is taken back off the surface.
272 to_bulk = (1.0 - reach) * rates.heat_to_wetted_wall
273 m_s = tank.surface_mass(state)
274 cp = parts[0].get("cp", T=90.0, q=0.0)
275 assert rates.liquid_temperature == pytest.approx(
276 to_bulk / ((state.liquid_mass - m_s) * cp), rel=1e-9
277 )
278 assert rates.surface_temperature == pytest.approx(0.0, abs=1e-12)
None test_the_surface_layer_warms_and_the_bulk_barely_does(parts)
None test_the_new_closures_off_are_exactly_the_old_tank(parts)
Tank build(parts, **kwargs)
None test_the_surface_cannot_outrun_the_wall_that_warms_it(parts)
None test_a_superheat_under_the_onset_warms_instead_of_boiling(parts)
None test_the_wall_heat_that_goes_to_the_bulk_boils_nothing_at_the_surface(parts)
None test_the_surface_boils_at_saturation_instead_of_running_to_the_critical_point(parts)
shut(Tank tank, parts, *, float wall_above, float pressure=101325.0)
None test_nucleate_boiling_takes_over_under_the_leidenfrost_point(parts)
None test_a_shut_lox_tank_with_warm_hardware_climbs_at_tens_of_psi_a_minute(parts)