代码仓库移植
This commit is contained in:
commit
0f57ba94f3
44 files changed
+7119
No files matched your search
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
|
||||
Reference in new issue
Block a user