代码仓库移植

This commit is contained in:
ljz committed 2026-06-03 15:41:04 +08:00
commit 0f57ba94f3
44 files changed
+7119

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# docs
Project notes, design documents, and reports.
Binary file not shown.
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,486 @@
# 0D–1D 气瓶–管路耦合瞬态放气仿真 MVP
**日期**:2026-04-14
**状态**:草案,待评审
**作者**:He Yunqin(通过 Claude Code / superpowers:brainstorming)
---
## 1. 目标与范围
### 1.1 目标
实现一个可运行的瞬态仿真程序,模拟以下场景:
- **高压气瓶** V₁ = 5 m³,P₁ = 10 MPa,T₁ = 300 K
- **低压气瓶** V₂ = 10 m³,P₂ = 0.101325 MPa(1 atm),T₂ = 300 K
- **连接管路** L = 1 m,D = 5 mm,划分为 N = 20 个有限体积单元
- **工质**:理想气体,γ = 1.4,R = 287 J/(kg·K)
- **初始条件**:管路与低压瓶同压同温(P₂, T₂),高压瓶独立
- **仿真时长**:t_end = 0.1 s(捕获开启瞬间的激波与膨胀波过程)
程序应输出:
1. 双瓶 P(t)、T(t) 时间曲线图
2. 管内 P(x)、u(x)、T(x) 演化动画(GIF)
3. 完整时间序列数据文件(`.npz`),支持离线加载任意时刻任意空间点的状态
### 1.2 MVP 范围边界
**本 MVP 做**:
- 0D 集中参数气瓶 + 1D 可压缩欧拉方程管路
- 虚网格(Ghost Cell)+ HLL Riemann 求解器耦合
- 一阶空间重构 + 一阶显式欧拉时间推进
- CFL 动态步长控制
- 基本 pytest 单元测试(共 10 条)
**本 MVP 明确不做**:
- 摩擦源项(Darcy–Weisbach 或 Fanning)
- 壁面传热(绝热假设)
- 二阶空间格式(MUSCL / limiter)
- 二阶时间格式(SSP-RK2/RK3)
- 局部阻力修正(入口/出口损失系数 ζ)
- HLLC、Roe、exact Riemann 等其他格式
- 网格收敛性扫描
- 真实气体状态方程
- 多管网络、分叉、汇合
---
## 2. 物理与数值模型
### 2.1 0D 气瓶模型
对每个气瓶,假设内部状态均匀、气体静止,采用质量与总内能两个守恒量:
```
dm/dt = ṁ_in (质量守恒)
dU/dt = Ḣ_in (= ṁ·h_t,in) (能量守恒,开口系第一定律)
```
其中:
- `m` = 总质量 [kg]
- `U` = 总内能 [J],对理想气体 `U = P·V/(γ−1) = m·R·T/(γ−1)`
- `Ḣ` = 总焓流 [W],正值表示流入
派生量通过状态方程按需计算:
```
ρ = m/V
T = (U/m) · (γ−1)/R
P = ρ·R·T = U·(γ−1)/V
```
**设计取舍**:选 `(m, U)` 作底层状态而非 `(P, T)` 的理由见 §4.3。
### 2.2 1D 管路模型
一维可压缩欧拉方程(守恒形式):
```
∂W/∂t + ∂F(W)/∂x = 0
W = [ρ, ρu, ρE]ᵀ
F = [ρu, ρu² + P, u·(ρE + P)]ᵀ
```
其中 `E = e + u²/2`,`e = P/(ρ(γ−1))`。
离散化:有限体积法,cell-averaged piecewise constant(一阶重构)。
```
W_i^{n+1} = W_i^n − (Δt/Δx)·(F_{i+1/2} − F_{i−1/2})
```
- `N = 20` 个单元
- `Δx = L/N = 0.05 m`
- 单元中心坐标 `x_i = (i + 0.5)·Δx`, i = 0..19
### 2.3 耦合:虚网格 + HLL
**虚网格定义**:管路左右各增加一个"虚拟单元",每个时间步开头根据当前气瓶状态填充:
```
W_ghost_L = [ρ₁, 0, P₁/(γ−1)]ᵀ ← 高压瓶
W_ghost_R = [ρ₂, 0, P₂/(γ−1)]ᵀ ← 低压瓶
```
速度取 0 的假设:把气瓶视为滞止状态(stagnation state)的无穷大储气罐,真实的加速过程由 HLL 在界面上解出。这是**建模简化**,不是物理真相;其代价是瓶内到界面的速度-压力关系并非严格等熵,对短管、大压差场景误差可接受。
**注意区分两件事**:
1. **系统总能守恒**:从"通量双用"的数学结构直接推出(见 §2.4),与 HLL 精度无关,严格到机器精度成立。
2. **气瓶能量更新的物理一致性**:HLL 的 `flux[2] = u(ρE + P)` 等同于总焓流密度 `ρu·h_t`。对于 u_ghost=0 的滞止态气瓶,h_t,ghost = c_p·T_tank,这恰好是开口系能量方程对气瓶的正确源项——即便 Riemann 求解器引入数值耗散,能量守恒量也被 HLL 的守恒形式严格守住。
**HLL 数值通量**(Harten–Lax–van Leer):
```
S_L = min(u_L − a_L, u_R − a_R)
S_R = max(u_L + a_L, u_R + a_R)
┌ F_L if S_L ≥ 0
F_HLL = │ (S_R·F_L − S_L·F_R + S_L·S_R·(W_R − W_L)) if S_L < 0 < S_R
│ ────────────────────────────────────
│ S_R − S_L
└ F_R if S_R ≤ 0
```
其中 `a = √(γP/ρ)`。
**关键性质**:HLL 自动处理双向流、激波、膨胀波和截流;无需手动判断流动方向或是否达到声速。
### 2.4 守恒性与通量双用
**核心耦合原理**:每个时间步,HLL 在管路左右两个边界各给出一个数值通量向量。**同一组通量被两边共享**:
- 用于更新管路首/末单元的保守变量(有限体积更新)
- 乘以管路截面积 A 后,作为气瓶的 `ṁ, Ḣ` 源项更新气瓶
这一"通量双用"机制从数学上天然保证系统总质量和总能量守恒到机器精度,是验证实现正确性的唯一充分必要条件。
---
## 3. 时间推进算法
### 3.1 单步推进的 7 个阶段
```
阶段 1 — 计算 CFL 时间步长
a_i = √(γ P_i / ρ_i) for each cell i
a_max = max over i of (|u_i| + a_i)
Δt = CFL · Δx / a_max
Δt = min(Δt, t_end − t) 夹到 t_end
阶段 2 — 冻结气瓶状态为虚网格
W_ghost_L = tank1.ghost_state() # 本步用的高压瓶快照
W_ghost_R = tank2.ghost_state() # 本步用的低压瓶快照
(管路 W 的快照留给 pipe.step 内部处理,solver 不碰)
阶段 3 — 计算两个边界通量(solver 层)
flux_L = HLL(W_ghost_L, pipe.W[:,0]) 左边界 (W_ghost_L ↔ cell 0)
flux_R = HLL(pipe.W[:,N-1], W_ghost_R) 右边界 (cell N-1 ↔ W_ghost_R)
注:只有这两个"边界通量"由 solver 计算,因为它们需要知道 ghost state。
所有 N-1 个"内部通量"由 pipe.step() 内部计算,solver 不参与。
阶段 4 — 管路一步推进(pipe.step 内部完成)
pipe.step(flux_L, flux_R, Δt):
W_snap = pipe.W.copy()
flux_int = [HLL(W_snap[:,i-1], W_snap[:,i]) for i in 1..N-1] 内部 N-1 个通量
首单元: pipe.W[:,0] = W_snap[:,0] − (Δt/Δx)·(flux_int[1] − flux_L)
末单元: pipe.W[:,N-1] = W_snap[:,N-1] − (Δt/Δx)·(flux_R − flux_int[N-1])
中间: pipe.W[:,i] = W_snap[:,i] − (Δt/Δx)·(flux_int[i+1] − flux_int[i])
for i in 1..N-2
阶段 5 — 同一组边界通量更新气瓶
fL_A = flux_L · A # (3,) 向量 × 标量面积
fR_A = flux_R · A
tank1.apply_flux(mdot=fL_A[0], edot=fL_A[2], dt=Δt, sign=-1) 流出
tank2.apply_flux(mdot=fR_A[0], edot=fR_A[2], dt=Δt, sign=+1) 流入
阶段 6 — 推进时间
t += Δt
阶段 7 — 记录历史
history['t'].append(t)
history['P1'].append(tank1.P)
history['T1'].append(tank1.T)
history['P2'].append(tank2.P)
history['T2'].append(tank2.T)
history['W_hist'].append(pipe.W.copy())
```
### 3.2 关键顺序约束
1. **必须先一次性算完所有界面通量,再一次性更新所有单元**。如果边算边更新,会导致"新 W 和旧 W 混用"。
2. **管路和气瓶的更新使用同一组冻结通量**。若用已被更新的 tank 状态重算左边界通量,即违反守恒性。
3. **阶段 4 和阶段 5 的先后顺序不重要**(彼此独立),但不能交错。
### 3.3 CFL 估算与步数预估
初始时刻(管路全场 = 低压瓶态):
```
a₀ = √(1.4 · 101325 / 1.177) ≈ 347 m/s
u₀ = 0
Δt₀ = 0.5 · 0.05 / 347 ≈ 7.2 × 10⁻⁵ s
```
激波进入管路后 `|u| + a` 可达 600–800 m/s,Δt 收缩到 ~30–40 μs。
`t_end = 0.1 s` 预计对应 **约 2000–3000 步**。NumPy 向量化 N=20 的数组,预计全程 **<15 秒** 跑完。
---
## 4. 组件与模块划分
### 4.1 文件布局
```
pipe-system-simulation-test/
├── src/
│ ├── config.py # 常量与工况(γ, R, V, P_init, L, D, N, CFL, t_end)
│ ├── riemann.py # HLL 数值通量(纯函数)
│ ├── tank.py # Tank 类(0D,状态 = (mass, U))
│ ├── pipe.py # Pipe 类(1D 有限体积场 W, 推进方法)
│ ├── solver.py # 时间循环与气瓶-管路耦合编排
│ ├── output.py # save_history / plot_timeseries / make_animation
│ └── main.py # 入口:装配、调度、sanity check
├── tests/
│ ├── test_riemann.py
│ ├── test_tank.py
│ ├── test_pipe.py
│ └── test_integration.py
├── conftest.py # sys.path 注入,让 tests/ 能从 src/ 导入
├── results/ # 运行产物(.npz, .png, .gif)
├── cases/ # (保留,后续扩展多工况用)
├── docs/
│ └── superpowers/specs/2026-04-14-0d-1d-tank-pipe-blowdown-mvp-design.md # 本文件
└── scripts/ # (保留,后续工具脚本用)
```
### 4.2 模块职责
| 模块 | 职责 | 对外接口 |
|---|---|---|
| `config.py` | 纯数据:物理常数、工况、仿真控制参数 | 顶层常量 |
| `riemann.py` | HLL 数值通量计算 | `hll_flux(W_L, W_R, gamma) → ndarray(3,)` |
| `tank.py` | 0D 气瓶状态与演化 | `Tank` 类,`ghost_state()`, `apply_flux()`,派生属性 `P/T/rho` |
| `pipe.py` | 1D 管路状态与一步推进 | `Pipe` 类,`primitives()`, `max_wave_speed()`, `step()` |
| `solver.py` | 装配时间循环、调用 HLL、协调气瓶与管路更新 | `run(tank1, tank2, pipe, t_end, cfl, verbose=False, log_every=100) → history: dict` |
| `output.py` | 持久化与可视化 | `save_history()`, `plot_tank_timeseries()`, `make_pipe_animation()` |
| `main.py` | 入口脚本:构造对象 → 调 solver → 调 output → sanity check | `main()` |
### 4.3 关键设计取舍
#### 4.3.1 Tank 底层状态选 `(mass, U)` 而非 `(P, T)`
**理由**:
- 守恒律在 `(m, U)` 空间是线性的:`m += ṁ·Δt`,`U += Ḣ·Δt`,两次加法即完成,零公式展开
- `(P, T, ρ)` 三者由状态方程约束,只有 2 个自由度;若都当第一身份存,必须人工保证同步,任何一步漏更新就违反状态方程
- HLL 的 `flux[2] = u(ρE + P)` 恰好等于总焓流密度 `ρu·h_t`,乘以 A 后就是开口系能量方程的右端项 `Ḣ`;写成 `dU/dt = Ḣ` 直接对应代码 `tank.U += flux[2]·A·dt`,无需手工添加"流动功修正"
- 派生属性(`tank.P`, `tank.T`, `tank.rho`)通过 `@property` 实时计算,永远与 `(m, U)` 一致,外部读不到过时值
**代价**:每次访问 `tank.P` 需要一次除法 + 一次乘法。对 0D 气瓶完全可忽略。
#### 4.3.2 界面通量"双用",由 solver 显式编排
- `solver.run()` 在每步先算边界 flux,再把 flux 分发给 `pipe.step(flux_L, flux_R, dt)` 和 `tank.apply_flux(ṁ, Ḣ, dt, sign)`
- `pipe.step()` 内部负责所有**内部**界面通量的计算和管路单元更新
- 气瓶不知道管路存在,管路不知道气瓶存在;耦合知识集中在 solver 一处
#### 4.3.3 ghost_state() 作为 Tank 的方法
- "如何把自身暴露成虚网格"是 tank 的职责,不是 solver 的职责
- solver 代码里就是 `hll_flux(tank1.ghost_state(), pipe.W[:,0], γ)`,可读性最高
- 未来如果要扩展成"带局部阻力修正的虚网格",只改 `Tank.ghost_state()` 即可
#### 4.3.4 sign 参数约定
`Tank.apply_flux(mdot, edot, dt, sign)` 接受一个显式的 sign 参数:
- `sign = -1`:气流"流出"瓶子。对 tank1(高压瓶),左边界通量向右为正,即流出,所以用 -1
- `sign = +1`:气流"流入"瓶子。对 tank2(低压瓶),右边界通量向右为正,即流入,所以用 +1
显式 sign 比在 Tank 内部判断"我是 tank1 还是 tank2"更干净。
---
## 5. 数据流与历史记录
### 5.1 History 数据结构
```python
history = {
't': np.ndarray, # shape (n_steps,) 时间序列
'P1': np.ndarray, # shape (n_steps,) 高压瓶压力
'T1': np.ndarray, # shape (n_steps,) 高压瓶温度
'P2': np.ndarray, # shape (n_steps,) 低压瓶压力
'T2': np.ndarray, # shape (n_steps,) 低压瓶温度
'W_hist': np.ndarray, # shape (n_steps, 3, N) 管路全程守恒变量
}
```
循环中用 Python list + append 收集,循环结束后一次性 `np.stack` / `np.asarray` 转成数组。
**内存估算**:
```
n_steps ≈ 3000, 3 vars × 20 cells × 8 bytes
→ 每步 480 B, 全程 ≈ 1.4 MB
```
完全无压力,**每步都记**,不做降采样。
### 5.2 持久化:`results/history.npz`
```python
np.savez_compressed(
"results/history.npz",
t = history['t'],
P1 = history['P1'], T1 = history['T1'],
P2 = history['P2'], T2 = history['T2'],
W_hist = history['W_hist'],
x = pipe_cell_centers, # shape (N,)
dx = pipe.dx,
gamma = GAMMA,
R_gas = R_GAS,
area = pipe.area,
)
```
未来加载与访问示例:
```python
d = np.load("results/history.npz")
rho = d['W_hist'][:, 0, :] # (n_steps, N)
u = d['W_hist'][:, 1, :] / rho
P = (d['W_hist'][:, 2, :] - 0.5*rho*u**2) * (d['gamma'] - 1)
```
### 5.3 可视化产物
```
results/
├── history.npz 全量时间序列,可离线复盘
├── tank_pressure.png P1(t), P2(t) 双曲线
├── tank_temperature.png T1(t), T2(t) 双曲线
└── pipe_animation.gif 管内 P(x), u(x), T(x) 三子图演化
```
**动画细节**:
- 用 `matplotlib.animation.FuncAnimation` + `PillowWriter`(GIF 输出,免 ffmpeg 依赖)
- 布局:3 个纵向子图,共享 x 轴(管路空间坐标 0–1 m)
- 每帧标题:`t = X.XXXX s (step k/n_steps)`
- 默认 `stride = 10`,约 300 帧,GIF 文件预计 5–10 MB
- 数据存储仍为全量;stride 只影响动画帧数
---
## 6. 错误处理
### 6.1 验证点清单
| 位置 | 检查 | 失败时 |
|---|---|---|
| 程序启动 | `results/` 目录存在(`os.makedirs(exist_ok=True)`) | 静默创建 |
| `config.py` 顶层 | γ>1, R>0, V>0, L>0, D>0, N≥2, P>0, T>0, CFL∈(0,1] | `AssertionError` |
| 时间步开始 | 管路所有单元 ρ>0, P>0 | `RuntimeError("负密度/负压力 at step k")` |
| CFL 步长 | `Δt > 1e-12` | `RuntimeError("dt 退化")` |
| HLL 内部 | 恢复原始变量时 ρ>0, P>0 | `ValueError` + 打印 W_L, W_R |
| Tank 更新后 | `mass > 0` | `RuntimeError("气瓶质量非正")` |
| 仿真结束 | `n_steps > 0` | `RuntimeError("一步都没跑")` |
| main.py 末尾 | 总质量相对误差 < 1e-10 | `AssertionError`(作为 sanity check 打印) |
| main.py 末尾 | 总能量相对误差 < 1e-10 | `AssertionError`(作为 sanity check 打印) |
### 6.2 策略
- **异常直接往上抛**,不 catch、不重试、不降级。让 Python traceback 定位问题
- **不** `try/except` 包装 `main.py`
- `solver.run()` 提供 `verbose=False` 开关(默认关)和 `log_every=100` 打印间隔,用于调试时查看步进日志
### 6.3 明确不做的检查
- 负质量流量判断(HLL 自动处理双向)
- 显式判截流(Riemann 自动处理)
- 动态 CFL 调整策略
- 并行/线程安全
---
## 7. 测试策略
### 7.1 测试清单(共 10 条,4 个文件,预计 <5 秒)
#### `tests/test_riemann.py` — HLL 求解器
1. **左右状态相同 → 返回纯物理通量**(无数值耗散)
2. **静止接触间断**(两侧 u=0,仅 ρ 不同)→ 质量通量和能量通量应为 0,动量通量 = P
3. **Sod 激波管初值的符号与量级检查**(不做精确值断言,检查 flux[0], flux[1], flux[2] 均 > 0)
#### `tests/test_tank.py` — 气瓶
4. **初始状态自洽**:给定 (P, T, V) 构造 → `mass` 和 `U` 满足理想气体关系
5. **ghost_state 是滞止态**:动量项 = 0,能量项 = P/(γ−1)
6. **apply_flux 方向性**:sign=-1 应减质量、减内能、降压
#### `tests/test_pipe.py` — 管路
7. **均匀初始化**:所有单元的 W 完全相同;`primitives()` 返回 u=0, P=P_init
8. **静止状态 + 一致压力通量 → 一步后 W 不变**(机器精度)
#### `tests/test_integration.py` — 端到端守恒律
9. **短仿真总质量守恒**:`t_end=1e-3`,`|Δm_total/m_total| < 1e-10`
10. **短仿真总能量守恒**:同上,`|ΔU_total/U_total| < 1e-10`
### 7.2 守恒律断言的价值
守恒律是**数学真理**,任何正确实现都必然满足。如果实现中"通量双用"机制写错(例如 tank 用了被更新后的 flux,或算 flux 时没快照 W),守恒律立刻以可观察的数量级被破坏。这比"和参考解对比"更可靠——后者有过拟合风险。
### 7.3 不做的测试
- 绘图/动画输出(肉眼验证更高效)
- `main.py` 装配逻辑(无独立逻辑)
- 网格收敛 / CFL 收敛 / 阶数验证
- Method of Manufactured Solutions
- mock/fixture 框架(10 条测试不需要抽象)
### 7.4 运行方式
```bash
pytest tests/ -v
```
项目根运行。`conftest.py` 自动注入 `src/` 到 `sys.path`。
### 7.5 可测性要求(对生产代码的约束)
1. 各模块顶层**无副作用**(禁止 print/read-file/random-seed)
2. `solver.run()` 不读取 `config` 模块,所有参数通过入参传入
3. `Pipe.primitives()` 返回 `(ρ, u, P, a)` 四元组,方便测试断言
---
## 8. 验收标准
MVP 完成的判据:
1. ✅ `python src/main.py` 能跑完、不崩溃、在 `results/` 下生成全部 4 个文件
2. ✅ `pytest tests/ -v` 全部 10 条测试通过
3. ✅ main.py 末尾的"总质量 & 总能量相对误差 < 1e-10" sanity check 通过
4. ✅ `P1(t)` 单调递减、`P2(t)` 单调递增
5. ✅ 动画能用常见 GIF 播放器(浏览器、图片查看器)正常打开
6. ✅ `history.npz` 可以用 `np.load` 重新加载并访问所有字段
7. ✅ 全程运行时间(不含测试) < 30 秒
---
## 9. 已知局限与后续扩展
**本 MVP 的物理局限**:
- 细管(D=5mm)下摩擦损失可能显著,当前绝热无摩擦会高估压力传递速率
- 瓶内气体从滞止到出口的加速不满足严格等熵(因为 u_ghost=0 的简化)
- 一阶空间导致膨胀波被耗散
- 一阶时间精度对大 CFL 下的波传播有额外耗散
**后续可扩展方向**(不在本 MVP 范围内):
1. 加 Darcy–Weisbach 摩擦源项(`src/friction.py`)
2. 加壁面对流换热(`src/heat_transfer.py`)
3. 升级到 MUSCL + minmod 二阶空间
4. 升级到 SSP-RK2 二阶时间
5. 在 `Tank.ghost_state()` 中加入入口损失系数 ζ
6. 配置化工况:YAML/JSON 读入 `cases/*.yaml`
7. 扩展到多管网络(带分叉节点的 0D/1D 混合拓扑)
8. 真实气体状态方程(Peng–Robinson / GERG-2008)
---
## 10. 参考
- Toro, E. F. *Riemann Solvers and Numerical Methods for Fluid Dynamics*. Springer. (HLL 格式的标准参考)
- Harten, A., Lax, P. D., and van Leer, B. (1983). "On upstream differencing and Godunov-type schemes for hyperbolic conservation laws." *SIAM Review* 25(1): 35–61.
- LeVeque, R. J. *Finite Volume Methods for Hyperbolic Problems*. Cambridge University Press.
- 用户在 brainstorming 阶段提供的参考资料(涵盖 0D–1D 耦合的虚网格 + Riemann 方法)
@@ -0,0 +1,401 @@
# 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 mass
- `U_liq` [J]: liquid zone total internal energy
- `U_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 volume
- `V_ull = V_total - V_liq`: ullage volume
- `liquid_level = V_liq / A_cross`: liquid height in the cylinder
- `m_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 enthalpy
- `Q_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/dt
- `dT_ull/dt` is 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^2
- `h_conv`: surface convective heat transfer coefficient [W/(m^2*K)], configurable, default = 50 W/(m^2*K)
**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_dry`
- `T_inner` passed 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
```python
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)
```python
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
```python
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:
1. Unpack y -> (m_liq, U_liq, U_ull)
2. Compute T_liq from (m_liq, U_liq) via CoolProp
3. Compute V_liq, V_ull, liquid_level from geometry
4. Compute m_He from constant-pressure constraint
5. Compute T_ull from (U_ull, m_N2_ull, m_He) -- iterative or CoolProp
6. Compute all heat transfer terms (Q_liq_to_ull, Q_leak_liq, Q_leak_ull)
7. Compute enthalpy terms for inlets/outlet
8. 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`:
1. **Use CoolProp.AbstractState:** Create persistent `AbstractState` objects for
N2 and He at module load time. Reuse them across calls (avoid per-call overhead).
2. **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.interp` for
fast evaluation. Fall back to CoolProp only when T is outside the table range.
3. **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 `events` mechanism 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
Binary file not shown.