fix(cryo_tank): use helium-only CoolProp pressure model

This commit is contained in:
lujingze committed 2026-06-08 04:26:56 +00:00
1 parent 4a22ef1334
commit 610ae39ee4
9 files changed
+126 -197

No files matched your search

+4 -5
View File
@@ -46,7 +46,7 @@ OUTPUT_DIR = "results/cryo_tank"
- `cryo_tank_level.png`:液位和充满率曲线。 - `cryo_tank_level.png`:液位和充满率曲线。
- `cryo_tank_he_flow.png`:氦气质量和氦气流量曲线。 - `cryo_tank_he_flow.png`:氦气质量和氦气流量曲线。
- `cryo_tank_heat.png`:漏热和气液换热曲线。 - `cryo_tank_heat.png`:漏热和气液换热曲线。
- `cryo_tank_pressure.png`:总压、氮气分压和氦气分压曲线。 - `cryo_tank_pressure.png`:总压和氦气气枕压力曲线。
如果没有看到 CSV 文件,先确认本地代码里是否有: 如果没有看到 CSV 文件,先确认本地代码里是否有:
@@ -105,7 +105,7 @@ y = [m_liq, U_liq, U_ull]
- `U_liq`:液相内能 - `U_liq`:液相内能
- `U_ull`:气枕区总内能 - `U_ull`:气枕区总内能
`derive(y)` 会根据状态量计算温度、体积、液位、充满率、氦气质量、氮气分压和氦气分压等派生量。 `derive(y)` 会根据状态量计算温度、体积、液位、充满率、氦气质量和氦气气枕压力等派生量。
`rhs(t, y)` 是 ODE 右端函数,由 `scipy.integrate.solve_ivp` 调用。 `rhs(t, y)` 是 ODE 右端函数,由 `scipy.integrate.solve_ivp` 调用。
@@ -133,7 +133,7 @@ run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0)
- 液氮密度、焓、内能计算。 - 液氮密度、焓、内能计算。
- 氮气饱和压力和饱和蒸气内能计算。 - 氮气饱和压力和饱和蒸气内能计算。
- 氦气理想气体焓、内能、比热计算。 - 氦气密度、焓、内能、比热计算。
- 建立液氮内能到温度的查表插值,加速 ODE 求解。 - 建立液氮内能到温度的查表插值,加速 ODE 求解。
该模块依赖 `CoolProp`。 该模块依赖 `CoolProp`。
@@ -348,7 +348,6 @@ CSV 第一行是列名,列名来自 `history` 字典,包括:
- `V_ull` - `V_ull`
- `liquid_level` - `liquid_level`
- `fill_fraction` - `fill_fraction`
- `P_N2`
- `P_He` - `P_He`
- `P_total` - `P_total`
- `Q_leak` - `Q_leak`
@@ -373,7 +372,7 @@ pytest -q
## 注意事项 ## 注意事项
- 本模型当前将储箱分为液相区和气枕区两个区域,不是完整 CFD 模型。 - 本模型当前将储箱分为液相区和气枕区两个区域,不是完整 CFD 模型。
- 气枕区氮气和氦气采用简化处理,氦气按理想气体热力学关系计算。 - 气枕区按纯氦气处理,物性由 CoolProp 计算。
- 液氮物性依赖 `CoolProp`,缺少该包会导致程序无法启动。 - 液氮物性依赖 `CoolProp`,缺少该包会导致程序无法启动。
- `results/` 目录默认被 `.gitignore` 忽略,仿真输出不会自动上传到 Git。 - `results/` 目录默认被 `.gitignore` 忽略,仿真输出不会自动上传到 Git。
- 建议每次修改参数后保存对应工况说明,避免不同仿真结果混在同一个输出目录中。 - 建议每次修改参数后保存对应工况说明,避免不同仿真结果混在同一个输出目录中。
-7
View File
@@ -5,13 +5,6 @@ Pure data module -- no functions, no side effects.
""" """
import math import math
# ---------- Gas constants ----------
R_UNIVERSAL = 8314.46 # J/(kmol*K)
M_N2 = 28.014 # kg/kmol
M_HE = 4.0026 # kg/kmol
R_HE = R_UNIVERSAL / M_HE # 2077.1 J/(kg*K)
R_N2 = R_UNIVERSAL / M_N2 # 296.8 J/(kg*K)
# ---------- Tank geometry ---------- # ---------- Tank geometry ----------
V_TOTAL = 420.1e-3 # m^3 (420.1 L) V_TOTAL = 420.1e-3 # m^3 (420.1 L)
H_TANK = 0.5 # m (cylinder height) H_TANK = 0.5 # m (cylinder height)
+1 -1
View File
@@ -49,7 +49,7 @@ def main():
print(f" T_liq = {info0['T_liq']:.2f} K, T_ull = {info0['T_ull']:.2f} K") print(f" T_liq = {info0['T_liq']:.2f} K, T_ull = {info0['T_ull']:.2f} K")
print(f" fill_fraction = {info0['fill_fraction']:.1%}") print(f" fill_fraction = {info0['fill_fraction']:.1%}")
print(f" m_He = {info0['m_He']*1000:.2f} g") print(f" m_He = {info0['m_He']*1000:.2f} g")
print(f" P_N2 = {info0['P_N2']/1e6:.4f} MPa, P_He = {info0['P_He']/1e6:.4f} MPa") print(f" P_He = {info0['P_He']/1e6:.4f} MPa")
print(f"Running to t_end = {T_END:.0f} s ...") print(f"Running to t_end = {T_END:.0f} s ...")
print() print()
+1 -2
View File
@@ -116,12 +116,11 @@ def plot_heat_fluxes(history, path):
def plot_pressure(history, path): def plot_pressure(history, path):
"""Plot tank pressure (P_total, P_N2, P_He) vs time.""" """Plot tank pressure (P_total and P_He) vs time."""
fig, ax = plt.subplots(figsize=(10, 5)) fig, ax = plt.subplots(figsize=(10, 5))
t = history['t'] t = history['t']
ax.plot(t, history['P_total'] / 1e6, label='P_total', linewidth=2) ax.plot(t, history['P_total'] / 1e6, label='P_total', linewidth=2)
ax.plot(t, history['P_N2'] / 1e6, label='P_N2', linestyle='--')
ax.plot(t, history['P_He'] / 1e6, label='P_He', linestyle='--') ax.plot(t, history['P_He'] / 1e6, label='P_He', linestyle='--')
ax.set_xlabel('Time [s]') ax.set_xlabel('Time [s]')
ax.set_ylabel('Pressure [MPa]') ax.set_ylabel('Pressure [MPa]')
+40 -63
View File
@@ -1,13 +1,11 @@
# src/cryo_tank/properties.py # src/cryo_tank/properties.py
""" """
Fluid property wrappers for liquid nitrogen, N2 vapor, and helium. Fluid property wrappers for liquid nitrogen and helium.
Performance strategy (per spec Section 8.1): Performance strategy:
- N2 (liquid & vapor): CoolProp with persistent AbstractState objects - N2 liquid: CoolProp with a persistent AbstractState object
- He: analytical ideal gas (cp=5196.2 J/(kg*K), cv=3117.1 J/(kg*K)) - He: CoolProp with a persistent AbstractState object
- Lookup tables for the ODE hot path (built at import time)
""" """
import numpy as np
import CoolProp.CoolProp as CP import CoolProp.CoolProp as CP
from CoolProp import AbstractState from CoolProp import AbstractState
@@ -41,81 +39,60 @@ def ln2_u(T, P):
def ln2_T_from_u(u, P): def ln2_T_from_u(u, P):
"""Recover LN2 temperature from specific internal energy [K]. """Recover LN2 temperature from pressure and specific internal energy [K]."""
_n2_state.update(CP.PUmass_INPUTS, P, u)
Uses lookup table interpolation for speed; falls back to CoolProp return _n2_state.T()
if outside the table range.
"""
return float(np.interp(u, _ln2_u_table, _ln2_T_table))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# N2 vapor properties (at saturation or specified conditions) # Helium properties via CoolProp
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def n2_sat_pressure(T): _HE_P_REF = 170000.0
"""N2 saturation pressure [Pa] at temperature T."""
_n2_state.update(CP.QT_INPUTS, 1.0, T)
return _n2_state.p()
def n2_vapor_u(T): def he_rho(T, P=_HE_P_REF):
"""N2 saturated vapor specific internal energy [J/kg] at temperature T.""" """He density [kg/m^3]."""
_n2_state.update(CP.QT_INPUTS, 1.0, T) _he_state.update(CP.PT_INPUTS, P, T)
return _n2_state.umass() return _he_state.rhomass()
def n2_vapor_rho(T): def he_cp(T=100.0, P=_HE_P_REF):
"""N2 saturated vapor density [kg/m^3] at temperature T."""
_n2_state.update(CP.QT_INPUTS, 1.0, T)
return _n2_state.rhomass()
# ---------------------------------------------------------------------------
# Helium properties (ideal gas: cp=5/2 R, cv=3/2 R, monatomic)
# ---------------------------------------------------------------------------
_HE_CP = 5196.2 # J/(kg*K), = 5/2 * R_He
_HE_CV = 3117.1 # J/(kg*K), = 3/2 * R_He
# Reference state: CoolProp He at T_ref=0K gives u_ref, h_ref
# We match CoolProp's reference by computing offset at a known point.
_he_state.update(CP.PT_INPUTS, 170000.0, 100.0)
_HE_H_REF = _he_state.hmass() - _HE_CP * 100.0 # h = cp*T + h_ref
_HE_U_REF = _he_state.umass() - _HE_CV * 100.0 # u = cv*T + u_ref
def he_cp():
"""He specific heat at constant pressure [J/(kg*K)].""" """He specific heat at constant pressure [J/(kg*K)]."""
return _HE_CP _he_state.update(CP.PT_INPUTS, P, T)
return _he_state.cpmass()
def he_cv(): def he_cv(T=100.0, P=_HE_P_REF):
"""He specific heat at constant volume [J/(kg*K)].""" """He specific heat at constant volume [J/(kg*K)]."""
return _HE_CV _he_state.update(CP.PT_INPUTS, P, T)
return _he_state.cvmass()
def he_h(T): def he_h(T, P=_HE_P_REF):
"""He specific enthalpy [J/kg] (ideal gas).""" """He specific enthalpy [J/kg]."""
return _HE_CP * T + _HE_H_REF _he_state.update(CP.PT_INPUTS, P, T)
return _he_state.hmass()
def he_u(T): def he_u(T, P=_HE_P_REF):
"""He specific internal energy [J/kg] (ideal gas).""" """He specific internal energy [J/kg]."""
return _HE_CV * T + _HE_U_REF _he_state.update(CP.PT_INPUTS, P, T)
return _he_state.umass()
def he_T_from_u(u): def he_T_from_u(u, P=_HE_P_REF):
"""Recover He temperature from specific internal energy [K].""" """Recover He temperature from pressure and specific internal energy [K]."""
return (u - _HE_U_REF) / _HE_CV _he_state.update(CP.PUmass_INPUTS, P, u)
return _he_state.T()
# --------------------------------------------------------------------------- def he_T_from_rho(P, rho):
# Lookup table for LN2: u(T) -> T at P = 0.17 MPa (built at import time) """Recover He temperature from pressure and density [K]."""
# --------------------------------------------------------------------------- _he_state.update(CP.DmassP_INPUTS, rho, P)
_LN2_T_MIN = 65.0 return _he_state.T()
_LN2_T_MAX = 82.0 # stay below saturation at 0.17 MPa (~82.03 K)
_LN2_TABLE_N = 200
_P_WORK = 170000.0
_ln2_T_table = np.linspace(_LN2_T_MIN, _LN2_T_MAX, _LN2_TABLE_N)
_ln2_u_table = np.array([ln2_u(T, _P_WORK) for T in _ln2_T_table]) def he_u_from_rho(P, rho):
# _ln2_u_table is monotonically increasing, so np.interp works for inverse lookup """Recover He specific internal energy from pressure and density [J/kg]."""
_he_state.update(CP.DmassP_INPUTS, rho, P)
return _he_state.umass()
+4 -8
View File
@@ -80,7 +80,6 @@ def run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0):
'V_ull': np.zeros(n), 'V_ull': np.zeros(n),
'liquid_level': np.zeros(n), 'liquid_level': np.zeros(n),
'fill_fraction': np.zeros(n), 'fill_fraction': np.zeros(n),
'P_N2': np.zeros(n),
'P_He': np.zeros(n), 'P_He': np.zeros(n),
'P_total': np.zeros(n), 'P_total': np.zeros(n),
'Q_leak': np.zeros(n), 'Q_leak': np.zeros(n),
@@ -100,9 +99,8 @@ def run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0):
history['V_ull'][i] = info['V_ull'] history['V_ull'][i] = info['V_ull']
history['liquid_level'][i] = info['liquid_level'] history['liquid_level'][i] = info['liquid_level']
history['fill_fraction'][i] = info['fill_fraction'] history['fill_fraction'][i] = info['fill_fraction']
history['P_N2'][i] = info['P_N2']
history['P_He'][i] = info['P_He'] history['P_He'][i] = info['P_He']
history['P_total'][i] = info['P_N2'] + info['P_He'] history['P_total'][i] = info['P_He']
# Recompute heat terms for recording # Recompute heat terms for recording
T_liq = info['T_liq'] T_liq = info['T_liq']
@@ -120,11 +118,9 @@ def run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0):
history['Q_leak'][i] = Q_leak history['Q_leak'][i] = Q_leak
history['Q_leak_liq'][i] = Q_leak_liq history['Q_leak_liq'][i] = Q_leak_liq
history['Q_leak_ull'][i] = Q_leak_ull history['Q_leak_ull'][i] = Q_leak_ull
history['mdot_He'][i] = tank._solve_he_flow_rate(
info, Q_liq_to_ull, Q_leak_ull
)
# He flow rate via finite difference on m_He (post-processing only, not in RHS)
dt = np.diff(t)
dm_He = np.diff(history['m_He'])
history['mdot_He'][0] = dm_He[0] / dt[0] if len(dt) > 0 else 0.0
history['mdot_He'][1:] = dm_He / dt
return history return history
+72 -93
View File
@@ -10,8 +10,8 @@ import math
import warnings import warnings
import numpy as np import numpy as np
from scipy.optimize import brentq
from cryo_tank.config import R_HE, R_N2
from cryo_tank import properties as prop from cryo_tank import properties as prop
@@ -77,21 +77,14 @@ class CryoTank:
self._m_liq_0 = rho_liq_0 * V_liq_0 self._m_liq_0 = rho_liq_0 * V_liq_0
self._U_liq_0 = self._m_liq_0 * prop.ln2_u(T_init, P_work) self._U_liq_0 = self._m_liq_0 * prop.ln2_u(T_init, P_work)
# Ullage initial state: N2 vapor at saturation + He to fill pressure # Ullage initial state: helium pressurization only.
# Use ideal gas law for N2 mass (consistent with derive/rhs which # Nitrogen evaporation is intentionally not modeled, so the ullage
# treat ullage N2 as ideal gas: P_N2 = m_N2 * R_N2 * T / V) # pressure is provided entirely by helium and the liquid sees the same
P_N2_0 = prop.n2_sat_pressure(T_init) # tank pressure for property lookup.
self.m_N2_ull = P_N2_0 * V_ull_0 / (R_N2 * T_init) # FIXED for all time self._m_He_0 = prop.he_rho(T_init, P_work) * V_ull_0
P_He_0 = P_work - P_N2_0 U_He_0 = self._m_He_0 * prop.he_u(T_init, P_work)
self._m_He_0 = P_He_0 * V_ull_0 / (R_HE * T_init) self._U_ull_0 = U_He_0
U_N2_ull_0 = self.m_N2_ull * prop.n2_vapor_u(T_init)
U_He_0 = self._m_He_0 * prop.he_u(T_init)
self._U_ull_0 = U_N2_ull_0 + U_He_0
# Saturation temperature warning threshold
self._T_sat = 82.0 # approximate, from CoolProp: ~82.03 K at 0.17 MPa
def initial_state(self): def initial_state(self):
"""Return the ODE initial state vector y0 = [m_liq, U_liq, U_ull].""" """Return the ODE initial state vector y0 = [m_liq, U_liq, U_ull]."""
@@ -108,7 +101,8 @@ class CryoTank:
"""Compute all derived quantities from state vector y. """Compute all derived quantities from state vector y.
Returns a dict with T_liq, T_ull, V_liq, V_ull, liquid_level, Returns a dict with T_liq, T_ull, V_liq, V_ull, liquid_level,
fill_fraction, m_He, P_N2, P_He, etc. fill_fraction, m_He, P_He, etc. Helium provides the full ullage
pressure in this no-evaporation model.
""" """
m_liq, U_liq, U_ull = y[0], y[1], y[2] m_liq, U_liq, U_ull = y[0], y[1], y[2]
@@ -126,62 +120,53 @@ class CryoTank:
# Solve T_ull from ullage energy # Solve T_ull from ullage energy
T_ull = self._solve_ullage_temperature(U_ull, V_ull) T_ull = self._solve_ullage_temperature(U_ull, V_ull)
# Recompute P_N2 and m_He with correct T_ull # No nitrogen evaporation: helium provides all gas pressure.
P_N2 = self.m_N2_ull * R_N2 * T_ull / V_ull P_He = self.P_work
P_He = self.P_work - P_N2 m_He = prop.he_rho(T_ull, P_He) * V_ull
m_He = P_He * V_ull / (R_HE * T_ull)
return { return {
'T_liq': T_liq, 'T_ull': T_ull, 'T_liq': T_liq, 'T_ull': T_ull,
'V_liq': V_liq, 'V_ull': V_ull, 'V_liq': V_liq, 'V_ull': V_ull,
'liquid_level': liquid_level, 'fill_fraction': fill_fraction, 'liquid_level': liquid_level, 'fill_fraction': fill_fraction,
'rho_liq': rho_liq, 'rho_liq': rho_liq,
'm_He': m_He, 'P_N2': P_N2, 'P_He': P_He, 'm_He': m_He, 'P_He': P_He,
} }
def _solve_ullage_temperature(self, U_ull, V_ull): def _solve_ullage_temperature(self, U_ull, V_ull):
"""Solve for T_ull given total ullage internal energy and volume. """Solve pure-He ullage temperature using CoolProp EOS.
U_ull = m_N2_ull * u_N2_vap(T) + m_He(T) * u_He(T) At fixed tank pressure, T is obtained from:
where m_He(T) = (P_work - m_N2_ull * R_N2 * T / V_ull) * V_ull / (R_He * T) U_ull = rho_He(P_work, T) * V_ull * u_He(P_work, T)
Solved by Newton iteration.
""" """
T = self._T_init # initial guess def residual(T):
for _ in range(50): rho = prop.he_rho(T, self.P_work)
P_N2 = self.m_N2_ull * R_N2 * T / V_ull u = prop.he_u(T, self.P_work)
P_He = self.P_work - P_N2 return rho * V_ull * u - U_ull
if P_He < 0:
P_He = 0.0
m_He = P_He * V_ull / (R_HE * T)
# N2 vapor internal energy (ideal gas approx): u = cv_N2 * T + u_ref T_grid = np.geomspace(20.0, 1000.0, 160)
cv_N2 = 743.0 T_prev = T_grid[0]
u_N2_ref = prop.n2_vapor_u(78.0) - cv_N2 * 78.0 f_prev = residual(T_prev)
u_N2 = cv_N2 * T + u_N2_ref if abs(f_prev) < 1e-8:
return float(T_prev)
U_calc = self.m_N2_ull * u_N2 + m_He * prop.he_u(T) best_T = T_prev
residual = U_calc - U_ull best_abs_f = abs(f_prev)
for T in T_grid[1:]:
f = residual(T)
if abs(f) < best_abs_f:
best_T = T
best_abs_f = abs(f)
if f_prev * f <= 0.0:
return brentq(residual, T_prev, T, xtol=1e-8, rtol=1e-10)
T_prev = T
f_prev = f
if abs(residual) < 1e-3: # converged (< 1 mJ) warnings.warn(
return T "Unable to bracket ullage temperature from CoolProp He state: "
f"U_ull={U_ull:.6g}, V_ull={V_ull:.6g}; "
# Numerical derivative f"using nearest T={best_T:.6g} K"
dT = 0.01 )
P_N2_p = self.m_N2_ull * R_N2 * (T + dT) / V_ull return float(best_T)
P_He_p = max(0.0, self.P_work - P_N2_p)
m_He_p = P_He_p * V_ull / (R_HE * (T + dT))
u_N2_p = cv_N2 * (T + dT) + u_N2_ref
U_calc_p = self.m_N2_ull * u_N2_p + m_He_p * prop.he_u(T + dT)
dU_dT = (U_calc_p - U_calc) / dT
if abs(dU_dT) < 1e-20:
break
T = T - residual / dU_dT
T = max(50.0, min(T, 400.0)) # clamp
warnings.warn(f"_solve_ullage_temperature did not converge: T={T:.2f}, residual={residual:.2e}")
return T
def rhs(self, t, y): def rhs(self, t, y):
"""ODE right-hand side: dy/dt = [dm_liq/dt, dU_liq/dt, dU_ull/dt]. """ODE right-hand side: dy/dt = [dm_liq/dt, dU_liq/dt, dU_ull/dt].
@@ -220,58 +205,52 @@ class CryoTank:
# --- Ullage zone --- # --- Ullage zone ---
dU_ull_dt = mdot_He * self.h_in_he + Q_liq_to_ull + Q_leak_ull dU_ull_dt = mdot_He * self.h_in_he + Q_liq_to_ull + Q_leak_ull
# --- Warnings ---
if T_liq > self._T_sat - 1.0:
warnings.warn(
f"T_liq={T_liq:.2f}K approaching saturation ({self._T_sat:.1f}K); "
"evaporation effects may be significant."
)
return np.array([dm_liq_dt, dU_liq_dt, dU_ull_dt]) return np.array([dm_liq_dt, dU_liq_dt, dU_ull_dt])
def _solve_he_flow_rate(self, info, Q_liq_to_ull, Q_leak_ull): def _solve_he_flow_rate(self, info, Q_liq_to_ull, Q_leak_ull):
"""Analytically solve for m_dot_He from dP/dt = 0 constraint. """Solve He inlet mass flow from constant-pressure CoolProp EOS.
The key equation: P_total = P_N2 + P_He = const. The ullage is pure He at P_work. With V_ull changing due to liquid
volume change, m_dot_He is found by differentiating:
P_N2 = m_N2_ull * R_N2 * T_ull / V_ull (N2 ideal gas, m_N2_ull = const) m(T, V) = rho(P_work, T) * V
P_He = m_He * R_HE * T_ull / V_ull U(T, V) = m(T, V) * u(P_work, T)
and combining it with dU/dt = m_dot_He*h_in + Q_ull.
dP/dt = 0 implies dP_He/dt = -dP_N2/dt.
Expanding and solving for m_dot_He gives a linear equation.
See spec Section 2.6 for full derivation.
""" """
T_ull = info['T_ull'] T_ull = info['T_ull']
V_ull = info['V_ull'] V_ull = info['V_ull']
m_He = info['m_He']
rho_liq = info['rho_liq'] rho_liq = info['rho_liq']
# dV_ull/dt = -dV_liq/dt = -dm_liq/dt / rho_liq = -(mdot_in - mdot_out) / rho_liq dV_ull_dt = -self.dm_liq_dt / rho_liq
dV_ull_dt = -self.dm_liq_dt / rho_liq # positive when liquid drains
# Total ullage heat input (excluding He inlet, which we're solving for)
Q_ull_no_he = Q_liq_to_ull + Q_leak_ull Q_ull_no_he = Q_liq_to_ull + Q_leak_ull
# Ullage total cv*mass (for dT_ull/dt estimation) def state_at(T, V):
cv_N2 = 743.0 # J/(kg*K), N2 vapor rho = prop.he_rho(T, self.P_work)
cv_He = prop.he_cv() m = rho * V
C_ull = self.m_N2_ull * cv_N2 + m_He * cv_He # total heat capacity [J/K] U = m * prop.he_u(T, self.P_work)
return m, U
R_mix = self.m_N2_ull * R_N2 + m_He * R_HE # effective "mR" [J/K] dT = max(1e-3, abs(T_ull) * 1e-5)
dV = max(1e-8, abs(V_ull) * 1e-5)
# Coefficient of m_dot_He in dP/dt = 0: m_plus, U_plus = state_at(T_ull + dT, V_ull)
# A * m_dot_He + B = 0 m_minus, U_minus = state_at(max(20.0, T_ull - dT), V_ull)
# m_dot_He = -B / A actual_dT = (T_ull + dT) - max(20.0, T_ull - dT)
A = R_HE * T_ull / V_ull + R_mix / (V_ull * C_ull) * self.h_in_he m_T = (m_plus - m_minus) / actual_dT
B = R_mix / (V_ull * C_ull) * Q_ull_no_he - R_mix * T_ull / (V_ull ** 2) * dV_ull_dt U_T = (U_plus - U_minus) / actual_dT
if abs(A) < 1e-30: m_plus, U_plus = state_at(T_ull, V_ull + dV)
m_minus, U_minus = state_at(T_ull, max(1e-12, V_ull - dV))
actual_dV = (V_ull + dV) - max(1e-12, V_ull - dV)
m_V = (m_plus - m_minus) / actual_dV
U_V = (U_plus - U_minus) / actual_dV
denominator = U_T - self.h_in_he * m_T
if abs(denominator) < 1e-30:
return 0.0 return 0.0
mdot_He = -B / A dT_dt = (Q_ull_no_he - (U_V - self.h_in_he * m_V) * dV_ull_dt) / denominator
mdot_He = m_T * dT_dt + m_V * dV_ull_dt
# Clamp: He can only flow in (strict mode per spec Section 10.3)
if mdot_He < 0: if mdot_He < 0:
warnings.warn( warnings.warn(
f"He backflow requested (m_dot_He={mdot_He:.4e} kg/s); " f"He backflow requested (m_dot_He={mdot_He:.4e} kg/s); "
+2 -15
View File
@@ -6,7 +6,6 @@ sys.path.insert(0, "src")
from cryo_tank.properties import ( from cryo_tank.properties import (
ln2_rho, ln2_h, ln2_u, ln2_T_from_u, ln2_rho, ln2_h, ln2_u, ln2_T_from_u,
n2_vapor_u, n2_sat_pressure,
he_u, he_h, he_cp, he_cv, he_u, he_h, he_cp, he_cv,
) )
from cryo_tank.config import P_WORKING from cryo_tank.config import P_WORKING
@@ -34,24 +33,12 @@ class TestLN2Properties:
assert abs(T_recovered - T_orig) < 0.01 assert abs(T_recovered - T_orig) < 0.01
class TestN2VaporProperties:
"""N2 vapor properties."""
def test_n2_sat_pressure_at_78K(self):
P_sat = n2_sat_pressure(78.0)
assert 0.10e6 < P_sat < 0.12e6 # ~0.1093 MPa
def test_n2_vapor_internal_energy_at_78K(self):
u = n2_vapor_u(78.0)
assert 50000 < u < 60000 # ~55547 J/kg
class TestHeliumProperties: class TestHeliumProperties:
"""Helium (ideal gas) properties.""" """Helium properties."""
def test_he_cp_near_5196(self): def test_he_cp_near_5196(self):
cp = he_cp() cp = he_cp()
assert abs(cp - 5196.2) < 10 # monatomic ideal gas assert abs(cp - 5196.2) < 10
def test_he_cv_near_3117(self): def test_he_cv_near_3117(self):
cv = he_cv() cv = he_cv()
+2 -3
View File
@@ -69,10 +69,9 @@ class TestInitialState:
assert abs(info['T_liq'] - T_INIT) < 0.1 assert abs(info['T_liq'] - T_INIT) < 0.1
assert abs(info['T_ull'] - T_INIT) < 1.0 assert abs(info['T_ull'] - T_INIT) < 1.0
def test_initial_pressure_components_sum_to_P_working(self): def test_initial_pressure_is_provided_entirely_by_helium(self):
tank = _make_tank() tank = _make_tank()
y0 = tank.initial_state() y0 = tank.initial_state()
info = tank.derive(y0) info = tank.derive(y0)
P_N2 = info['P_N2']
P_He = info['P_He'] P_He = info['P_He']
assert abs(P_N2 + P_He - P_WORKING) / P_WORKING < 1e-6 assert abs(P_He - P_WORKING) / P_WORKING < 1e-12