16 KiB
Cryogenic LN2 Tank Simulation Module -- Design Specification
Date: 2026-04-16 Status: Approved
1. Overview
A transient thermodynamic simulation module for a cryogenic liquid nitrogen (LN2) storage tank with helium pressurization. The module is an independent Python package within the existing pipe-system-simulation project, designed for future coupling with the pipe system.
1.1 Physical Scenario
A cylindrical cryogenic tank stores liquid nitrogen. It has:
- Two inlets: LN2 inlet and He pressurization inlet
- One outlet: LN2 outlet
- A heat leak interface for connecting insulation models
The tank pressure is maintained at a constant 0.17 MPa (absolute) by adjusting helium flow. Liquid nitrogen flows in at 1.144 kg/s (77 K) and out at 1.1895 kg/s. The helium inlet temperature is 100 K, and its flow rate is determined by the constant-pressure constraint.
1.2 Key Design Decisions (from brainstorming)
| Decision | Choice | Rationale |
|---|---|---|
| Relationship to pipe system | Independent module (A) | Can run standalone, future coupling possible |
| Tank model | Two-zone (liquid + ullage) (A) | Captures liquid/gas temperature difference |
| Interphase mass transfer | Not considered | Only heat transfer between liquid and gas zones |
| Fluid properties | CoolProp (C) | Highest accuracy for cryogenic conditions |
| Heat leak interface | Plugin-style (C) | Flexible, extensible, decoupled |
| Pressure control | Algebraic constraint (A) | Exact constant pressure, no PID tuning |
| Time integration | scipy solve_ivp (A) | Adaptive stepping, good accuracy |
| Liquid temperature | Uniform (homogeneous) | Justified by continuous flow and shallow liquid layer |
2. Physical Model
2.1 Two-Zone Model
The tank is divided into:
- Liquid zone (lower): LN2 at uniform temperature T_liq
- Ullage zone (upper): mixture of N2 vapor + He gas at uniform temperature T_ull
The two zones exchange heat across the liquid surface. No evaporation or condensation (no interphase mass transfer).
2.2 State Variables (ODE)
The ODE state vector has 3 components: y = [m_liq, U_liq, U_ull]
m_liq[kg]: liquid nitrogen massU_liq[J]: liquid zone total internal energyU_ull[J]: ullage zone total internal energy (N2 vapor + He combined)
2.3 Derived Quantities (not ODE states)
These are computed at each evaluation from the state + constraints:
T_liq: liquid temperature (from m_liq, U_liq via CoolProp)T_ull: ullage temperature (from U_ull, m_N2_ull, m_He via CoolProp/ideal gas)V_liq = m_liq / rho_LN2(T_liq, P): liquid volumeV_ull = V_total - V_liq: ullage volumeliquid_level = V_liq / A_cross: liquid height in the cylinderm_He: helium mass from constant-pressure constraint (see Section 2.6)m_dot_He: helium mass flow rate (time derivative of m_He, also from constraint)T_out = T_liq: LN2 outlet temperature (uniform assumption)
2.4 Liquid Zone Governing Equations
Mass conservation:
dm_liq/dt = m_dot_in_LN2 - m_dot_out_LN2
= 1.144 - 1.1895
= -0.0455 kg/s (constant)
Energy conservation:
dU_liq/dt = m_dot_in_LN2 * h_in_LN2
- m_dot_out_LN2 * h_liq
- Q_liq_to_ull
+ Q_leak_liq
Where:
h_in_LN2 = h_N2_liquid(T_in=77K, P=0.17MPa)from CoolProp [J/kg]h_liq = h_N2_liquid(T_liq, P=0.17MPa)from CoolProp [J/kg] -- also the outlet enthalpyQ_liq_to_ull: heat transfer from liquid to ullage across the liquid surface [W]Q_leak_liq: heat leak into liquid zone from environment [W]
2.5 Ullage Zone Governing Equations
Mass conservation:
m_N2_ull = constant (no mass transfer, set at initialization)
dm_He/dt = m_dot_He (determined by constant-pressure constraint)
Energy conservation:
dU_ull/dt = m_dot_He * h_in_He
+ Q_liq_to_ull
+ Q_leak_ull
Where:
h_in_He = h_He(T_in=100K, P=0.17MPa)from CoolProp [J/kg]Q_liq_to_ull: heat transfer from liquid surface (same magnitude, opposite sign as in liquid equation)Q_leak_ull: heat leak into ullage zone from environment [W]
2.6 Constant-Pressure Constraint (Analytical m_dot_He Derivation)
Tank pressure is maintained at P_total = 0.17 MPa at all times. The ullage gas follows Dalton's law:
P_total = P_N2 + P_He = 0.17 MPa
The N2 partial pressure depends on the fixed N2 vapor mass, ullage volume, and ullage temperature. The He mass required to provide the remaining pressure:
P_N2 = f(m_N2_ull, V_ull, T_ull) (CoolProp or ideal gas)
P_He = P_total - P_N2
m_He = P_He * V_ull / (R_He * T_ull) (He is well-approximated as ideal gas at these conditions)
Where R_He = R_universal / M_He = 8314.46 / 4.0026 = 2077.1 J/(kg*K).
m_He is NOT an ODE state variable. It is a derived quantity from the algebraic constraint.
Analytical derivation of m_dot_He (avoiding finite-difference instability):
Since P_total = const, differentiating dP/dt = 0 and using the ideal gas relation for He (P_He * V_ull = m_He * R_He * T_ull) yields:
m_He = P_He * V_ull / (R_He * T_ull)
dm_He/dt = (1 / R_He) * [ P_He * dV_ull/dt / T_ull
+ V_ull * dP_He/dt / T_ull
- P_He * V_ull * dT_ull/dt / T_ull^2 ]
The terms dV_ull/dt, dP_He/dt, and dT_ull/dt can all be expressed analytically in terms of the current state and known quantities:
dV_ull/dt = -dV_liq/dt = (m_dot_out - m_dot_in) / rho_liq(from liquid mass balance)dP_He/dt = -dP_N2/dt(since P_total is constant); dP_N2/dt is computed from the N2 ideal gas law with fixed m_N2_ull and known dV_ull/dt, dT_ull/dtdT_ull/dtis obtained from the ullage energy equation (which itself contains m_dot_He)
This creates a linear equation in m_dot_He that can be solved explicitly within each RHS evaluation. The key insight: dU_ull/dt = m_dot_He * h_in_He + Q_terms, and T_ull is a function of U_ull, so dT_ull/dt is linearly related to m_dot_He. Substituting into the dm_He/dt expression and solving for m_dot_He yields a closed-form formula with no finite differences, ensuring numerical stability with adaptive ODE solvers.
Implementation note: The analytical derivation will be implemented in
tank_model.py as a dedicated method _solve_he_flow_rate() that returns
m_dot_He as a function of the current state. This avoids the "algebraic loop"
issue identified in review.
2.7 Heat Transfer Models
Liquid-to-ullage surface heat transfer:
Q_liq_to_ull = h_conv * A_surface * (T_liq - T_ull)
A_surface: liquid surface area = cross-sectional area of cylinder = pi/4 * D^2h_conv: surface convective heat transfer coefficient [W/(m^2K)], configurable, default = 50 W/(m^2K)
Heat leak from environment (plugin interface): Total heat leak Q_leak is computed by the configured HeatLeakModel, then distributed to liquid and ullage zones by wetted area ratio:
Q_leak = heat_leak_model.compute(T_inner, T_env)
Q_leak_liq = Q_leak * A_wet / A_total
Q_leak_ull = Q_leak * A_dry / A_total
Where:
A_wet = A_bottom + pi * D * liquid_level(bottom cap + wetted side wall)A_dry = A_top + pi * D * (H - liquid_level)(top cap + dry side wall)A_total = A_wet + A_dryT_innerpassed to the model is a weighted average or conservative choice (e.g., T_liq for wet, T_ull for dry -- or simplified to just T_liq since most heat goes into the liquid)
Simplification: for the heat leak model input, use T_liq as T_inner since the liquid dominates thermal mass.
3. Heat Leak Interface
3.1 Base Class
class HeatLeakModel:
def compute(self, T_inner: float, T_env: float) -> float:
"""Return total heat leak Q [W], positive = heat flows into tank."""
raise NotImplementedError
3.2 MLI (Vacuum Multi-Layer Insulation)
class MLIHeatLeak(HeatLeakModel):
def __init__(self, A_total, q_mli=1.0):
"""
A_total: total tank surface area [m^2]
q_mli: specific heat flux [W/m^2], default 1.0 W/m^2 (typical MLI performance)
"""
Computes: Q = A_total * q_mli
Note: MLI heat flux is largely independent of temperature difference in the typical operating range, so q_mli is a fixed parameter.
3.3 Foam/Wrap Insulation
class FoamHeatLeak(HeatLeakModel):
def __init__(self, A_total, k_eff, delta):
"""
A_total: total tank surface area [m^2]
k_eff: effective thermal conductivity [W/(m*K)]
Can be a float (constant) or a callable k_eff(T) -> float
that returns conductivity as a function of temperature.
At cryogenic temperatures, k varies significantly with T.
delta: insulation thickness [m]
"""
Computes: Q = A_total * k(T_mean) * (T_env - T_inner) / delta
Where T_mean = (T_env + T_inner) / 2 when k_eff is a function, or simply
uses the constant value when k_eff is a float.
4. Tank Geometry
Cylindrical tank:
- Total volume: V = 420.1 L = 0.4201 m^3
- Height: H = 0.5 m
- Cross-sectional area: A = V / H = 0.8402 m^2
- Diameter: D = sqrt(4*A/pi) = 1.034 m
- Side area: A_side = pi * D * H = 1.625 m^2
- Top area = Bottom area = A = 0.8402 m^2
- Total surface area: A_total = A_side + 2*A = 3.306 m^2
Liquid level at any time:
liquid_level = V_liq / A = (m_liq / rho_liq) / A
fill_fraction = liquid_level / H
5. Initial Conditions
| Quantity | Value | Notes |
|---|---|---|
| T_liq(0) | 78 K | Given |
| T_ull(0) | 78 K | Given (same as liquid initially) |
| P_total | 0.17 MPa | Constant throughout |
| Ullage fraction | 30% | Gas pocket volume / total volume |
| V_liq(0) | 0.2941 m^3 | 70% of 0.4201 |
| V_ull(0) | 0.1260 m^3 | 30% of 0.4201 |
| m_liq(0) | rho_LN2(78K, 0.17MPa) * 0.2941 | From CoolProp |
| U_liq(0) | m_liq(0) * u_LN2(78K, 0.17MPa) | Specific internal energy from CoolProp |
| m_N2_ull | rho_N2_vapor(78K, P_N2_sat) * V_ull(0) | N2 vapor at initial conditions, FIXED for all time |
| P_N2(0) | N2 saturation pressure at 78K | From CoolProp |
| P_He(0) | P_total - P_N2(0) | Helium makes up the pressure difference |
| m_He(0) | P_He(0) * V_ull(0) / (R_He * 78) | Ideal gas for He |
| U_ull(0) | m_N2_ull * u_N2_vapor(78K) + m_He(0) * u_He(78K) | Combined internal energy |
6. Inlet/Outlet Conditions
| Port | Flow rate | Temperature | Pressure | Fluid |
|---|---|---|---|---|
| LN2 inlet | 1.144 kg/s | 77 K | 0.17 MPa | Liquid nitrogen |
| LN2 outlet | 1.1895 kg/s | T_liq (computed) | 0.17 MPa | Liquid nitrogen |
| He inlet | m_dot_He (computed) | 100 K | 0.17 MPa | Helium gas |
Net liquid drain rate: 0.0455 kg/s.
7. Numerical Method
- Time integration: scipy.integrate.solve_ivp with RK45 (adaptive Runge-Kutta)
- Tolerances: rtol = 1e-8, atol = 1e-10
- Simulation duration: t_end = 3600 s (1 hour)
- Dense output: enabled for smooth interpolation of results
- State vector: y = [m_liq, U_liq, U_ull] (3 components)
RHS function evaluation at each call:
- Unpack y -> (m_liq, U_liq, U_ull)
- Compute T_liq from (m_liq, U_liq) via CoolProp
- Compute V_liq, V_ull, liquid_level from geometry
- Compute m_He from constant-pressure constraint
- Compute T_ull from (U_ull, m_N2_ull, m_He) -- iterative or CoolProp
- Compute all heat transfer terms (Q_liq_to_ull, Q_leak_liq, Q_leak_ull)
- Compute enthalpy terms for inlets/outlet
- Assemble and return dy/dt = [dm_liq/dt, dU_liq/dt, dU_ull/dt]
8. Code Structure
src/cryo_tank/
__init__.py
config.py # Tank parameters, inlet/outlet conditions, simulation control
properties.py # CoolProp wrappers for N2 and He properties
heat_leak.py # HeatLeakModel base + MLIHeatLeak + FoamHeatLeak
tank_model.py # CryoTank class: geometry, state, rhs(), derived quantities
solver.py # run(tank, t_end) -> history dict
output.py # Plotting and reporting
main.py # Entry point
Module dependencies:
main.py -> config.py, tank_model.py, solver.py, output.py
tank_model.py -> properties.py, heat_leak.py
solver.py -> tank_model.py (calls tank.rhs)
output.py -> (only depends on history data dict)
8.1 CoolProp Performance Optimization (properties.py)
CoolProp Python calls can be slow when invoked thousands of times in an ODE RHS.
The following optimizations are mandatory in properties.py:
-
Use CoolProp.AbstractState: Create persistent
AbstractStateobjects for N2 and He at module load time. Reuse them across calls (avoid per-call overhead). -
Lookup table with interpolation: For the dominant hot-path properties (LN2 density, enthalpy, internal energy at P = 0.17 MPa as a function of T), build a 1D interpolation table at startup covering the expected temperature range (e.g., 70-90 K for liquid, 70-300 K for gas). Use
numpy.interpfor fast evaluation. Fall back to CoolProp only when T is outside the table range. -
He as ideal gas: Helium at 0.17 MPa and 78-300 K is well-described by the ideal gas law. Use analytical expressions (cp, cv, h, u) instead of CoolProp for He wherever possible to avoid unnecessary library calls.
9. Output Quantities
All recorded as time series:
| Output | Symbol | Unit |
|---|---|---|
| Time | t | s |
| Liquid temperature | T_liq | K |
| Ullage temperature | T_ull | K |
| Liquid mass | m_liq | kg |
| Ullage N2 vapor mass | m_N2_ull | kg (constant) |
| Helium mass | m_He | kg |
| Helium flow rate | m_dot_He | kg/s |
| Liquid volume / level | V_liq, liquid_level | m^3, m |
| Fill fraction | fill_fraction | -- |
| LN2 outlet temperature | T_out = T_liq | K |
| Heat leak total | Q_leak | W |
| Heat leak to liquid | Q_leak_liq | W |
| Heat leak to ullage | Q_leak_ull | W |
| Surface heat transfer | Q_liq_to_ull | W |
| Tank pressure | P_total | Pa (constant 0.17 MPa) |
Output files:
- Time-series plots (PNG): T_liq & T_ull vs t, liquid level vs t, m_dot_He vs t, heat fluxes vs t
- History data (NPZ): all time series for post-processing
- Summary report (HTML): key parameters and embedded figures
10. Constraints, Limits, and Robustness
10.1 Normal Operating Limits
- Tank must not be overpressurized: P_total <= 0.8 MPa (max bearing pressure). Should raise warning/error if pressure constraint cannot be maintained.
- Liquid level must remain >= 0. When m_liq reaches 0, simulation should stop (use solve_ivp
eventsmechanism to detect zero-crossing).
10.2 Near-Full Tank (fill_fraction > 95%)
When the ullage volume becomes very small, pressure sensitivity to mass/temperature changes grows dramatically. This can cause ODE solver instability.
Handling: when fill_fraction > 0.95, log a warning. The solver should still function because the analytical m_dot_He derivation avoids the finite-difference instability. If the solver fails to converge, reduce rtol/atol or switch to a stiffer solver (e.g., Radau).
10.3 Helium Backflow (m_dot_He < 0)
If the constant-pressure constraint computes m_dot_He < 0 (meaning pressure is too high and He should flow out), this indicates the operating regime has changed (e.g., heat leak is raising ullage temperature/pressure faster than liquid draining creates space). Two modes:
- Strict mode (default): Clamp m_dot_He = 0, allow pressure to drift above P_target. Log a warning with the overpressure magnitude. If P > 0.8 MPa (max bearing pressure), terminate simulation with an error.
- Vent mode (future): Add a pressure relief mechanism. Not implemented in v1.
10.4 Model Applicability (No Mass Transfer Assumption)
The current model assumes no evaporation/condensation between liquid and gas zones. This is valid when:
- Liquid temperature remains well below the saturation temperature at 0.17 MPa (~83.7 K)
- The net liquid drain is fast relative to temperature rise from heat leak
If T_liq approaches saturation temperature, the model will log a warning: "T_liq approaching saturation (83.7 K); evaporation effects may be significant."
Future extension: add Hertz-Knudsen evaporation model as an optional feature.
11. Testing Strategy
- Unit tests for properties.py: verify CoolProp wrappers return physically reasonable values
- Unit tests for heat_leak.py: verify MLI and Foam models compute correct Q for known inputs
- Unit tests for tank_model.py: verify geometry calculations, initial state, RHS evaluation
- Integration test: short run (10s), verify mass conservation (liquid mass change = net flow * dt)
- Steady-state test: with zero net flow and zero heat leak, verify state remains constant