487 lines
19 KiB
Markdown
487 lines
19 KiB
Markdown
# 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 方法)
|