Files
pipe-system-simulation-test/docs/superpowers/specs/2026-04-14-0d-1d-tank-pipe-blowdown-mvp-design.md
T
2026-06-03 15:41:04 +08:00

487 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 方法)