19 KiB
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(捕获开启瞬间的激波与膨胀波过程)
程序应输出:
- 双瓶 P(t)、T(t) 时间曲线图
- 管内 P(x)、u(x)、T(x) 演化动画(GIF)
- 完整时间序列数据文件(
.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 在界面上解出。这是建模简化,不是物理真相;其代价是瓶内到界面的速度-压力关系并非严格等熵,对短管、大压差场景误差可接受。
注意区分两件事:
- 系统总能守恒:从"通量双用"的数学结构直接推出(见 §2.4),与 HLL 精度无关,严格到机器精度成立。
- 气瓶能量更新的物理一致性: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 关键顺序约束
- 必须先一次性算完所有界面通量,再一次性更新所有单元。如果边算边更新,会导致"新 W 和旧 W 混用"。
- 管路和气瓶的更新使用同一组冻结通量。若用已被更新的 tank 状态重算左边界通量,即违反守恒性。
- 阶段 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(高压瓶),左边界通量向右为正,即流出,所以用 -1sign = +1:气流"流入"瓶子。对 tank2(低压瓶),右边界通量向右为正,即流入,所以用 +1
显式 sign 比在 Tank 内部判断"我是 tank1 还是 tank2"更干净。
5. 数据流与历史记录
5.1 History 数据结构
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
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,
)
未来加载与访问示例:
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 求解器
- 左右状态相同 → 返回纯物理通量(无数值耗散)
- 静止接触间断(两侧 u=0,仅 ρ 不同)→ 质量通量和能量通量应为 0,动量通量 = P
- Sod 激波管初值的符号与量级检查(不做精确值断言,检查 flux[0], flux[1], flux[2] 均 > 0)
tests/test_tank.py — 气瓶
- 初始状态自洽:给定 (P, T, V) 构造 →
mass和U满足理想气体关系 - ghost_state 是滞止态:动量项 = 0,能量项 = P/(γ−1)
- apply_flux 方向性:sign=-1 应减质量、减内能、降压
tests/test_pipe.py — 管路
- 均匀初始化:所有单元的 W 完全相同;
primitives()返回 u=0, P=P_init - 静止状态 + 一致压力通量 → 一步后 W 不变(机器精度)
tests/test_integration.py — 端到端守恒律
- 短仿真总质量守恒:
t_end=1e-3,|Δm_total/m_total| < 1e-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 运行方式
pytest tests/ -v
项目根运行。conftest.py 自动注入 src/ 到 sys.path。
7.5 可测性要求(对生产代码的约束)
- 各模块顶层无副作用(禁止 print/read-file/random-seed)
solver.run()不读取config模块,所有参数通过入参传入Pipe.primitives()返回(ρ, u, P, a)四元组,方便测试断言
8. 验收标准
MVP 完成的判据:
- ✅
python src/main.py能跑完、不崩溃、在results/下生成全部 4 个文件 - ✅
pytest tests/ -v全部 10 条测试通过 - ✅ main.py 末尾的"总质量 & 总能量相对误差 < 1e-10" sanity check 通过
- ✅
P1(t)单调递减、P2(t)单调递增 - ✅ 动画能用常见 GIF 播放器(浏览器、图片查看器)正常打开
- ✅
history.npz可以用np.load重新加载并访问所有字段 - ✅ 全程运行时间(不含测试) < 30 秒
9. 已知局限与后续扩展
本 MVP 的物理局限:
- 细管(D=5mm)下摩擦损失可能显著,当前绝热无摩擦会高估压力传递速率
- 瓶内气体从滞止到出口的加速不满足严格等熵(因为 u_ghost=0 的简化)
- 一阶空间导致膨胀波被耗散
- 一阶时间精度对大 CFL 下的波传播有额外耗散
后续可扩展方向(不在本 MVP 范围内):
- 加 Darcy–Weisbach 摩擦源项(
src/friction.py) - 加壁面对流换热(
src/heat_transfer.py) - 升级到 MUSCL + minmod 二阶空间
- 升级到 SSP-RK2 二阶时间
- 在
Tank.ghost_state()中加入入口损失系数 ζ - 配置化工况:YAML/JSON 读入
cases/*.yaml - 扩展到多管网络(带分叉节点的 0D/1D 混合拓扑)
- 真实气体状态方程(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 方法)