feat(cryo_tank): use ullage temperature state
This commit is contained in:
1 parent
610ae39ee4
commit
1cf8427b52
7 files changed
+571
-109
No files matched your search
@@ -0,0 +1,491 @@
|
||||
# cryo_tank 模型方程与 ODE 求解逻辑
|
||||
|
||||
本文只保留当前 `src/cryo_tank/` 模型内部实际使用的方程,以及 ODE 方程组的求解逻辑。当前版本采用方案二:把气枕温度 `T_ull` 作为 ODE 状态量,氦气质量 `m_He` 由恒压状态方程派生。
|
||||
|
||||
## 1. 模型假设
|
||||
|
||||
当前模型是液氮贮箱的两区集总参数模型:
|
||||
|
||||
- 液相区为液氮。
|
||||
- 气枕区为纯氦气。
|
||||
- 气枕压力固定为工作压力 `P_work`。
|
||||
- 不考虑氮气蒸发、冷凝和气液相间质量传递。
|
||||
- 氦气质量不是独立状态量,而是维持恒压所需的派生量。
|
||||
- 氦气只允许补入,不允许倒流;若计算得到负的氦气流量,则报告流量钳制为 0。
|
||||
|
||||
对应代码:
|
||||
|
||||
- 状态和派生量:`src/cryo_tank/tank_model.py:98-129`
|
||||
- ODE 右端项:`src/cryo_tank/tank_model.py:131-163`
|
||||
- 氦气补气温度方程:`src/cryo_tank/tank_model.py:165-214`
|
||||
|
||||
## 2. 状态向量
|
||||
|
||||
ODE 状态向量为:
|
||||
|
||||
```text
|
||||
y = [m_liq, U_liq, T_ull]
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
```text
|
||||
m_liq : 液氮质量 [kg]
|
||||
U_liq : 液相总内能 [J]
|
||||
T_ull : 气枕区温度 [K]
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:1-7`,`src/cryo_tank/tank_model.py:87-89`
|
||||
|
||||
## 3. 初始状态方程
|
||||
|
||||
初始液相体积:
|
||||
|
||||
```text
|
||||
V_liq_0 = (1 - ullage_fraction) * V_total
|
||||
```
|
||||
|
||||
初始气枕体积:
|
||||
|
||||
```text
|
||||
V_ull_0 = ullage_fraction * V_total
|
||||
```
|
||||
|
||||
初始液氮质量:
|
||||
|
||||
```text
|
||||
rho_liq_0 = rho_LN2(T_init, P_work)
|
||||
m_liq_0 = rho_liq_0 * V_liq_0
|
||||
```
|
||||
|
||||
初始液相总内能:
|
||||
|
||||
```text
|
||||
U_liq_0 = m_liq_0 * u_LN2(T_init, P_work)
|
||||
```
|
||||
|
||||
初始气枕温度:
|
||||
|
||||
```text
|
||||
T_ull_0 = T_init
|
||||
```
|
||||
|
||||
初始氦气质量是派生量:
|
||||
|
||||
```text
|
||||
m_He_0 = rho_He(T_init, P_work) * V_ull_0
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:71-85`
|
||||
|
||||
## 4. 几何方程
|
||||
|
||||
横截面积:
|
||||
|
||||
```text
|
||||
A_cross = V_total / H_tank
|
||||
```
|
||||
|
||||
圆柱直径:
|
||||
|
||||
```text
|
||||
D = sqrt(4 * A_cross / pi)
|
||||
```
|
||||
|
||||
侧面积:
|
||||
|
||||
```text
|
||||
A_side = pi * D * H_tank
|
||||
```
|
||||
|
||||
端盖面积:
|
||||
|
||||
```text
|
||||
A_cap = A_cross
|
||||
```
|
||||
|
||||
总面积:
|
||||
|
||||
```text
|
||||
A_total = A_side + 2 * A_cap
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:41-48`
|
||||
|
||||
液位对应的湿壁和干壁面积:
|
||||
|
||||
```text
|
||||
level = clamp(liquid_level, 0, H_tank)
|
||||
A_wet = A_cap + pi * D * level
|
||||
A_dry = A_cap + pi * D * (H_tank - level)
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:91-96`
|
||||
|
||||
## 5. 派生量方程
|
||||
|
||||
给定状态:
|
||||
|
||||
```text
|
||||
y = [m_liq, U_liq, T_ull]
|
||||
```
|
||||
|
||||
### 5.1 液相派生量
|
||||
|
||||
```text
|
||||
u_liq = U_liq / m_liq
|
||||
T_liq = T_LN2_from_u(u_liq, P_work)
|
||||
rho_liq = rho_LN2(T_liq, P_work)
|
||||
V_liq = m_liq / rho_liq
|
||||
liquid_level = V_liq / A_cross
|
||||
fill_fraction = liquid_level / H_tank
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:107-113`
|
||||
|
||||
### 5.2 气枕派生量
|
||||
|
||||
```text
|
||||
V_ull = V_total - V_liq
|
||||
P_He = P_work
|
||||
rho_He = rho_He(T_ull, P_He)
|
||||
m_He = rho_He * V_ull
|
||||
U_ull = m_He * u_He(T_ull, P_He)
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:115-121`
|
||||
|
||||
## 6. 换热方程
|
||||
|
||||
### 6.1 液相与气枕界面换热
|
||||
|
||||
```text
|
||||
Q_liq_to_ull = h_conv * A_cross * (T_liq - T_ull)
|
||||
```
|
||||
|
||||
符号约定:
|
||||
|
||||
```text
|
||||
Q_liq_to_ull > 0 : 热量从液相传给气枕
|
||||
Q_liq_to_ull < 0 : 热量从气枕传给液相
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:141-142`
|
||||
|
||||
### 6.2 外界总漏热
|
||||
|
||||
当前主程序默认使用 MLI 漏热模型:
|
||||
|
||||
```text
|
||||
Q_leak = A_total * q_mli
|
||||
```
|
||||
|
||||
代码位置:
|
||||
|
||||
- 默认创建:`src/cryo_tank/main.py:32-33`
|
||||
- MLI 方程:`src/cryo_tank/heat_leak.py:21-40`
|
||||
|
||||
代码中还提供 Foam 漏热模型:
|
||||
|
||||
```text
|
||||
T_mean = (T_inner + T_env) / 2
|
||||
Q_leak = A_total * k_eff(T_mean) * (T_env - T_inner) / delta
|
||||
```
|
||||
|
||||
如果 `k_eff` 是常数,则直接使用该常数。
|
||||
|
||||
代码位置:`src/cryo_tank/heat_leak.py:43-70`
|
||||
|
||||
### 6.3 漏热分配
|
||||
|
||||
总漏热按湿壁面积和干壁面积分配:
|
||||
|
||||
```text
|
||||
Q_leak_liq = Q_leak * A_wet / (A_wet + A_dry)
|
||||
Q_leak_ull = Q_leak * A_dry / (A_wet + A_dry)
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:144-148`
|
||||
|
||||
## 7. 液氮质量方程
|
||||
|
||||
液氮质量变化率:
|
||||
|
||||
```text
|
||||
dm_liq/dt = mdot_in_ln2 - mdot_out_ln2
|
||||
```
|
||||
|
||||
代码位置:
|
||||
|
||||
- 净流量预计算:`src/cryo_tank/tank_model.py:64-65`
|
||||
- ODE 中使用:`src/cryo_tank/tank_model.py:150-151`
|
||||
|
||||
## 8. 液相能量方程
|
||||
|
||||
入口液氮焓:
|
||||
|
||||
```text
|
||||
h_in_ln2 = h_LN2(T_in_ln2, P_work)
|
||||
```
|
||||
|
||||
当前液相出口焓:
|
||||
|
||||
```text
|
||||
h_liq = h_LN2(T_liq, P_work)
|
||||
```
|
||||
|
||||
液相总内能变化率:
|
||||
|
||||
```text
|
||||
dU_liq/dt =
|
||||
mdot_in_ln2 * h_in_ln2
|
||||
- mdot_out_ln2 * h_liq
|
||||
- Q_liq_to_ull
|
||||
+ Q_leak_liq
|
||||
```
|
||||
|
||||
代码位置:
|
||||
|
||||
- 入口焓预计算:`src/cryo_tank/tank_model.py:60-62`
|
||||
- 能量方程:`src/cryo_tank/tank_model.py:152-156`
|
||||
|
||||
## 9. 气枕温度和氦气质量导数方程
|
||||
|
||||
当前模型把 `T_ull` 作为 ODE 状态。氦气质量由恒压关系派生:
|
||||
|
||||
```text
|
||||
m_He = rho_He(T_ull, P_work) * V_ull
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:115-121`
|
||||
|
||||
### 9.1 气枕体积变化率
|
||||
|
||||
液氮质量变化导致气枕体积变化:
|
||||
|
||||
```text
|
||||
dV_ull/dt = -dm_liq_dt / rho_liq
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:186-188`
|
||||
|
||||
### 9.2 气枕区非氦气入口热量
|
||||
|
||||
```text
|
||||
Q_ull_no_he = Q_liq_to_ull + Q_leak_ull
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:188`
|
||||
|
||||
### 9.3 恒压氦气状态函数
|
||||
|
||||
在固定压力 `P_work` 下,给定 `T_ull` 和 `V_ull`:
|
||||
|
||||
```text
|
||||
rho = rho_He(T_ull, P_work)
|
||||
m(T, V) = rho * V
|
||||
U(T, V) = m(T, V) * u_He(T_ull, P_work)
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:115-121`
|
||||
|
||||
### 9.4 解析偏导数
|
||||
|
||||
代码不再对温度和体积做有限差分,而是使用恒压物性导数:
|
||||
|
||||
```text
|
||||
drhodT_P = (partial rho / partial T)_P
|
||||
dudT_P = (partial u / partial T)_P
|
||||
```
|
||||
|
||||
CoolProp 偏导接口位置:`src/cryo_tank/properties.py:100-109`
|
||||
|
||||
由此得到:
|
||||
|
||||
```text
|
||||
m_T = V_ull * drhodT_P
|
||||
m_V = rho
|
||||
U_T = V_ull * (u * drhodT_P + rho * dudT_P)
|
||||
U_V = rho * u
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:165-178`
|
||||
|
||||
### 9.5 气枕温度变化率
|
||||
|
||||
氦气入口焓:
|
||||
|
||||
```text
|
||||
h_in_he = h_He(T_in_he, P_work)
|
||||
```
|
||||
|
||||
气枕温度变化率:
|
||||
|
||||
```text
|
||||
dT_ull/dt =
|
||||
[Q_ull_no_he - (U_V + P_work - h_in_he * m_V) * dV_ull/dt]
|
||||
/ [U_T - h_in_he * m_T]
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:190-197`
|
||||
|
||||
若分母过小:
|
||||
|
||||
```text
|
||||
if abs(U_T - h_in_he * m_T) < 1e-30:
|
||||
dT_ull/dt = 0
|
||||
mdot_He = 0
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:190-193`
|
||||
|
||||
### 9.6 氦气质量变化率
|
||||
|
||||
```text
|
||||
dm_He/dt = mdot_He
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
```text
|
||||
mdot_He = m_T * dT_ull/dt + m_V * dV_ull/dt
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:198`
|
||||
|
||||
若 `mdot_He < 0`,报告流量钳制为 0:
|
||||
|
||||
```text
|
||||
mdot_He = 0
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:200-205`
|
||||
|
||||
## 10. 完整 ODE 方程组
|
||||
|
||||
状态向量:
|
||||
|
||||
```text
|
||||
y = [m_liq, U_liq, T_ull]
|
||||
```
|
||||
|
||||
ODE 方程组:
|
||||
|
||||
```text
|
||||
dm_liq/dt = mdot_in_ln2 - mdot_out_ln2
|
||||
```
|
||||
|
||||
```text
|
||||
dU_liq/dt =
|
||||
mdot_in_ln2 * h_in_ln2
|
||||
- mdot_out_ln2 * h_liq
|
||||
- Q_liq_to_ull
|
||||
+ Q_leak_liq
|
||||
```
|
||||
|
||||
```text
|
||||
dT_ull/dt =
|
||||
[Q_ull_no_he - (U_V + P_work - h_in_he * m_V) * dV_ull/dt]
|
||||
/ [U_T - h_in_he * m_T]
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
```text
|
||||
h_liq = h_LN2(T_liq, P_work)
|
||||
Q_liq_to_ull = h_conv * A_cross * (T_liq - T_ull)
|
||||
Q_ull_no_he = Q_liq_to_ull + Q_leak_ull
|
||||
Q_leak_liq = Q_leak * A_wet / (A_wet + A_dry)
|
||||
Q_leak_ull = Q_leak * A_dry / (A_wet + A_dry)
|
||||
```
|
||||
|
||||
氦气质量和质量流量是派生量:
|
||||
|
||||
```text
|
||||
m_He = rho_He(T_ull, P_work) * V_ull
|
||||
mdot_He = m_T * dT_ull/dt + m_V * dV_ull/dt
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/tank_model.py:131-214`
|
||||
|
||||
## 11. ODE 求解逻辑
|
||||
|
||||
求解器入口:
|
||||
|
||||
```text
|
||||
run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0)
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/solver.py:22-24`
|
||||
|
||||
求解初值:
|
||||
|
||||
```text
|
||||
y0 = tank.initial_state()
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/solver.py:43`
|
||||
|
||||
调用 `solve_ivp`:
|
||||
|
||||
```text
|
||||
solve_ivp(
|
||||
tank.rhs,
|
||||
[0.0, t_end],
|
||||
y0,
|
||||
method="RK45",
|
||||
rtol=rtol,
|
||||
atol=atol,
|
||||
max_step=max_step,
|
||||
events=[_liquid_empty_event],
|
||||
dense_output=True,
|
||||
)
|
||||
```
|
||||
|
||||
代码位置:`src/cryo_tank/solver.py:45-55`
|
||||
|
||||
液体排空事件:
|
||||
|
||||
```text
|
||||
event(t, y) = m_liq
|
||||
```
|
||||
|
||||
事件属性:
|
||||
|
||||
```text
|
||||
terminal = True
|
||||
direction = -1
|
||||
```
|
||||
|
||||
含义:
|
||||
|
||||
- 当液氮质量下降到 0 时终止求解。
|
||||
- 只检测从正到零的方向。
|
||||
|
||||
代码位置:`src/cryo_tank/solver.py:14-19`
|
||||
|
||||
求解失败时抛出错误;若液体提前排空,则发出警告。
|
||||
|
||||
代码位置:`src/cryo_tank/solver.py:57-64`
|
||||
|
||||
## 12. 后处理逻辑
|
||||
|
||||
求解完成后,`solver.run()` 对每个输出时刻执行:
|
||||
|
||||
```text
|
||||
info = tank.derive(y_i)
|
||||
```
|
||||
|
||||
并重新计算:
|
||||
|
||||
```text
|
||||
Q_liq_to_ull
|
||||
Q_leak
|
||||
Q_leak_liq
|
||||
Q_leak_ull
|
||||
mdot_He
|
||||
```
|
||||
|
||||
其中 `m_He`、`U_ull`、`mdot_He` 均为后处理派生结果,不是独立 ODE 状态。
|
||||
|
||||
代码位置:`src/cryo_tank/solver.py:70-124`
|
||||
Binary file not shown.
@@ -96,16 +96,16 @@ find results/cryo_tank -maxdepth 1 -type f -print
|
||||
状态量为:
|
||||
|
||||
```text
|
||||
y = [m_liq, U_liq, U_ull]
|
||||
y = [m_liq, U_liq, T_ull]
|
||||
```
|
||||
|
||||
含义分别是:
|
||||
|
||||
- `m_liq`:液氮质量
|
||||
- `U_liq`:液相内能
|
||||
- `U_ull`:气枕区总内能
|
||||
- `T_ull`:气枕区温度
|
||||
|
||||
`derive(y)` 会根据状态量计算温度、体积、液位、充满率、氦气质量和氦气气枕压力等派生量。
|
||||
`derive(y)` 会根据状态量计算温度、体积、液位、充满率、氦气质量、气枕区总内能和氦气气枕压力等派生量。当前模型按恒压纯氦气枕处理,`m_He = rho_He(T_ull, P_work) * V_ull`,所以氦气质量是派生量,不是独立 ODE 状态。
|
||||
|
||||
`rhs(t, y)` 是 ODE 右端函数,由 `scipy.integrate.solve_ivp` 调用。
|
||||
|
||||
@@ -132,9 +132,11 @@ run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0)
|
||||
主要功能:
|
||||
|
||||
- 液氮密度、焓、内能计算。
|
||||
- 氮气饱和压力和饱和蒸气内能计算。
|
||||
- 根据液氮比内能和压力反推液相温度。
|
||||
- 氦气密度、焓、内能、比热计算。
|
||||
- 建立液氮内能到温度的查表插值,加速 ODE 求解。
|
||||
- 根据氦气压力和密度反推气枕温度。
|
||||
- 计算氦气恒压密度温度导数 `(partial rho / partial T)_P`。
|
||||
- 计算氦气恒压内能温度导数 `(partial u / partial T)_P`。
|
||||
|
||||
该模块依赖 `CoolProp`。
|
||||
|
||||
@@ -373,6 +375,7 @@ pytest -q
|
||||
|
||||
- 本模型当前将储箱分为液相区和气枕区两个区域,不是完整 CFD 模型。
|
||||
- 气枕区按纯氦气处理,物性由 CoolProp 计算。
|
||||
- 气枕温度 `T_ull` 是 ODE 状态量;氦气质量 `m_He` 和补气流量 `mdot_He` 是恒压条件下的派生结果。
|
||||
- 液氮物性依赖 `CoolProp`,缺少该包会导致程序无法启动。
|
||||
- `results/` 目录默认被 `.gitignore` 忽略,仿真输出不会自动上传到 Git。
|
||||
- 建议每次修改参数后保存对应工况说明,避免不同仿真结果混在同一个输出目录中。
|
||||
@@ -3,6 +3,7 @@
|
||||
Configuration for the cryogenic LN2 tank simulation.
|
||||
Pure data module -- no functions, no side effects.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
# ---------- Tank geometry ----------
|
||||
@@ -29,7 +30,7 @@ MDOT_OUT_LN2 = 1.1895 # kg/s
|
||||
T_IN_HE = 100.0 # K
|
||||
|
||||
# ---------- Heat transfer ----------
|
||||
H_CONV_SURFACE = 50.0 # W/(m^2*K) liquid-to-ullage surface convection
|
||||
H_CONV_SURFACE = 0.0 # W/(m^2*K) liquid-to-ullage surface convection
|
||||
T_ENV = 300.0 # K ambient temperature
|
||||
|
||||
# ---------- Simulation control ----------
|
||||
|
||||
@@ -96,3 +96,16 @@ def he_u_from_rho(P, rho):
|
||||
"""Recover He specific internal energy from pressure and density [J/kg]."""
|
||||
_he_state.update(CP.DmassP_INPUTS, rho, P)
|
||||
return _he_state.umass()
|
||||
|
||||
|
||||
def he_drho_dT_const_p(T, P=_HE_P_REF):
|
||||
"""He density temperature derivative at constant pressure [kg/(m^3*K)]."""
|
||||
_he_state.update(CP.PT_INPUTS, P, T)
|
||||
return _he_state.first_partial_deriv(CP.iDmass, CP.iT, CP.iP)
|
||||
|
||||
|
||||
def he_du_dT_const_p(T, P=_HE_P_REF):
|
||||
"""He internal-energy temperature derivative at constant pressure [J/(kg*K)]."""
|
||||
_he_state.update(CP.PT_INPUTS, P, T)
|
||||
return _he_state.first_partial_deriv(CP.iUmass, CP.iT, CP.iP)
|
||||
|
||||
@@ -71,7 +71,7 @@ def run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0):
|
||||
't': t,
|
||||
'm_liq': sol.y[0],
|
||||
'U_liq': sol.y[1],
|
||||
'U_ull': sol.y[2],
|
||||
'U_ull': np.zeros(n),
|
||||
'T_liq': np.zeros(n),
|
||||
'T_ull': np.zeros(n),
|
||||
'm_He': np.zeros(n),
|
||||
@@ -94,6 +94,7 @@ def run(tank, t_end, rtol=1e-8, atol=1e-10, max_step=10.0):
|
||||
|
||||
history['T_liq'][i] = info['T_liq']
|
||||
history['T_ull'][i] = info['T_ull']
|
||||
history['U_ull'][i] = info['U_ull']
|
||||
history['m_He'][i] = info['m_He']
|
||||
history['V_liq'][i] = info['V_liq']
|
||||
history['V_ull'][i] = info['V_ull']
|
||||
|
||||
+55
-102
@@ -2,7 +2,7 @@
|
||||
"""
|
||||
CryoTank: two-zone (liquid + ullage) cryogenic tank model.
|
||||
|
||||
State vector y = [m_liq, U_liq, U_ull] (3 components).
|
||||
State vector y = [m_liq, U_liq, T_ull] (3 components).
|
||||
Derived quantities (T, V, m_He, etc.) computed by derive(y).
|
||||
ODE right-hand side provided by rhs(t, y).
|
||||
"""
|
||||
@@ -10,7 +10,6 @@ import math
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
from scipy.optimize import brentq
|
||||
|
||||
from cryo_tank import properties as prop
|
||||
|
||||
@@ -60,7 +59,7 @@ class CryoTank:
|
||||
|
||||
# Precompute constant inlet enthalpies
|
||||
self.h_in_ln2 = prop.ln2_h(T_in_ln2, P_work)
|
||||
self.h_in_he = prop.he_h(T_in_he)
|
||||
self.h_in_he = prop.he_h(T_in_he, P_work)
|
||||
|
||||
# Net liquid flow (constant)
|
||||
self.dm_liq_dt = mdot_in_ln2 - mdot_out_ln2
|
||||
@@ -81,14 +80,13 @@ class CryoTank:
|
||||
# Nitrogen evaporation is intentionally not modeled, so the ullage
|
||||
# pressure is provided entirely by helium and the liquid sees the same
|
||||
# tank pressure for property lookup.
|
||||
self._T_ull_0 = T_init
|
||||
self._m_He_0 = prop.he_rho(T_init, P_work) * V_ull_0
|
||||
|
||||
U_He_0 = self._m_He_0 * prop.he_u(T_init, P_work)
|
||||
self._U_ull_0 = U_He_0
|
||||
self._U_ull_0 = self._m_He_0 * prop.he_u(T_init, P_work)
|
||||
|
||||
def initial_state(self):
|
||||
"""Return the ODE initial state vector y0 = [m_liq, U_liq, U_ull]."""
|
||||
return np.array([self._m_liq_0, self._U_liq_0, self._U_ull_0])
|
||||
"""Return the ODE initial state vector y0 = [m_liq, U_liq, T_ull]."""
|
||||
return np.array([self._m_liq_0, self._U_liq_0, self._T_ull_0])
|
||||
|
||||
def wetted_areas(self, liquid_level):
|
||||
"""Return (A_wet, A_dry) for the given liquid level [m]."""
|
||||
@@ -101,86 +99,44 @@ class CryoTank:
|
||||
"""Compute all derived quantities from state vector y.
|
||||
|
||||
Returns a dict with T_liq, T_ull, V_liq, V_ull, liquid_level,
|
||||
fill_fraction, m_He, P_He, etc. Helium provides the full ullage
|
||||
pressure in this no-evaporation model.
|
||||
fill_fraction, m_He, U_ull, 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, T_ull = y[0], y[1], y[2]
|
||||
|
||||
# Liquid zone
|
||||
u_liq = U_liq / m_liq # specific internal energy
|
||||
u_liq = U_liq / m_liq
|
||||
T_liq = prop.ln2_T_from_u(u_liq, self.P_work)
|
||||
rho_liq = prop.ln2_rho(T_liq, self.P_work)
|
||||
V_liq = m_liq / rho_liq
|
||||
liquid_level = V_liq / self.A_cross
|
||||
fill_fraction = liquid_level / self.H_tank
|
||||
|
||||
# Ullage zone
|
||||
# Ullage zone: pure helium at tank pressure. T_ull is the ODE state;
|
||||
# m_He is the amount required to maintain P_work in the current volume.
|
||||
V_ull = self.V_total - V_liq
|
||||
|
||||
# Solve T_ull from ullage energy
|
||||
T_ull = self._solve_ullage_temperature(U_ull, V_ull)
|
||||
|
||||
# No nitrogen evaporation: helium provides all gas pressure.
|
||||
P_He = self.P_work
|
||||
m_He = prop.he_rho(T_ull, P_He) * V_ull
|
||||
rho_He = prop.he_rho(T_ull, P_He)
|
||||
m_He = rho_He * V_ull
|
||||
U_ull = m_He * prop.he_u(T_ull, P_He)
|
||||
|
||||
return {
|
||||
'T_liq': T_liq, 'T_ull': T_ull,
|
||||
'V_liq': V_liq, 'V_ull': V_ull,
|
||||
'liquid_level': liquid_level, 'fill_fraction': fill_fraction,
|
||||
'rho_liq': rho_liq,
|
||||
'm_He': m_He, 'P_He': P_He,
|
||||
'rho_liq': rho_liq, 'rho_He': rho_He,
|
||||
'm_He': m_He, 'U_ull': U_ull, 'P_He': P_He,
|
||||
}
|
||||
|
||||
def _solve_ullage_temperature(self, U_ull, V_ull):
|
||||
"""Solve pure-He ullage temperature using CoolProp EOS.
|
||||
|
||||
At fixed tank pressure, T is obtained from:
|
||||
U_ull = rho_He(P_work, T) * V_ull * u_He(P_work, T)
|
||||
"""
|
||||
def residual(T):
|
||||
rho = prop.he_rho(T, self.P_work)
|
||||
u = prop.he_u(T, self.P_work)
|
||||
return rho * V_ull * u - U_ull
|
||||
|
||||
T_grid = np.geomspace(20.0, 1000.0, 160)
|
||||
T_prev = T_grid[0]
|
||||
f_prev = residual(T_prev)
|
||||
if abs(f_prev) < 1e-8:
|
||||
return float(T_prev)
|
||||
|
||||
best_T = T_prev
|
||||
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
|
||||
|
||||
warnings.warn(
|
||||
"Unable to bracket ullage temperature from CoolProp He state: "
|
||||
f"U_ull={U_ull:.6g}, V_ull={V_ull:.6g}; "
|
||||
f"using nearest T={best_T:.6g} K"
|
||||
)
|
||||
return float(best_T)
|
||||
|
||||
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, dT_ull/dt].
|
||||
|
||||
This is called by scipy.integrate.solve_ivp.
|
||||
"""
|
||||
info = self.derive(y)
|
||||
m_liq = y[0]
|
||||
T_liq = info['T_liq']
|
||||
T_ull = info['T_ull']
|
||||
V_ull = info['V_ull']
|
||||
rho_liq = info['rho_liq']
|
||||
liquid_level = info['liquid_level']
|
||||
m_He = info['m_He']
|
||||
|
||||
# --- Heat transfer ---
|
||||
Q_liq_to_ull = self.h_conv * self.A_cross * (T_liq - T_ull)
|
||||
@@ -191,9 +147,6 @@ class CryoTank:
|
||||
Q_leak_liq = Q_leak * A_wet / A_total if A_total > 0 else 0.0
|
||||
Q_leak_ull = Q_leak * A_dry / A_total if A_total > 0 else 0.0
|
||||
|
||||
# --- He flow rate (analytical, per spec Section 2.6) ---
|
||||
mdot_He = self._solve_he_flow_rate(info, Q_liq_to_ull, Q_leak_ull)
|
||||
|
||||
# --- Liquid zone ---
|
||||
dm_liq_dt = self.dm_liq_dt # constant: mdot_in - mdot_out
|
||||
h_liq = prop.ln2_h(T_liq, self.P_work)
|
||||
@@ -203,59 +156,59 @@ class CryoTank:
|
||||
+ Q_leak_liq)
|
||||
|
||||
# --- Ullage zone ---
|
||||
dU_ull_dt = mdot_He * self.h_in_he + Q_liq_to_ull + Q_leak_ull
|
||||
dT_ull_dt, _ = self._solve_ullage_temperature_rate(
|
||||
info, Q_liq_to_ull, Q_leak_ull
|
||||
)
|
||||
|
||||
return np.array([dm_liq_dt, dU_liq_dt, dU_ull_dt])
|
||||
return np.array([dm_liq_dt, dU_liq_dt, dT_ull_dt])
|
||||
|
||||
def _solve_he_flow_rate(self, info, Q_liq_to_ull, Q_leak_ull):
|
||||
"""Solve He inlet mass flow from constant-pressure CoolProp EOS.
|
||||
|
||||
The ullage is pure He at P_work. With V_ull changing due to liquid
|
||||
volume change, m_dot_He is found by differentiating:
|
||||
m(T, V) = rho(P_work, T) * V
|
||||
U(T, V) = m(T, V) * u(P_work, T)
|
||||
and combining it with dU/dt = m_dot_He*h_in + Q_ull.
|
||||
"""
|
||||
def _he_mass_energy_partials(self, info):
|
||||
"""Return local partials for m(T,V) and U(T,V) at constant pressure."""
|
||||
T_ull = info['T_ull']
|
||||
V_ull = info['V_ull']
|
||||
rho_liq = info['rho_liq']
|
||||
rho = info['rho_He']
|
||||
u = prop.he_u(T_ull, self.P_work)
|
||||
drho_dT = prop.he_drho_dT_const_p(T_ull, self.P_work)
|
||||
du_dT = prop.he_du_dT_const_p(T_ull, self.P_work)
|
||||
|
||||
m_T = V_ull * drho_dT
|
||||
m_V = rho
|
||||
U_T = V_ull * (u * drho_dT + rho * du_dT)
|
||||
U_V = rho * u
|
||||
return m_T, U_T, m_V, U_V
|
||||
|
||||
def _solve_ullage_temperature_rate(self, info, Q_liq_to_ull, Q_leak_ull):
|
||||
"""Solve dT_ull/dt and He inlet flow from constant-pressure EOS.
|
||||
|
||||
T_ull is the ODE state. The pure-He ullage is maintained at P_work,
|
||||
so m_He = rho(P_work, T_ull) * V_ull is a derived quantity.
|
||||
"""
|
||||
rho_liq = info['rho_liq']
|
||||
dV_ull_dt = -self.dm_liq_dt / rho_liq
|
||||
Q_ull_no_he = Q_liq_to_ull + Q_leak_ull
|
||||
|
||||
def state_at(T, V):
|
||||
rho = prop.he_rho(T, self.P_work)
|
||||
m = rho * V
|
||||
U = m * prop.he_u(T, self.P_work)
|
||||
return m, U
|
||||
|
||||
dT = max(1e-3, abs(T_ull) * 1e-5)
|
||||
dV = max(1e-8, abs(V_ull) * 1e-5)
|
||||
|
||||
m_plus, U_plus = state_at(T_ull + dT, V_ull)
|
||||
m_minus, U_minus = state_at(max(20.0, T_ull - dT), V_ull)
|
||||
actual_dT = (T_ull + dT) - max(20.0, T_ull - dT)
|
||||
m_T = (m_plus - m_minus) / actual_dT
|
||||
U_T = (U_plus - U_minus) / actual_dT
|
||||
|
||||
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
|
||||
|
||||
m_T, U_T, m_V, U_V = self._he_mass_energy_partials(info)
|
||||
denominator = U_T - self.h_in_he * m_T
|
||||
if abs(denominator) < 1e-30:
|
||||
return 0.0
|
||||
return 0.0, 0.0
|
||||
|
||||
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
|
||||
dT_ull_dt = (Q_ull_no_he
|
||||
- (U_V + self.P_work - self.h_in_he * m_V)
|
||||
* dV_ull_dt) / denominator
|
||||
mdot_He = m_T * dT_ull_dt + m_V * dV_ull_dt
|
||||
|
||||
if mdot_He < 0:
|
||||
warnings.warn(
|
||||
f"He backflow requested (m_dot_He={mdot_He:.4e} kg/s); "
|
||||
"clamping to 0. Pressure may drift above target."
|
||||
"clamping reported flow to 0. Pressure control may be invalid."
|
||||
)
|
||||
mdot_He = 0.0
|
||||
|
||||
return dT_ull_dt, mdot_He
|
||||
|
||||
def _solve_he_flow_rate(self, info, Q_liq_to_ull, Q_leak_ull):
|
||||
"""Return derived He inlet mass flow for the current state [kg/s]."""
|
||||
_, mdot_He = self._solve_ullage_temperature_rate(
|
||||
info, Q_liq_to_ull, Q_leak_ull
|
||||
)
|
||||
return mdot_He
|
||||
Reference in new issue
Block a user