feat(body): add force_region, fix sensor API, reorganize docs
- Add force_region object type: local Guo forcing via sparse compact list
- ForceRegionSoA container, ForceRegionKernel, stepper dispatch
- add_body("force_region", ...) + set_force(id, fx, fy) API
- Fix read_sensor(normalize=...) not being passed from Simulation layer
- Fix force_region incorrectly entering curved cut-link path (P0 blocker)
- Clean up module boundaries: body/__init__ no longer imports from lbm
- Circluar import fix: common/streakline <-> pathline
- Package data globs fixed for recursive kernel files
- Version unified to 0.3.0
- Performance analysis: pycuda launch overhead vs GPU compute at various grid sizes
- Nsight Systems + Nsight Compute profiling data and report
- Documentation reorganized under docs/ (audit, validation_specs)
- README overhaul: multi-body examples, validated benchmarks, force_region docs
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2e052480c2
commit
b110591433
24
README.md
24
README.md
@ -123,6 +123,30 @@ Future geometry types (polygon, mesh) will use the same `add_body()` function wi
|
||||
| `sim.read_force(id)` -> ndarray | Force vector [fx, fy] (2D) |
|
||||
| `sim.read_torque(id)` -> ndarray | Torque [tz] (2D) |
|
||||
| `sim.read_sensor(id)` -> ndarray | Area-averaged velocity via GPU sensor kernel |
|
||||
| `sim.set_force(id, fx=..., fy=...)` | Set force density on a force_region object (notice: see persistence note below) |
|
||||
|
||||
### force_region usage
|
||||
|
||||
```python
|
||||
# Create a circular force application region
|
||||
fr_id = sim.add_body("force_region", center=(50, 50), radius=15)
|
||||
|
||||
# Set force density (lattice units, implicit GPU upload)
|
||||
sim.set_force(fr_id, fx=0.001, fy=0.0)
|
||||
|
||||
# The region applies Guo forcing on each step. Zero force = no-op.
|
||||
sim.set_force(fr_id, fx=0.0, fy=0.0) # disable force
|
||||
```
|
||||
|
||||
**Persistence note:** `set_force()` writes the action buffer directly but does not update the object's state record. If `sync_to_gpu()` is called afterward, the force will be reset to zero. For the common usage pattern (initialize -> set_force -> run -> set_force -> run ...), this is not an issue. A future update will add proper force storage in the object state.
|
||||
|
||||
### Comparison: body types
|
||||
|
||||
| Type | Flag overlay | Produces cut-links | Readback | Runtime control |
|
||||
|------|-------------|-------------------|----------|-----------------|
|
||||
| `"circle"` | OBSTACLE + BC_CURVED | Yes (Bouzidi) | force/torque | `set_body(id, omega=...)` |
|
||||
| `"sensor"` | FLUID + SENSOR_FLAG | No | area-averaged velocity | None needed |
|
||||
| `"force_region"` | None (zero mask) | **No** | None | `set_force(id, fx=..., fy=...)` |
|
||||
|
||||
#### Data access
|
||||
|
||||
|
||||
489
docs/validation_specs/Force_region.md
Normal file
489
docs/validation_specs/Force_region.md
Normal file
@ -0,0 +1,489 @@
|
||||
# Force region 最小方案设计
|
||||
|
||||
目标是在现有功能冻结的前提下,为项目增加一个**局部体力施加区域**,用于研究局部驱动与流场结构之间的关系。该功能的第一阶段只追求可用、低风险、低侵入,不追求最泛化的抽象,也不与 sensor 合并。
|
||||
|
||||
当前代码已经具备三条很明确的数据链:全场 `OneStep` 推进、稀疏 `curved` link 链、稀疏 `sensor` cell 链。`force_region` 更接近第三类:它是一个静态几何 footprint,加上运行时 action,而不是一个需要改变主域 flag 的边界对象。因此,第一阶段更适合沿用 `sensor` 风格的稀疏 cell 管理,而不是引入全场 owner map。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
| 类别 | 内容 |
|
||||
|---|---|
|
||||
| 目标 | 在圆形区域内对流体施加局部体力 |
|
||||
| 目标 | 运行时更新区域体力参数,不重新编译 |
|
||||
| 目标 | 尽量少改现有主热路径和已有稳定功能 |
|
||||
| 非目标 | 与 sensor 统一成单一抽象 |
|
||||
| 非目标 | 非圆形几何 |
|
||||
| 非目标 | 3D |
|
||||
| 非目标 | 多区域重叠叠加规则的完整设计 |
|
||||
| 非目标 | 数值上最严格的“逐格 collision 内联 forcing”实现 |
|
||||
|
||||
## 需求边界
|
||||
|
||||
第一阶段建议只支持以下能力:
|
||||
|
||||
- `force_region` 是一个圆形 footprint
|
||||
- 区域内部施加均匀体力 `fx, fy`
|
||||
- 区域几何在初始化后不变
|
||||
- 体力大小可在运行时更新
|
||||
- 只对 fluid cell 生效
|
||||
- 不要求读取区域平均速度
|
||||
- 不要求与 `sensor` 或 `body` 的接口完全统一
|
||||
|
||||
这意味着第一阶段的问题不是“建立通用 actuator 体系”,而是“在现有框架内插入一条最小局部 forcing 数据链”。
|
||||
|
||||
## 现有代码中与该功能直接相关的结构
|
||||
|
||||
| 模块 | 当前作用 | 与 force region 的关系 |
|
||||
|---|---|---|
|
||||
| `body/geometry/circle` | 生成圆形 footprint | 可复用 |
|
||||
| `body/manager` | 汇总 compact list 并上传 GPU | 适合新增 force-region list |
|
||||
| `lbm/curved_links` | 稀疏 SoA 容器 | 可参考 `SensorSoA` 形式扩展 |
|
||||
| `lbm/kernels/step/aux_kernels` | 稀疏辅助 kernel | 适合新增 `ForceRegionKernel` |
|
||||
| `lbm/stepper` | 编排主 step 与辅助 kernel | 适合插入 force-region launch |
|
||||
| `operators/helpers` | 现有全局 Guo forcing 路径 | 可复用公式与语义 |
|
||||
|
||||
需要特别注意的一点是:现有全局体力通过 `d_params.fx/fy/fz` 进入 `collide_dispatch()`。这条路径服务的是**全场统一 forcing**。局部体力如果要完全沿用同一语义,最严格的做法是让每个 cell 在线程内部读取本地 force。但这会推动设计进入全场 owner map 或 per-cell force field,而这不符合第一阶段的低侵入目标。
|
||||
|
||||
## 推荐的第一阶段总路线
|
||||
|
||||
推荐路线是:
|
||||
|
||||
- `force_region` 作为一种新对象类型存在
|
||||
- 它不修改 domain flag
|
||||
- 它只产出稀疏 cell footprint
|
||||
- 由一个独立的 `ForceRegionKernel` 在主 step 外对这些 cell 做局部 forcing 修正
|
||||
|
||||
换句话说,第一阶段采用的是:
|
||||
|
||||
- **主推进仍保持全场统一**
|
||||
- **局部 forcing 通过稀疏后处理 kernel 注入**
|
||||
|
||||
这不是最终最泛化的架构,但它与现有 `sensor` 管理方式一致,工作量低,也最不容易破坏当前稳定基线。
|
||||
|
||||
## 为什么不优先采用全场 owner map
|
||||
|
||||
全场 owner map 的优点是语义干净:`OneStep` 的每个 cell 都可直接知道自己是否属于某个区域,再把局部体力并入 collision forcing。这在数值上更直接,也更容易扩展到复杂局部 forcing。
|
||||
|
||||
但在当前阶段,这条路的代价偏高:
|
||||
|
||||
- 需要给全场新增一个 device-side 标识数组
|
||||
- 需要改主 step kernel 形参和热路径逻辑
|
||||
- 需要明确多区域覆盖规则
|
||||
- 需要同时处理 double-buffer 与 esopull 路径中的局部 forcing 接入方式
|
||||
|
||||
对当前目标来说,这会把“加一个可用功能”变成“改造主推进语义”。因此,第一阶段更合适的路线仍是稀疏 compact list。
|
||||
|
||||
## 为什么不把 force region 直接并入 sensor
|
||||
|
||||
二者的几何 footprint 确实相似,都可以表示成“圆形覆盖的一批 cell”。但功能角色不同:
|
||||
|
||||
| 类型 | 本质 | 时序位置 |
|
||||
|---|---|---|
|
||||
| `sensor` | 读出局部信息 | step 后 |
|
||||
| `force_region` | 修改 DDF 演化 | step 中或 step 后的局部修正 |
|
||||
|
||||
因此,第一阶段不建议在抽象层面合并二者。更合理的做法是:
|
||||
|
||||
- 复用几何 footprint 生成逻辑
|
||||
- 保持独立对象语义
|
||||
- 保持独立 kernel 与独立 compact list
|
||||
|
||||
这样既能少写代码,也不会把接口语义搅乱。
|
||||
|
||||
## 对象层建议
|
||||
|
||||
`force_region` 建议作为一种新的 body-side 对象类型,而不是把普通刚体、sensor、局部 forcing 区域全部塞进同一语义分支。
|
||||
|
||||
### 推荐最低要求
|
||||
|
||||
| 字段 | 作用 |
|
||||
|---|---|
|
||||
| `obj_id` | 索引 action 与 compact list |
|
||||
| `geometry` | 圆形 footprint 来源 |
|
||||
| `state` | 可沿用通用容器,但第一阶段基本不用 |
|
||||
| `control` 或 action slot | 存 `fx, fy` |
|
||||
| `type tag` | 区分普通 body、sensor、force_region |
|
||||
|
||||
### 不必在第一阶段敲死的点
|
||||
|
||||
下面这些都可以留给 coder 选择:
|
||||
|
||||
- 是加 `is_force_region` 布尔字段,还是引入更清楚的 `role` / `kind` 标签
|
||||
- `fx, fy` 放在 `state`、`control`,还是直接只存在 `action` 打包层
|
||||
- `add_body("force_region", ...)` 还是单独加 `add_force_region(...)`
|
||||
|
||||
第一阶段更重要的是把数据链接起来,而不是先做完美 API。
|
||||
|
||||
## 几何层建议
|
||||
|
||||
`body/geometry/circle` 已经有生成 sensor cells 的逻辑。这里最自然的做法不是复制一份,而是把“圆形 footprint cell 枚举”抽成一个中性函数。
|
||||
|
||||
### 推荐抽象
|
||||
|
||||
建议在 circle geometry 中形成一个中性能力,例如:
|
||||
|
||||
- `build_footprint_cells()`
|
||||
|
||||
然后:
|
||||
|
||||
- `sensor` 使用这批 cells 作为采样点
|
||||
- `force_region` 使用同一批 cells 作为施力点
|
||||
|
||||
### 第一阶段不必决定的点
|
||||
|
||||
- footprint 是否严格包含圆内格点中心
|
||||
- 是否允许边界一圈采用不同准则
|
||||
- 是否为将来的 polygon/mesh 提前抽公共父类 helper
|
||||
|
||||
只要 footprint 定义在 host 和测试中保持一致即可。
|
||||
|
||||
## flag 处理建议
|
||||
|
||||
`force_region` 第一阶段**不应修改 flag field**。
|
||||
|
||||
原因很直接:
|
||||
|
||||
- 它不是 solid
|
||||
- 它不是 boundary condition
|
||||
- 它不是 sensor flag
|
||||
- 它不参与 channel topology 构造
|
||||
|
||||
因此它应当是“有 footprint,但不 overlay flag”的对象。
|
||||
|
||||
这意味着在 `build_flags()` 过程中:
|
||||
|
||||
- 普通 body 继续覆盖 obstacle / curved 标记
|
||||
- sensor 继续覆盖 sensor 标记
|
||||
- force_region 返回零 mask,或根本不进入该分支
|
||||
|
||||
两种实现都可以,留给 coder 决定。若追求语义更清楚,倾向于根本不让它参与 flag merge。
|
||||
|
||||
## compact list 设计建议
|
||||
|
||||
第一阶段建议新增一条与 sensor 平行的数据链:
|
||||
|
||||
- `force_region cells`
|
||||
- `force_region obj_id`
|
||||
|
||||
### 数据最小集
|
||||
|
||||
| 列 | 类型 | 作用 |
|
||||
|---|---|---|
|
||||
| `cells` | `uint32` | 被施力格点索引 |
|
||||
| `obj_id` | `int32` | 从 action 中取 `fx, fy` |
|
||||
|
||||
这与 `SensorSoA` 的结构完全同型,只是消费 kernel 不同。
|
||||
|
||||
### 可选实现
|
||||
|
||||
| 方案 | 特点 |
|
||||
|---|---|
|
||||
| 单独建 `ForceRegionSoA` | 语义清楚,后续好扩展 |
|
||||
| 复用 `SensorSoA` 结构实现新字段 | 改动少,但语义稍混 |
|
||||
|
||||
第一阶段更推荐单独建 `ForceRegionSoA`。这会多一点样板代码,但可读性更好,也避免后续 `sensor` 和 `force_region` 在 field 中混淆。
|
||||
|
||||
## action contract 建议
|
||||
|
||||
当前 `action` buffer 在 2D 下每对象为 `3 * dim = 6` 个 `float`,而现阶段只真正使用了最后一个 `omega`。这给了第一阶段一个很好的低成本入口:
|
||||
|
||||
- 不改 action buffer 总长度
|
||||
- 不改编译期 layout 宏
|
||||
- 先在已有 slot 中约定 `fx, fy` 的位置
|
||||
|
||||
### 一个可行的最小约定
|
||||
|
||||
| slot | 含义 |
|
||||
|---|---|
|
||||
| `0` | `fx` |
|
||||
| `1` | `fy` |
|
||||
| `5` | `omega` |
|
||||
|
||||
其余位置保留。
|
||||
|
||||
### 这里不必现在敲死
|
||||
|
||||
这部分可以留给 coder 做最终选择,只要满足两点:
|
||||
|
||||
- `force_region` kernel 能稳定读到 `fx, fy`
|
||||
- 现有 `CurvedBoundaryKernel` 继续稳定读到 `omega`
|
||||
|
||||
若 coder 认为显式 helper 更重要,可以新增类似 `action_force_x()` / `action_force_y()` 的 device helper,而不是在 kernel 中直接写魔数偏移。
|
||||
|
||||
## kernel 路线建议
|
||||
|
||||
第一阶段最合适的是新增一个独立 kernel,例如:
|
||||
|
||||
- `ForceRegionKernel`
|
||||
|
||||
它应当像 `SensorKernel` 一样按 compact list 启动,而不是并入 `OneStep`。
|
||||
|
||||
### 建议职责
|
||||
|
||||
每个线程处理一个 force-region cell:
|
||||
|
||||
1. 读该 cell 的线性索引 `k`
|
||||
2. 读所属对象 `obj_id`
|
||||
3. 从 `action` 中读 `fx, fy`
|
||||
4. 从当前 DDF 缓冲中加载该 cell 的 `f[NQ]`
|
||||
5. 计算 `rho, ux, uy`
|
||||
6. 按现有 Guo forcing 语义构造 `Fin`
|
||||
7. 对该 cell 的分布函数做 forcing 修正
|
||||
8. 写回 DDF
|
||||
|
||||
### 需要特别控制的事
|
||||
|
||||
第一阶段最好不要在这个 kernel 内部引入过多策略分支。尤其避免一开始就支持:
|
||||
|
||||
- 多个不同 forcing scheme
|
||||
- 复杂边界跳过逻辑
|
||||
- 非均匀空间 force
|
||||
- 区域内按距离衰减的 force
|
||||
|
||||
先只做“圆内均匀体力”。
|
||||
|
||||
## ForceRegionKernel 应该写在哪个时间位置
|
||||
|
||||
现有 `stepper` 顺序大致为:
|
||||
|
||||
1. curved boundary
|
||||
2. main step
|
||||
3. sensor
|
||||
|
||||
第一阶段建议将其改为:
|
||||
|
||||
1. curved boundary
|
||||
2. main step
|
||||
3. force region
|
||||
4. sensor
|
||||
|
||||
### 这样做的原因
|
||||
|
||||
- 主推进链尽量不动
|
||||
- force region 作为局部修正子步存在
|
||||
- sensor 若未来同时存在,可以读到施力后的局部结果
|
||||
|
||||
### 这里保留给 coder 的决策点
|
||||
|
||||
`ForceRegionKernel` 也可以放在 sensor 前后不同位置,但需要统一解释语义:
|
||||
|
||||
| 放置位置 | 语义 |
|
||||
|---|---|
|
||||
| 主 step 之后、sensor 之前 | sensor 读到施力后的状态 |
|
||||
| sensor 之后 | sensor 读到施力前主步结果 |
|
||||
|
||||
若第一阶段不做 sensor 联动,则这只是将来兼容性的选择。默认更建议放在 sensor 之前。
|
||||
|
||||
## DDF 更新方式的设计空间
|
||||
|
||||
这里是实现中最需要留白的部分。第一阶段文档不应把写法锁死,但应给出边界。
|
||||
|
||||
### 必须满足的原则
|
||||
|
||||
- 局部体力的数值语义尽量对齐现有 `forcing_guo.cuh`
|
||||
- 不应在 `ForceRegionKernel` 中引入与现有 collision family 矛盾的私有公式
|
||||
- 若采用 operator splitting,应在注释中明确说明它不是“主 collision 内联 forcing”
|
||||
|
||||
### 可接受的实现方向
|
||||
|
||||
| 方向 | 特点 |
|
||||
|---|---|
|
||||
| 直接按 Guo forcing 增量对 `f[i]` 做修正 | 最小、直观 |
|
||||
| 抽一个独立 helper,复用 `compute_guo_forcing()` | 与现有 forcing 公式最一致 |
|
||||
| 在 kernel 中根据 collision family 调整 prefactor | 更接近现有 `collide_*` 写法,但代码稍重 |
|
||||
|
||||
### 第一阶段更适合的判断
|
||||
|
||||
更推荐:
|
||||
|
||||
- 尽量复用 `compute_guo_forcing()`
|
||||
- 用很薄的一层 helper 完成 DDF 增量写回
|
||||
|
||||
但是否把 `c_tau = 1 - omega/2` 放在 kernel 里、helper 里,还是借新函数收口,可以留给 coder。
|
||||
|
||||
## 数值语义说明
|
||||
|
||||
第一阶段方案本质上是一个**局部 forcing 修正子步**,而不是严格在 `OneStep` 内逐格并入 collision forcing。这一点需要在实现处用注释写清楚。
|
||||
|
||||
### 这个选择的代价
|
||||
|
||||
- 语义上更像 operator splitting
|
||||
- 与全局 forcing 在热路径中的时序并不完全相同
|
||||
- 在强 forcing 情况下,可能与将来更严格实现有差异
|
||||
|
||||
### 这个选择的收益
|
||||
|
||||
- 主 kernel 改动极小
|
||||
- 不需要全场 owner map
|
||||
- 易于复用现有稀疏对象链路
|
||||
- 第一阶段很快能得到可用结果
|
||||
|
||||
对当前“先追求使用”的目标,这个 tradeoff 是合理的。
|
||||
|
||||
## 过滤规则建议
|
||||
|
||||
第一阶段应当保持规则简单。
|
||||
|
||||
### 建议规则
|
||||
|
||||
- 只对 `is_fluid(fl)` 的格点施力
|
||||
- 对 obstacle / wall / inlet / outlet 一律跳过
|
||||
- 与 curved body footprint 重叠时,由 cell flag 决定是否生效,而不是 force-region 自己猜
|
||||
|
||||
### 这里不必先完善的点
|
||||
|
||||
- actuator 与 actuator 的重叠叠加规则
|
||||
- 对 inlet/outlet 邻近区域的特殊处理
|
||||
- 对 sensor / force_region 同 id 绑定的逻辑
|
||||
|
||||
如果第一阶段允许多个 force region 存在,最简单的策略是先约束:
|
||||
|
||||
- footprint 不应重叠
|
||||
|
||||
或者:
|
||||
|
||||
- 重叠行为未定义,由上层调用者避免
|
||||
|
||||
## 配置与 API 建议
|
||||
|
||||
这里也不需要一次性定死。
|
||||
|
||||
### 最小外部接口至少应支持
|
||||
|
||||
- 新建一个圆形 `force_region`
|
||||
- 运行时更新它的 `fx, fy`
|
||||
|
||||
### 可选 API 形式
|
||||
|
||||
| 形式 | 特点 |
|
||||
|---|---|
|
||||
| `add_body("force_region", ...)` | 改动少,沿用现有入口 |
|
||||
| `add_force_region(...)` | 语义更清楚 |
|
||||
|
||||
### 运行时控制也有两种自然路线
|
||||
|
||||
| 形式 | 特点 |
|
||||
|---|---|
|
||||
| 扩展 `set_body(id, fx=..., fy=...)` | 改动小 |
|
||||
| 新增 `set_force_region(id, fx=..., fy=...)` | 语义清楚 |
|
||||
|
||||
第一阶段若追求少改代码,可优先沿用现有 `set_body()`。若 coder 认为 API 语义更重要,也可以新增专门入口。
|
||||
|
||||
## 对 `ObjectManager` 的建议
|
||||
|
||||
`ObjectManager` 已经偏大,因此第一阶段不建议再把很多新判断揉成一团。更合适的方式是:
|
||||
|
||||
- 在现有结构上增加一条最薄的新分支
|
||||
- 尽量与 `sensor` 的组织方式平行
|
||||
- 不在 `build_flags()` 中塞 force-region 的特例
|
||||
|
||||
### 在实现层面建议保持的分工
|
||||
|
||||
| 职责 | 位置 |
|
||||
|---|---|
|
||||
| 圆形 footprint 枚举 | `geometry` |
|
||||
| force-region SoA 打包 | `body/coupling` 或与其平行的位置 |
|
||||
| GPU buffer 生命周期 | `field` / SoA 容器 |
|
||||
| kernel 调度 | `stepper` |
|
||||
|
||||
这能避免 `manager` 再次回到“万能管理器”的方向。
|
||||
|
||||
## 对测试的建议
|
||||
|
||||
第一阶段至少需要三类测试。
|
||||
|
||||
### 1. 结构测试
|
||||
|
||||
- `force_region` 不改变 flag field
|
||||
- compact list cell 数与 footprint 预期一致
|
||||
- action 更新后 GPU buffer 内容可追踪
|
||||
|
||||
### 2. 数值冒烟测试
|
||||
|
||||
- 静止流场中施加恒定 `fx` 后,区域内流体沿 x 方向加速
|
||||
- `fx > 0` 与 `fx < 0` 响应方向相反
|
||||
- `fy` 路径同理
|
||||
|
||||
### 3. 回归保护
|
||||
|
||||
- 未添加 `force_region` 时,结果与当前版本一致
|
||||
- 添加 `force_region` 但 `fx=fy=0` 时,结果与当前版本一致或只存在舍入级差异
|
||||
|
||||
### 暂不要求的测试
|
||||
|
||||
- 与解析解严格比对
|
||||
- 多 region 重叠叠加
|
||||
- 强 forcing 的稳定性边界扫描
|
||||
|
||||
## 第一阶段完成后的理想状态
|
||||
|
||||
完成后,项目应具备以下能力:
|
||||
|
||||
- 能创建一个圆形局部施力区域
|
||||
- 能在运行时改变其 `fx, fy`
|
||||
- 不需要重新编译
|
||||
- 不需要引入全场 owner map
|
||||
- 不破坏现有 curved boundary 与 sensor 链路
|
||||
- 不迫使 `body` 与 `lbm` 做新的深度耦合
|
||||
|
||||
从架构角度看,第一阶段的关键成果不是“得到最完美的 actuator 抽象”,而是验证:
|
||||
|
||||
- 稀疏 cell list 路线足以支撑局部 forcing 研究
|
||||
- 现有系统能容纳一条新的 compact-list kernel 链路
|
||||
|
||||
## 第二阶段可能的演进方向
|
||||
|
||||
这些不属于当前实现范围,但第一阶段设计应避免把它们堵死:
|
||||
|
||||
- 读取 force-region 内平均速度或其他观测量
|
||||
- 将 `force_region` 与 `sensor` 的 footprint builder 进一步共用
|
||||
- 升级为更一般的 `region operator`
|
||||
- 改为在 `OneStep` 内逐格读取局部 force
|
||||
- 支持非圆形几何与 3D
|
||||
|
||||
第一阶段不需要为这些能力写完结构,只需要不要在命名和数据布局上把后路堵死。
|
||||
|
||||
## 建议的实现顺序
|
||||
|
||||
### 阶段 1
|
||||
|
||||
建立最小对象与 footprint 数据链。
|
||||
|
||||
- 新增 `force_region` 类型
|
||||
- 让 circle geometry 能产出中性 footprint cells
|
||||
- 新增 force-region SoA 与 GPU upload
|
||||
|
||||
### 阶段 2
|
||||
|
||||
接入运行时 action。
|
||||
|
||||
- 在 action slot 中为 `fx, fy` 留位置
|
||||
- 增加 host 侧更新接口
|
||||
- 保证不影响现有 `omega` 读取
|
||||
|
||||
### 阶段 3
|
||||
|
||||
新增局部 forcing kernel 并接入调度。
|
||||
|
||||
- 写 `ForceRegionKernel`
|
||||
- 接入 `stepper`
|
||||
- 明确与 sensor 的先后顺序
|
||||
|
||||
### 阶段 4
|
||||
|
||||
补最小测试与文档注释。
|
||||
|
||||
- 结构测试
|
||||
- 冒烟测试
|
||||
- 未启用时的回归保护
|
||||
|
||||
## 最终判断
|
||||
|
||||
第一阶段最稳妥的最小方案不是全场 owner map,也不是把 force-region 并进 sensor,而是:
|
||||
|
||||
- 把它当作一种新的稀疏 cell-list 对象
|
||||
- 复用圆形 footprint 生成逻辑
|
||||
- 通过独立 `ForceRegionKernel` 对局部 DDF 做 forcing 修正
|
||||
- 在接口和数据布局上只做最低必要改动
|
||||
|
||||
这条路线与现有代码风格最一致,也为后续更严格或更泛化的实现保留了余地。
|
||||
398
docs/validation_specs/Force_region_validation.md
Normal file
398
docs/validation_specs/Force_region_validation.md
Normal file
@ -0,0 +1,398 @@
|
||||
# Force region 验证清单
|
||||
|
||||
这份清单面向当前已经实现的最小版 `force_region`:
|
||||
|
||||
- 2D
|
||||
- 圆形 footprint
|
||||
- 运行时设置均匀 `fx, fy`
|
||||
- operator-split 的局部 Guo forcing 修正
|
||||
- 不与 sensor 绑定
|
||||
|
||||
当前最合理的验证思路不是一次追求“单一终极 benchmark”,而是分三层推进:
|
||||
|
||||
1. **代码回归层**:先确认新功能没有破坏旧链路
|
||||
2. **物理正确性层**:先做最稳的流体响应验证
|
||||
3. **研究使用层**:再进入圆柱尾迹中的局部强迫实验
|
||||
|
||||
文献上,LBM forcing 的核心依据仍是 Guo forcing 的离散处理 [Guo02d]。对 body-force 驱动的通道流,Poiseuille 流是最经典的基准之一,[Zou95b] 给出了平板间 Poiseuille 流的解析结果,[Guo02d] 也把 steady Poiseuille flow 作为 forcing 验证的一部分。对**空间变化** forcing 的风险与误差来源,[Sil20b] 给出了很直接的提醒:非均匀 body force 会把离散误差暴露得更明显,因此验证必须包含**网格、黏度、幅值**三个维度的稳健性检查。
|
||||
|
||||
## 一句话总策略
|
||||
|
||||
先不要一上来把 `force_region` 扔进圆柱尾迹做复杂控制。先用最简单、最容易解释的流场把它跑顺:
|
||||
|
||||
- 零强迫回归
|
||||
- 单方向局部加速
|
||||
- 长通道近似 Poiseuille
|
||||
- 再进圆柱尾迹
|
||||
|
||||
## 第一层
|
||||
|
||||
### 必做回归检查
|
||||
|
||||
这些检查不依赖文献,但必须先过。
|
||||
|
||||
| 编号 | 目的 | 设置 | 预期结果 | 是否阻塞 |
|
||||
|---|---|---|---|---|
|
||||
| R1 | 新功能不引入假障碍 | 只加 `force_region`,不加圆柱 | `n_curved == 0`,flag 场不出现 obstacle / curved 覆盖 | 是 |
|
||||
| R2 | 零强迫不改变解 | 加 `force_region`,但 `fx=fy=0` | 与无 `force_region` 基线一致,或只剩舍入级差异 | 是 |
|
||||
| R3 | force 设置后可持续生效 | `set_force()` 后步进,再触发一次 action 刷新链路 | force 不被静默清零 | 是 |
|
||||
| R4 | 符号方向正确 | 同一算例跑 `+fx` 和 `-fx` | 主流响应方向相反,幅值同量级 | 是 |
|
||||
|
||||
R2 很关键。它回答的是:当前实现是不是“只是多了一条 dormant 数据链”,而不是“只要建了 force_region 就改了解”。
|
||||
|
||||
## 第二层
|
||||
|
||||
### 物理正确性主验证
|
||||
|
||||
这部分建议按从简单到复杂的顺序做。
|
||||
|
||||
## Case A
|
||||
|
||||
### 静止流场中的单圆局部加速
|
||||
|
||||
这是最小物理 sanity check。
|
||||
|
||||
#### 设置建议
|
||||
|
||||
- 格子:`nx = 256`, `ny = 128`
|
||||
- 碰撞:先用 `SRT`,LES 关闭
|
||||
- 流动初值:入口速度设为 0 或足够小,先得到近静止背景流
|
||||
- 不加圆柱
|
||||
- force region:
|
||||
- 圆心 `(96, 64)`
|
||||
- 半径 `r = 12`
|
||||
- 先做 `fx > 0, fy = 0`
|
||||
- 强迫幅值:从小值开始,例如 `fx = 1e-6`, `5e-6`, `1e-5`
|
||||
- 每组运行到准稳态或至少足够长,使局部响应明显
|
||||
|
||||
#### 该看什么
|
||||
|
||||
- `ux` 在 force region 附近应整体偏正
|
||||
- 区域下游应形成沿 `+x` 延伸的动量尾迹
|
||||
- `uy` 的主量级应明显小于 `ux`
|
||||
- 将 `fx` 改成负值后,整幅图应沿 x 方向近似镜像
|
||||
|
||||
#### 预期结果
|
||||
|
||||
这个 case 没有一个成熟的“标准 benchmark 数字”,但应满足最基本物理响应:
|
||||
|
||||
- 加速方向与 force 方向一致
|
||||
- 幅值随 `fx` 单调增强
|
||||
- 小 force 下解的结构应平滑,不应出现棋盘格、局部爆点或非对称破缺
|
||||
|
||||
#### 判定标准
|
||||
|
||||
| 检查项 | 通过标准 |
|
||||
|---|---|
|
||||
| 方向性 | `fx > 0` 时主响应沿 `+x`,`fx < 0` 时相反 |
|
||||
| 单调性 | 增大 `fx` 后,局部 `ux` 峰值与尾迹强度单调增大 |
|
||||
| 对称性 | 在无背景横向偏置时,上下半区统计应近似对称 |
|
||||
| 稳定性 | 小幅强迫下不出现数值振荡或孤立噪点 |
|
||||
|
||||
这个 case 的作用不是给出论文级定量结论,而是先证明:**局部 forcing 至少在最简单流场中按直觉工作。**
|
||||
|
||||
## Case B
|
||||
|
||||
### 长通道中的近 Poiseuille 验证
|
||||
|
||||
这是最重要的文献支持主验证。
|
||||
|
||||
Poiseuille 流是 LBM forcing 的经典基准,[Guo02d] 用 steady Poiseuille flow 验证 forcing 处理,[Zou95b] 给出了平板间 Poiseuille 流的解析形式。对当前代码,最现实的做法不是强行构造完全理想的周期通道,而是在**长通道中用超大圆形 force region 近似全域恒定体力驱动**,只在远离入口和出口的位置取剖面比较。
|
||||
|
||||
#### 建议设置
|
||||
|
||||
- 格子:`nx = 1024`, `ny = 128`
|
||||
- 碰撞:先用 `SRT`;通过后再试 `TRT` / `MRT`
|
||||
- LES:关闭
|
||||
- 圆柱:不加
|
||||
- 入口速度:尽量设小,避免 inlet 自身主导流场
|
||||
- force region:
|
||||
- 圆心放在通道中段,例如 `(512, 64)`
|
||||
- 半径取足够大,使绝大多数通道内部 fluid cell 都被覆盖,例如 `r = 600`
|
||||
- 强迫方向:`fx > 0, fy = 0`
|
||||
- 强迫幅值:先从小值开始,避免压缩性误差,例如 `1e-6` 量级
|
||||
|
||||
#### 为什么这样设
|
||||
|
||||
当前 `force_region` 只有圆形 footprint,没有全域矩形 forcing,也没有 x 周期边界。因此最稳妥的近似是:
|
||||
|
||||
- 用超大圆把中部通道几乎全覆盖
|
||||
- 只在远离入口和出口的位置取横截面
|
||||
- 检查剖面是否接近抛物线
|
||||
|
||||
#### 该比对什么
|
||||
|
||||
对 fully developed Poiseuille,经典剖面是抛物线 [Zou95b]:
|
||||
|
||||
\[
|
||||
u(y) = u_{max}\left(1 - \frac{y^2}{L^2}\right)
|
||||
\]
|
||||
|
||||
其中中心线速度最大,壁面速度为 0。对当前代码,最重要的不是把解析常数拟合到最后一位,而是确认以下结构性特征:
|
||||
|
||||
- 中部横截面是单峰抛物形
|
||||
- 中心线最大
|
||||
- 贴壁速度接近 0
|
||||
- `uy` 远小于 `ux`
|
||||
- 沿 x 方向进入发展段后,剖面变化减小
|
||||
|
||||
#### 推荐取样方式
|
||||
|
||||
在以下几个 x 位置取横截面:
|
||||
|
||||
- `x = 256`
|
||||
- `x = 512`
|
||||
- `x = 768`
|
||||
|
||||
只要中后段两条剖面几乎重合,就说明流场已经接近 fully developed,可拿来与抛物线拟合。
|
||||
|
||||
#### 预期结果
|
||||
|
||||
- 中段与后段 `ux(y)` 应近似抛物线 [Guo02d, Zou95b]
|
||||
- 若 `fx` 增大,剖面整体抬高,但形状仍保持单峰抛物型
|
||||
- 中段以后,剖面随 x 的变化应减弱
|
||||
|
||||
#### 如何定量判定
|
||||
|
||||
建议做三个量:
|
||||
|
||||
| 指标 | 解释 | 通过建议 |
|
||||
|---|---|---|
|
||||
| 抛物线拟合残差 | 用最小二乘拟合 `u(y)=a-b(y-yc)^2` | 中后段残差明显小于入口附近 |
|
||||
| 发展段一致性 | 比较两个下游截面 | 两条曲线基本重合 |
|
||||
| 横向速度污染 | `max |uy| / max |ux|` | 远小于 1 |
|
||||
|
||||
#### 关于误差的解释
|
||||
|
||||
不要把这个 case 解释得过头。因为当前代码的边界条件并不是“理想周期 x + 纯 body-force 驱动”,所以更合理的表述是:
|
||||
|
||||
- 这是一个**近 Poiseuille** 验证
|
||||
- 它主要验证 `force_region` 是否能在长通道里生成正确的 fully-developed 流动结构
|
||||
- 它不是对解析解逐点 machine-accuracy 的测试
|
||||
|
||||
另外,[Zou95b] 指出 bounce-back 边界会引入一阶壁面误差。因此如果壁面附近有明显偏差,不必立刻把它归咎于 `force_region` 本身;重点看的是中部剖面形状与下游收敛行为 [Zou95b]。
|
||||
|
||||
## Case C
|
||||
|
||||
### 幅值扫描
|
||||
|
||||
这个 case 用来回答:当前 operator-split forcing 在多大范围内是“线性可解释”的。
|
||||
|
||||
#### 设置建议
|
||||
|
||||
复用 Case B,只扫描 `fx`:
|
||||
|
||||
- `1e-6`
|
||||
- `2e-6`
|
||||
- `5e-6`
|
||||
- `1e-5`
|
||||
- `2e-5`
|
||||
|
||||
#### 预期结果
|
||||
|
||||
在小 force 区间内:
|
||||
|
||||
- 中心线速度大致随 `fx` 单调增加
|
||||
- 抛物剖面形状保持不变,仅幅值放大
|
||||
- 若开始出现明显非对称、密度波动或剖面畸变,说明已进入“不再适合作为线性基准”的区间
|
||||
|
||||
#### 该记录的量
|
||||
|
||||
- 中心线最大速度
|
||||
- 下游某截面的体积流量
|
||||
- `rho` 偏离 1 的最大值
|
||||
|
||||
这一步的目的不是证明某个严格比例律,而是找到一个**数值上舒服的工作区间**。后面做圆柱尾迹控制时,应优先选这个区间内的 force 幅值。
|
||||
|
||||
## Case D
|
||||
|
||||
### 网格与黏度稳健性
|
||||
|
||||
这是空间变化 forcing 必须做的检查,[Sil20b] 对这一点尤其值得参考。该文的核心提醒不是“所有非均匀 forcing 都会错”,而是:**空间变化 body force 更容易把离散误差暴露出来**,因此需要明确看网格和黏度敏感性 [Sil20b]。
|
||||
|
||||
#### 推荐做法
|
||||
|
||||
选用 Case B 或 Case A 中一个已经表现稳定的设置,然后做:
|
||||
|
||||
### 网格扫描
|
||||
|
||||
- `256 x 64`
|
||||
- `512 x 128`
|
||||
- `1024 x 256`
|
||||
|
||||
保持物理几何比例一致。
|
||||
|
||||
### 黏度扫描
|
||||
|
||||
固定一个小 force 幅值,扫两到三个 `nu`。
|
||||
|
||||
#### 预期结果
|
||||
|
||||
- 网格加密后,主剖面与主流结构应收敛
|
||||
- 黏度变化会改变速度幅值和扩散尺度,但不应引入新的奇怪畸变
|
||||
- 如果某个设置对黏度特别敏感,优先检查强迫是否过强、局部 Mach 数是否过高、或 region 是否离边界太近
|
||||
|
||||
#### 如何解读
|
||||
|
||||
[Sil20b] 的重点是在 TRT 下讨论空间变化 force 的离散误差来源,并指出某些 force 展开会引入额外不一致项。当前实现采用的是一个 operator-split 的局部 Guo 修正,而不是把空间变化 force 直接并入 `OneStep` 的逐格 collision。因此这一步更应被视为:
|
||||
|
||||
- 对当前实现的**数值稳健性检查**
|
||||
- 而不是与 [Sil20b] 的离散系统逐条同构比较
|
||||
|
||||
## 第三层
|
||||
|
||||
### 研究使用前的圆柱尾迹验证
|
||||
|
||||
在前两层都通过后,再进入最关心的 case:圆柱尾迹中的局部 forcing。
|
||||
|
||||
这里最重要的不是先追一个“标准答案数值”,而是先建立**可解释的响应地图**。
|
||||
|
||||
## Case E
|
||||
|
||||
### 圆柱后方局部推流
|
||||
|
||||
可用你已经验证过的固定圆柱 baseline,先选最稳的低 Re 工况,例如二维层流涡街基线。
|
||||
|
||||
#### 建议设置
|
||||
|
||||
- 使用你当前已经验证可靠的 2D 固定圆柱基线
|
||||
- 先选一个自然涡街清晰、但还不太剧烈的 Re
|
||||
- force region 放在圆柱后方近尾迹区
|
||||
|
||||
例如:
|
||||
|
||||
- 圆柱中心 `(xc, yc)`
|
||||
- force region 中心可先试:
|
||||
- `(xc + 2D, yc)`
|
||||
- `(xc + 3D, yc)`
|
||||
- 半径先从 `0.3D` 到 `0.8D` 扫
|
||||
- 先做 `fx > 0, fy = 0`
|
||||
|
||||
#### 该看什么
|
||||
|
||||
- 尾迹中心线速度是否回升
|
||||
- 近尾迹回流区是否缩短
|
||||
- `Cl` 振幅是否降低
|
||||
- `Cd_mean` 是否变化
|
||||
- 涡脱落频率 `St` 是否偏移
|
||||
- streakline / vorticity 图是否出现更对称或更拉长的尾迹结构
|
||||
|
||||
#### 预期结果
|
||||
|
||||
对于圆柱尾迹中的外加 forcing,不同 forcing 形式都说明了同一个大方向:**局部强迫会改写尾迹结构、拖曳、升力波动与脱落频率**。[Kim05b] 虽然研究的是壁面 blowing/suction 而不是体力区,但其结果清楚表明:分布式 forcing 可以显著抑制或改变 Kármán 涡街,从而改变 mean drag 与 lift fluctuation [Kim05b]。
|
||||
|
||||
对当前 `force_region`,更合理的预期是:
|
||||
|
||||
- 小 `fx > 0`:尾迹被顺流“拉长”,回流区可能缩短
|
||||
- 足够合适的位置和幅值下:`Cl_rms` 下降,尾迹更接近对称
|
||||
- 过强 forcing:可能引入新的不稳定结构,而不是单调“变好”
|
||||
|
||||
#### 判定方式
|
||||
|
||||
建议不要只看一张图,至少同时记录:
|
||||
|
||||
| 量 | 作用 |
|
||||
|---|---|
|
||||
| `Cd_mean` | 看平均阻力变化 |
|
||||
| `Cl_rms` | 看升力波动是否被抑制 |
|
||||
| `St` | 看脱落频率是否移动 |
|
||||
| vorticity / streakline 图 | 看结构是否更对称、更拉长、或出现新模态 |
|
||||
|
||||
#### 这个 case 的定位
|
||||
|
||||
这是**研究型验证**,不是严格 benchmark。
|
||||
|
||||
更准确地说,它回答的是:
|
||||
|
||||
- `force_region` 是否足够稳定和可控,能够进入尾迹控制实验
|
||||
- 不同位置、半径、幅值是否产生可解释的流场响应
|
||||
|
||||
而不是:
|
||||
|
||||
- 某个单一数字是否必须匹配文献到几个百分点
|
||||
|
||||
## 推荐的执行顺序
|
||||
|
||||
建议按下面顺序推进。
|
||||
|
||||
| 顺序 | 算例 | 作用 |
|
||||
|---|---|---|
|
||||
| 1 | R1-R4 | 先清掉实现错误 |
|
||||
| 2 | Case A | 验证最基本物理响应 |
|
||||
| 3 | Case B | 做文献支持最强的主验证 |
|
||||
| 4 | Case C | 找到安全幅值区间 |
|
||||
| 5 | Case D | 看网格与黏度稳健性 |
|
||||
| 6 | Case E | 进入圆柱尾迹研究 |
|
||||
|
||||
## 每个算例建议保存的输出
|
||||
|
||||
为了后面快速回看,建议每个 case 固定保存下面这些量。
|
||||
|
||||
| 输出 | 是否必需 | 说明 |
|
||||
|---|---|---|
|
||||
| 配置文件快照 | 必需 | 保证能复现实验 |
|
||||
| `fx, fy` 与 region 几何 | 必需 | force center, radius, amplitude |
|
||||
| 一张 `ux` 场图 | 必需 | 看主流响应 |
|
||||
| 一张 vorticity 图 | 推荐 | 看结构变化 |
|
||||
| 一条下游横截面 `ux(y)` | 必需 | Poiseuille 与 channel 类验证最关键 |
|
||||
| `rho` 最大偏离 | 推荐 | 监控压缩性污染 |
|
||||
| 若有圆柱:`Cd_mean`, `Cl_rms`, `St` | 必需 | 尾迹控制最关键 |
|
||||
|
||||
## 最低通过线
|
||||
|
||||
如果时间紧,只要先达到下面这三条,就可以开始把 `force_region` 用进研究实验:
|
||||
|
||||
1. **零强迫回归通过**
|
||||
2. **静止流场中局部加速方向正确、幅值单调**
|
||||
3. **长通道中下游横截面能稳定收敛到近抛物剖面**
|
||||
|
||||
做到这三条,说明这个新功能至少已经:
|
||||
|
||||
- 不破坏旧功能
|
||||
- 有正确方向性
|
||||
- 能在最经典的 body-force 场景中生成合理结构
|
||||
|
||||
## 如何解读失败
|
||||
|
||||
### 若 Case A 失败
|
||||
|
||||
优先怀疑:
|
||||
|
||||
- action slot 读写
|
||||
- force region footprint
|
||||
- `is_fluid(flag[k])` 过滤
|
||||
- `ForceRegionKernel` 写回 DDF 的路径
|
||||
|
||||
### 若 Case B 失败
|
||||
|
||||
优先怀疑:
|
||||
|
||||
- force region 是否真的覆盖了足够长的通道中段
|
||||
- inlet/outlet 是否过强地主导了解
|
||||
- force 幅值是否太大
|
||||
- 取样截面是否还在入口发展段
|
||||
|
||||
### 若 Case D 失败
|
||||
|
||||
优先怀疑:
|
||||
|
||||
- spatially varying forcing 本身的离散误差被放大
|
||||
- 当前 operator-split 修正对该参数区间不够稳
|
||||
- 需要收缩到更小 force、更低 Mach、更细网格
|
||||
|
||||
## 最后的建议
|
||||
|
||||
当前 `force_region` 最适合先被当作一个**研究工具**,而不是一开始就当作“已严格 benchmark 的标准物理模块”。最稳妥的路线是:
|
||||
|
||||
- 用 [Guo02d] 和 [Zou95b] 支撑主验证思路
|
||||
- 用 [Sil20b] 提醒自己必须做稳健性检查
|
||||
- 用 [Kim05b] 作为圆柱尾迹控制的高层参考,理解应该关注哪些响应量
|
||||
|
||||
这样做,既能尽快把功能用于研究,也不会把验证叙事说得过头。
|
||||
|
||||
## 参考依据
|
||||
|
||||
- Guo, Zheng, Shi 讨论了 forcing term 的离散效应,并将 steady Poiseuille flow 作为数值验证的一部分 [Guo02d]
|
||||
- Zou, Hou, Doolen 给出了平板间 Poiseuille 流的解析解,并指出 bounce-back 壁面会引入一阶误差 [Zou95b]
|
||||
- Silva 分析了空间变化 body force 在 LBM 中的离散误差来源,说明非均匀 forcing 必须做网格与黏度稳健性检查 [Sil20b]
|
||||
- Kim 和 Choi 表明分布式 forcing 可以显著改变圆柱尾迹、drag、lift fluctuation 和 shedding mode,可作为圆柱尾迹研究阶段的高层参照 [Kim05b]
|
||||
@ -130,3 +130,23 @@ def pack_sensor_to_soa(
|
||||
idx_arr[i] = np.uint32(sc.idx)
|
||||
id_arr[i] = np.int32(sc.body_id)
|
||||
return idx_arr, id_arr
|
||||
|
||||
|
||||
def pack_force_region_to_soa(
|
||||
cells: List[SensorCell],
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Pack SensorCell list into 2-column SoA for ForceRegionSoA.
|
||||
|
||||
Reuses SensorCell records since the footprint enumeration is identical
|
||||
to sensor cells. Returns:
|
||||
cells (uint32), obj_ids (int32) -- empty arrays if *cells* is empty.
|
||||
"""
|
||||
n = len(cells)
|
||||
if n == 0:
|
||||
return np.zeros(0, dtype=np.uint32), np.zeros(0, dtype=np.int32)
|
||||
idx_arr = np.empty(n, dtype=np.uint32)
|
||||
id_arr = np.empty(n, dtype=np.int32)
|
||||
for i, sc in enumerate(cells):
|
||||
idx_arr[i] = np.uint32(sc.idx)
|
||||
id_arr[i] = np.int32(sc.body_id)
|
||||
return idx_arr, id_arr
|
||||
|
||||
@ -129,16 +129,18 @@ class ObjectManager:
|
||||
|
||||
# -- Compact list building -----------------------------------------------
|
||||
def build_compact_lists(self, domain_flags: np.ndarray | None = None):
|
||||
"""Build cut-link SoA columns and sensor lists.
|
||||
"""Build cut-link SoA columns, sensor lists, and force-region lists.
|
||||
|
||||
Returns:
|
||||
cl_fluid_idx, cl_dir, cl_q, cl_body_id, cl_rx, cl_ry, cl_rz,
|
||||
cl_fallback_class,
|
||||
sensor_cells, sensor_obj_id
|
||||
sensor_cells, sensor_obj_id,
|
||||
fr_cells, fr_obj_id
|
||||
"""
|
||||
cl_idx, cl_dir, cl_q, cl_bid = [], [], [], []
|
||||
cl_rx, cl_ry, cl_rz, cl_fallback = [], [], [], []
|
||||
s_cells, s_ids = [], []
|
||||
fr_cells, fr_ids = [], []
|
||||
self.sensor_cell_counts.fill(0)
|
||||
|
||||
ez = None
|
||||
@ -148,7 +150,8 @@ class ObjectManager:
|
||||
ex, ey = D2Q9_EX, D2Q9_EY
|
||||
|
||||
for obj in self._registry.objects:
|
||||
if not obj.is_sensor and hasattr(obj, 'get_curved_list'):
|
||||
# Curved list: only real obstacle bodies (not sensors, not force_regions).
|
||||
if (not obj.is_sensor) and (not obj.is_force_region) and hasattr(obj, 'get_curved_list'):
|
||||
(
|
||||
fluid_idx, dirs, q_vals, body_ids,
|
||||
rx_vals, ry_vals, rz_vals, fallback_vals
|
||||
@ -173,6 +176,11 @@ class ObjectManager:
|
||||
s_ids.append(ids)
|
||||
if 0 <= obj.obj_id < self.sensor_cell_counts.size:
|
||||
self.sensor_cell_counts[obj.obj_id] = int(len(cells))
|
||||
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
|
||||
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
|
||||
if len(cells) > 0:
|
||||
fr_cells.append(cells)
|
||||
fr_ids.append(ids)
|
||||
|
||||
r_idx = np.concatenate(cl_idx) if cl_idx else np.zeros(0, dtype=np.uint32)
|
||||
r_dir = np.concatenate(cl_dir) if cl_dir else np.zeros(0, dtype=np.uint8)
|
||||
@ -187,11 +195,14 @@ class ObjectManager:
|
||||
)
|
||||
sensor_cells = np.concatenate(s_cells) if s_cells else np.zeros(0, dtype=np.uint32)
|
||||
sensor_obj_id = np.concatenate(s_ids) if s_ids else np.zeros(0, dtype=np.int32)
|
||||
r_fr_cells = np.concatenate(fr_cells) if fr_cells else np.zeros(0, dtype=np.uint32)
|
||||
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
||||
|
||||
return (
|
||||
r_idx, r_dir, r_q, r_bid,
|
||||
r_rx, r_ry, r_rz, r_fallback,
|
||||
sensor_cells, sensor_obj_id,
|
||||
r_fr_cells, r_fr_ids,
|
||||
)
|
||||
|
||||
# -- GPU sync ------------------------------------------------------------
|
||||
@ -214,6 +225,7 @@ class ObjectManager:
|
||||
cl_fluid_idx, cl_dir, cl_q, cl_body_id,
|
||||
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
||||
sensor_cells, sensor_obj_id,
|
||||
fr_cells, fr_obj_id,
|
||||
) = self.build_compact_lists(domain_flags=field.flag)
|
||||
|
||||
field.curved.assign_host(
|
||||
@ -221,6 +233,7 @@ class ObjectManager:
|
||||
cl_rx, cl_ry, cl_rz, cl_fallback_class,
|
||||
)
|
||||
field.sensors.assign_host(sensor_cells, sensor_obj_id)
|
||||
field.force_regions.assign_host(fr_cells, fr_obj_id)
|
||||
|
||||
field.upload_compact_lists()
|
||||
field.update_params(n_objects=self.count)
|
||||
@ -377,13 +390,37 @@ class ObjectManager:
|
||||
if self.action_gpu is not None:
|
||||
cuda.memcpy_htod(self.action_gpu, self.action)
|
||||
|
||||
def set_force_state(self, body_id: int, fx: float = 0.0, fy: float = 0.0) -> None:
|
||||
"""Set force density on a force_region object (action slot 0/1).
|
||||
|
||||
Only touches the force component slots; does not affect omega.
|
||||
"""
|
||||
self._validate_body_id(body_id)
|
||||
dim = self.cfg.dim if self.cfg else 2
|
||||
slot = 3 * dim
|
||||
base = body_id * slot
|
||||
self.action[base + 0] = np.float32(fx)
|
||||
self.action[base + 1] = np.float32(fy)
|
||||
if self.action_gpu is not None:
|
||||
cuda.memcpy_htod(self.action_gpu, self.action)
|
||||
|
||||
def _validate_body_id(self, body_id: int) -> None:
|
||||
if body_id < 0 or body_id >= self.count:
|
||||
raise IndexError(
|
||||
f"body_id {body_id} out of range for {self.count} objects")
|
||||
|
||||
def _refresh_action_from_objects(self) -> None:
|
||||
"""Populate action slots from object state using the runtime contract."""
|
||||
"""Populate action slots from object state using the runtime contract.
|
||||
|
||||
Slot layout per object (DIM=2 in this example, slots derived from 3*DIM):
|
||||
[0] = vx (= fx for force_region objects; 0 for bodies/sensors)
|
||||
[1] = vy (= fy for force_region objects; 0 for bodies/sensors)
|
||||
[5] = omega (= body rotation for circle objects; 0 for sensors/force_region)
|
||||
|
||||
Slot 0/1 are consumed by ForceRegionKernel for force_region objects.
|
||||
Slot -1 is consumed by CurvedBoundaryKernel for body rotation (action_omega).
|
||||
Sensors do not consume action slots.
|
||||
"""
|
||||
if self.count == 0:
|
||||
return
|
||||
dim = self.cfg.dim if self.cfg else 2
|
||||
@ -393,6 +430,8 @@ class ObjectManager:
|
||||
base = obj.obj_id * slot
|
||||
if base + slot > self.action.size:
|
||||
continue
|
||||
self.action[base + 0] = np.float32(getattr(obj.state, "vx", 0.0))
|
||||
self.action[base + 1] = np.float32(getattr(obj.state, "vy", 0.0))
|
||||
self.action[base + slot - 1] = np.float32(
|
||||
getattr(obj.state, "omega", 0.0))
|
||||
|
||||
|
||||
@ -76,12 +76,14 @@ class SimObject:
|
||||
center: Optional[Tuple[float, ...]] = None,
|
||||
radius: float = 0.0,
|
||||
is_sensor: bool = False,
|
||||
is_force_region: bool = False,
|
||||
) -> None:
|
||||
self.obj_id = obj_id
|
||||
self.center = center if center is not None else (0.0, 0.0)
|
||||
self.radius = radius
|
||||
self._geometry = geometry
|
||||
self._is_sensor = is_sensor
|
||||
self._is_force_region = is_force_region
|
||||
|
||||
c = self.center
|
||||
z0 = float(c[2]) if len(c) > 2 else 0.0
|
||||
@ -105,6 +107,10 @@ class SimObject:
|
||||
def is_sensor(self) -> bool:
|
||||
return self._is_sensor
|
||||
|
||||
@property
|
||||
def is_force_region(self) -> bool:
|
||||
return self._is_force_region
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Methods consumed by ObjectManager.build_compact_lists() / build_flags()
|
||||
# ------------------------------------------------------------------
|
||||
@ -112,8 +118,11 @@ class SimObject:
|
||||
"""Return (nx*ny,) uint16 array with flag bits set for this object.
|
||||
|
||||
Sensors produce ``FLUID|SENSOR_FLAG``; bodies produce
|
||||
``SOLID|OBSTACLE|BC_CURVED``.
|
||||
``SOLID|OBSTACLE|BC_CURVED``. Force-region objects produce zero
|
||||
(no flag overlay — forcing is applied via the compact-list kernel).
|
||||
"""
|
||||
if self._is_force_region:
|
||||
return np.zeros(nx * max(ny, 1) * max(nz, 1), dtype=np.uint16)
|
||||
if self._is_sensor:
|
||||
return self.geometry.build_sensor_flag_mask(nx, ny)
|
||||
return self.geometry.build_flag_mask(nx, ny)
|
||||
@ -144,9 +153,12 @@ class SimObject:
|
||||
Returns:
|
||||
cells (uint32), obj_ids (int32).
|
||||
"""
|
||||
from .coupling.soa_packer import pack_sensor_to_soa
|
||||
if self._is_force_region:
|
||||
from .coupling.soa_packer import pack_force_region_to_soa as packer
|
||||
else:
|
||||
from .coupling.soa_packer import pack_sensor_to_soa as packer
|
||||
|
||||
cells = self.geometry.build_sensor_cells(nx, ny)
|
||||
for sc in cells:
|
||||
sc.body_id = self.obj_id
|
||||
return pack_sensor_to_soa(cells)
|
||||
return packer(cells)
|
||||
|
||||
@ -256,3 +256,55 @@ class SensorSoA:
|
||||
"""Free GPU storage and reset ``count``."""
|
||||
self.free_gpu_columns()
|
||||
self.count = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForceRegionSoA:
|
||||
"""Force-region cell list with GPU mirrors for ForceRegionKernel."""
|
||||
|
||||
cells: np.ndarray = field(
|
||||
default_factory=lambda: np.zeros(0, dtype=np.uint32))
|
||||
obj_id: np.ndarray = field(
|
||||
default_factory=lambda: np.zeros(0, dtype=np.int32))
|
||||
|
||||
cells_gpu: Optional[cuda.DeviceAllocation] = None
|
||||
obj_id_gpu: Optional[cuda.DeviceAllocation] = None
|
||||
|
||||
count: int = 0
|
||||
|
||||
def assign_host(self, cells: np.ndarray, obj_id: np.ndarray) -> None:
|
||||
"""Store host force-region columns."""
|
||||
self.cells = cells
|
||||
self.obj_id = obj_id
|
||||
|
||||
def upload(self, stream: Optional[cuda.Stream] = None) -> None:
|
||||
"""Reallocate GPU columns and upload.
|
||||
|
||||
Args:
|
||||
stream: If set, use async host-to-device copies on this stream.
|
||||
"""
|
||||
self.free_gpu_columns()
|
||||
n = int(len(self.cells))
|
||||
self.count = n
|
||||
if n == 0:
|
||||
return
|
||||
|
||||
self.cells_gpu = cuda.mem_alloc(int(self.cells.nbytes))
|
||||
self.obj_id_gpu = cuda.mem_alloc(int(self.obj_id.nbytes))
|
||||
if stream is not None:
|
||||
cuda.memcpy_htod_async(self.cells_gpu, self.cells, stream)
|
||||
cuda.memcpy_htod_async(self.obj_id_gpu, self.obj_id, stream)
|
||||
else:
|
||||
cuda.memcpy_htod(self.cells_gpu, self.cells)
|
||||
cuda.memcpy_htod(self.obj_id_gpu, self.obj_id)
|
||||
|
||||
def free_gpu_columns(self) -> None:
|
||||
_free_alloc(self.cells_gpu)
|
||||
_free_alloc(self.obj_id_gpu)
|
||||
self.cells_gpu = None
|
||||
self.obj_id_gpu = None
|
||||
|
||||
def free(self) -> None:
|
||||
"""Free GPU storage and reset ``count``."""
|
||||
self.free_gpu_columns()
|
||||
self.count = 0
|
||||
|
||||
@ -21,7 +21,7 @@ import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
|
||||
from ..config import LBMConfig
|
||||
from .curved_links import CurvedLinkSoA, SensorSoA
|
||||
from .curved_links import CurvedLinkSoA, SensorSoA, ForceRegionSoA
|
||||
from .descriptors import FLUID, SOLID, BC_WALL, BC_INLET, BC_OUTLET
|
||||
|
||||
_SUPPORTED_STORE_PRECISIONS = ("FP32", "FP16S")
|
||||
@ -60,6 +60,7 @@ class LBMField:
|
||||
# Compact lists (filled by ObjectManager.sync_to_gpu)
|
||||
self.curved = CurvedLinkSoA()
|
||||
self.sensors = SensorSoA()
|
||||
self.force_regions = ForceRegionSoA()
|
||||
|
||||
# GPU allocations – sized by storage precision
|
||||
_ddf_bytes = self.n * self.nq * self.store_bytes
|
||||
@ -138,6 +139,10 @@ class LBMField:
|
||||
def n_sensor(self) -> int:
|
||||
return self.sensors.count
|
||||
|
||||
@property
|
||||
def n_force_region(self) -> int:
|
||||
return self.force_regions.count
|
||||
|
||||
# -- DDF download with precision decode ----------------------------------
|
||||
def download_ddf(self, *, step_id: int | None = None, force: bool = False):
|
||||
"""Copy DDF from GPU to host self.ddf (float32), decoding FP16S if needed.
|
||||
@ -243,9 +248,10 @@ class LBMField:
|
||||
cuda.memcpy_htod(self.flag_gpu, self.flag)
|
||||
|
||||
def upload_compact_lists(self, stream: Optional[cuda.Stream] = None) -> None:
|
||||
"""Upload cut-link and sensor compact lists to GPU."""
|
||||
"""Upload compact lists (curved, sensor, force_region) to GPU."""
|
||||
self.curved.upload(stream=stream)
|
||||
self.sensors.upload(stream=stream)
|
||||
self.force_regions.upload(stream=stream)
|
||||
|
||||
# -- Read lattice descriptors from CUDA module ---------------------------
|
||||
def _read_lattice_vectors(self):
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
#define NT 256
|
||||
#define MULT_GPU 0
|
||||
|
||||
#define NX 384
|
||||
#define NY 192
|
||||
#define NX 100
|
||||
#define NY 80
|
||||
#define NZ 1
|
||||
|
||||
// ---- Lattice model (single source of truth) ----
|
||||
|
||||
@ -3,6 +3,6 @@
|
||||
#ifndef CELERIS_CONFIG_OBJECTS_H
|
||||
#define CELERIS_CONFIG_OBJECTS_H
|
||||
|
||||
#define N_OBJS 2
|
||||
#define N_OBJS 1
|
||||
|
||||
#endif
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#define CELERIS_CONFIG_PHYSICS_H
|
||||
|
||||
#define LBtype float
|
||||
#define VIS 0.0035000000
|
||||
#define VIS 0.0100000000
|
||||
#define RHO 1.0
|
||||
#define U0 0.03
|
||||
|
||||
|
||||
@ -98,4 +98,93 @@ __global__ void SensorKernel(
|
||||
(void)flag;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ForceRegionKernel — operator-split local Guo forcing correction
|
||||
//
|
||||
// Runs after the main collide+stream step. Applies a Guo forcing term to
|
||||
// cells within a force_region footprint. This is an operator-split correction,
|
||||
// not inline in OneStep's collide_dispatch.
|
||||
//
|
||||
// The Guo formula and c_tau = 1 - omega/2 conventions match collide_srt,
|
||||
// collide_mrt, and collide_trt exactly.
|
||||
// ---------------------------------------------------------------------------
|
||||
__device__ __forceinline__ float action_force_x(const float* action, int body_id)
|
||||
{
|
||||
if (action == nullptr || body_id < 0) return 0.0f;
|
||||
unsigned int base = (unsigned int)body_id * (unsigned int)OBS_BODY_SLOT_FLOATS;
|
||||
return action[base + 0u];
|
||||
}
|
||||
|
||||
__device__ __forceinline__ float action_force_y(const float* action, int body_id)
|
||||
{
|
||||
if (action == nullptr || body_id < 0) return 0.0f;
|
||||
unsigned int base = (unsigned int)body_id * (unsigned int)OBS_BODY_SLOT_FLOATS;
|
||||
return action[base + 1u];
|
||||
}
|
||||
|
||||
__global__ void ForceRegionKernel(
|
||||
fpxx* fi,
|
||||
const unsigned short* flag,
|
||||
const unsigned int* fr_cells,
|
||||
const int* fr_obj_id,
|
||||
const float* action,
|
||||
unsigned int n_cells)
|
||||
{
|
||||
unsigned int tid = threadIdx.x + blockIdx.x * blockDim.x;
|
||||
if (tid >= n_cells) return;
|
||||
|
||||
unsigned long k = (unsigned long)fr_cells[tid];
|
||||
int bid = fr_obj_id[tid];
|
||||
float fx = action_force_x(action, bid);
|
||||
float fy = action_force_y(action, bid);
|
||||
if (fx == 0.0f && fy == 0.0f) return;
|
||||
|
||||
// Only apply force to fluid cells — skip obstacles, walls, boundaries
|
||||
if (!is_fluid(flag[k])) return;
|
||||
|
||||
float f[NQ];
|
||||
#pragma unroll
|
||||
for (int i = 0; i < NQ; i++)
|
||||
f[i] = load_ddf(fi, index_f(k, (unsigned int)i));
|
||||
|
||||
#if DIM == 2
|
||||
float rho_n, ux, uy;
|
||||
compute_rho_u(f, rho_n, ux, uy);
|
||||
|
||||
// Guo velocity correction: u* = u + F / (2 * rho)
|
||||
float rho2 = 0.5f / rho_n;
|
||||
ux = fmaf(fx, rho2, ux);
|
||||
uy = fmaf(fy, rho2, uy);
|
||||
|
||||
float Fin[NQ];
|
||||
compute_guo_forcing(ux, uy, fx, fy, Fin);
|
||||
|
||||
float omega = d_params.omega;
|
||||
float c_tau = 1.0f - 0.5f * omega;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < NQ; i++)
|
||||
f[i] += c_tau * Fin[i];
|
||||
#elif DIM == 3
|
||||
float rho_n, ux, uy, uz;
|
||||
compute_rho_u(f, rho_n, ux, uy, uz);
|
||||
|
||||
float rho2 = 0.5f / rho_n;
|
||||
ux = fmaf(fx, rho2, ux);
|
||||
uy = fmaf(fy, rho2, uy);
|
||||
float Fin[NQ];
|
||||
// fz placeholder: 0.0f (z-force not plumbed through action slots yet)
|
||||
compute_guo_forcing(ux, uy, uz, fx, fy, 0.0f, Fin);
|
||||
|
||||
float omega = d_params.omega;
|
||||
float c_tau = 1.0f - 0.5f * omega;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < NQ; i++)
|
||||
f[i] += c_tau * Fin[i];
|
||||
#endif
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < NQ; i++)
|
||||
store_ddf(fi, index_f(k, (unsigned int)i), f[i]);
|
||||
}
|
||||
|
||||
#endif // CELERIS_STEP_AUX_KERNELS_CU
|
||||
|
||||
@ -30,6 +30,7 @@ class LBMStepper:
|
||||
|
||||
self.curved_fn = module.get_function("CurvedBoundaryKernel")
|
||||
self.sensor_fn = module.get_function("SensorKernel")
|
||||
self.force_region_fn = module.get_function("ForceRegionKernel")
|
||||
|
||||
tpb = cfg.threads_per_block
|
||||
self.block = (tpb, 1, 1)
|
||||
@ -86,6 +87,7 @@ class LBMStepper:
|
||||
)
|
||||
f.ddf_gpu, f.temp_gpu = f.temp_gpu, f.ddf_gpu
|
||||
|
||||
self._launch_force_region(action_gpu, **launch_kw)
|
||||
self._launch_sensor(obs_gpu, **launch_kw)
|
||||
self._step_count += 1
|
||||
if n > 0:
|
||||
@ -135,6 +137,23 @@ class LBMStepper:
|
||||
**launch_kw,
|
||||
)
|
||||
|
||||
def _launch_force_region(self, action_gpu, **launch_kw):
|
||||
f = self.field
|
||||
if f.n_force_region == 0:
|
||||
return
|
||||
tpb = self.cfg.threads_per_block
|
||||
grid_f = ((f.n_force_region + tpb - 1) // tpb, 1, 1)
|
||||
fr = f.force_regions
|
||||
assert fr.cells_gpu is not None and fr.obj_id_gpu is not None
|
||||
self.force_region_fn(
|
||||
f.ddf_gpu, f.flag_gpu,
|
||||
fr.cells_gpu, fr.obj_id_gpu,
|
||||
action_gpu,
|
||||
np.uint32(f.n_force_region),
|
||||
block=(tpb, 1, 1), grid=grid_f,
|
||||
**launch_kw,
|
||||
)
|
||||
|
||||
@property
|
||||
def step_count(self) -> int:
|
||||
return self._step_count
|
||||
|
||||
@ -88,7 +88,8 @@ class Simulation:
|
||||
"""Add a simulation body. Returns body_id.
|
||||
|
||||
Args:
|
||||
type: ``"circle"`` or ``"sensor"`` (future: ``"polygon"``, ``"mesh"``).
|
||||
type: ``"circle"``, ``"sensor"``, or ``"force_region"``
|
||||
(future: ``"polygon"``, ``"mesh"``).
|
||||
center: (x, y) center coordinates (lattice units).
|
||||
radius: body radius (lattice units).
|
||||
**kwargs: passed to the geometry constructor.
|
||||
@ -112,10 +113,17 @@ class Simulation:
|
||||
obj = SimObject(obj_id=-1, geometry=geom,
|
||||
center=center, radius=radius,
|
||||
is_sensor=True)
|
||||
elif type_lower == "force_region":
|
||||
from .body.geometry.circle import CircleGeometry
|
||||
cx, cy = float(center[0]), float(center[1])
|
||||
geom = CircleGeometry(cx, cy, radius)
|
||||
obj = SimObject(obj_id=-1, geometry=geom,
|
||||
center=center, radius=radius,
|
||||
is_force_region=True)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unknown body type '{type}'. "
|
||||
"Supported: 'circle', 'sensor'."
|
||||
"Supported: 'circle', 'sensor', 'force_region'."
|
||||
)
|
||||
return self.bodies.add(obj)
|
||||
|
||||
@ -190,6 +198,16 @@ class Simulation:
|
||||
if self.bodies.action_gpu is not None:
|
||||
cuda.memcpy_htod(self.bodies.action_gpu, self.bodies.action)
|
||||
|
||||
def set_force(self, id: int, fx: float = 0.0, fy: float = 0.0) -> None:
|
||||
"""Set force density on a force_region object (implicit GPU upload).
|
||||
|
||||
Args:
|
||||
id: body_id from add_body(type='force_region', ...).
|
||||
fx: x-component of force density (lattice units).
|
||||
fy: y-component of force density (lattice units).
|
||||
"""
|
||||
self.bodies.set_force_state(body_id=id, fx=float(fx), fy=float(fy))
|
||||
|
||||
# -- Telemetry readback --------------------------------------------------
|
||||
def read_force(self, id: int) -> np.ndarray:
|
||||
"""Download and return the force vector on body *id* (async stream, synced)."""
|
||||
@ -201,10 +219,15 @@ class Simulation:
|
||||
self._stream_obs_download()
|
||||
return self.bodies.read_torque(id)
|
||||
|
||||
def read_sensor(self, id: int) -> np.ndarray:
|
||||
"""Download and return the sensor reading for body *id* (async stream, synced)."""
|
||||
def read_sensor(self, id: int, *, normalize: bool = True) -> np.ndarray:
|
||||
"""Download and return the sensor reading for body *id* (async stream, synced).
|
||||
|
||||
Args:
|
||||
normalize: If True, return area-averaged velocity. If False,
|
||||
return the raw sum accumulated by the GPU SensorKernel.
|
||||
"""
|
||||
self._stream_obs_download()
|
||||
return self.bodies.read_sensor(id)
|
||||
return self.bodies.read_sensor(id, normalize=normalize)
|
||||
|
||||
def _stream_obs_download(self) -> None:
|
||||
"""Async obs download on internal stream, then synchronize."""
|
||||
|
||||
489
tests/audit/Force_region.md
Normal file
489
tests/audit/Force_region.md
Normal file
@ -0,0 +1,489 @@
|
||||
# Force region 最小方案设计
|
||||
|
||||
目标是在现有功能冻结的前提下,为项目增加一个**局部体力施加区域**,用于研究局部驱动与流场结构之间的关系。该功能的第一阶段只追求可用、低风险、低侵入,不追求最泛化的抽象,也不与 sensor 合并。
|
||||
|
||||
当前代码已经具备三条很明确的数据链:全场 `OneStep` 推进、稀疏 `curved` link 链、稀疏 `sensor` cell 链。`force_region` 更接近第三类:它是一个静态几何 footprint,加上运行时 action,而不是一个需要改变主域 flag 的边界对象。因此,第一阶段更适合沿用 `sensor` 风格的稀疏 cell 管理,而不是引入全场 owner map。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
| 类别 | 内容 |
|
||||
|---|---|
|
||||
| 目标 | 在圆形区域内对流体施加局部体力 |
|
||||
| 目标 | 运行时更新区域体力参数,不重新编译 |
|
||||
| 目标 | 尽量少改现有主热路径和已有稳定功能 |
|
||||
| 非目标 | 与 sensor 统一成单一抽象 |
|
||||
| 非目标 | 非圆形几何 |
|
||||
| 非目标 | 3D |
|
||||
| 非目标 | 多区域重叠叠加规则的完整设计 |
|
||||
| 非目标 | 数值上最严格的“逐格 collision 内联 forcing”实现 |
|
||||
|
||||
## 需求边界
|
||||
|
||||
第一阶段建议只支持以下能力:
|
||||
|
||||
- `force_region` 是一个圆形 footprint
|
||||
- 区域内部施加均匀体力 `fx, fy`
|
||||
- 区域几何在初始化后不变
|
||||
- 体力大小可在运行时更新
|
||||
- 只对 fluid cell 生效
|
||||
- 不要求读取区域平均速度
|
||||
- 不要求与 `sensor` 或 `body` 的接口完全统一
|
||||
|
||||
这意味着第一阶段的问题不是“建立通用 actuator 体系”,而是“在现有框架内插入一条最小局部 forcing 数据链”。
|
||||
|
||||
## 现有代码中与该功能直接相关的结构
|
||||
|
||||
| 模块 | 当前作用 | 与 force region 的关系 |
|
||||
|---|---|---|
|
||||
| `body/geometry/circle` | 生成圆形 footprint | 可复用 |
|
||||
| `body/manager` | 汇总 compact list 并上传 GPU | 适合新增 force-region list |
|
||||
| `lbm/curved_links` | 稀疏 SoA 容器 | 可参考 `SensorSoA` 形式扩展 |
|
||||
| `lbm/kernels/step/aux_kernels` | 稀疏辅助 kernel | 适合新增 `ForceRegionKernel` |
|
||||
| `lbm/stepper` | 编排主 step 与辅助 kernel | 适合插入 force-region launch |
|
||||
| `operators/helpers` | 现有全局 Guo forcing 路径 | 可复用公式与语义 |
|
||||
|
||||
需要特别注意的一点是:现有全局体力通过 `d_params.fx/fy/fz` 进入 `collide_dispatch()`。这条路径服务的是**全场统一 forcing**。局部体力如果要完全沿用同一语义,最严格的做法是让每个 cell 在线程内部读取本地 force。但这会推动设计进入全场 owner map 或 per-cell force field,而这不符合第一阶段的低侵入目标。
|
||||
|
||||
## 推荐的第一阶段总路线
|
||||
|
||||
推荐路线是:
|
||||
|
||||
- `force_region` 作为一种新对象类型存在
|
||||
- 它不修改 domain flag
|
||||
- 它只产出稀疏 cell footprint
|
||||
- 由一个独立的 `ForceRegionKernel` 在主 step 外对这些 cell 做局部 forcing 修正
|
||||
|
||||
换句话说,第一阶段采用的是:
|
||||
|
||||
- **主推进仍保持全场统一**
|
||||
- **局部 forcing 通过稀疏后处理 kernel 注入**
|
||||
|
||||
这不是最终最泛化的架构,但它与现有 `sensor` 管理方式一致,工作量低,也最不容易破坏当前稳定基线。
|
||||
|
||||
## 为什么不优先采用全场 owner map
|
||||
|
||||
全场 owner map 的优点是语义干净:`OneStep` 的每个 cell 都可直接知道自己是否属于某个区域,再把局部体力并入 collision forcing。这在数值上更直接,也更容易扩展到复杂局部 forcing。
|
||||
|
||||
但在当前阶段,这条路的代价偏高:
|
||||
|
||||
- 需要给全场新增一个 device-side 标识数组
|
||||
- 需要改主 step kernel 形参和热路径逻辑
|
||||
- 需要明确多区域覆盖规则
|
||||
- 需要同时处理 double-buffer 与 esopull 路径中的局部 forcing 接入方式
|
||||
|
||||
对当前目标来说,这会把“加一个可用功能”变成“改造主推进语义”。因此,第一阶段更合适的路线仍是稀疏 compact list。
|
||||
|
||||
## 为什么不把 force region 直接并入 sensor
|
||||
|
||||
二者的几何 footprint 确实相似,都可以表示成“圆形覆盖的一批 cell”。但功能角色不同:
|
||||
|
||||
| 类型 | 本质 | 时序位置 |
|
||||
|---|---|---|
|
||||
| `sensor` | 读出局部信息 | step 后 |
|
||||
| `force_region` | 修改 DDF 演化 | step 中或 step 后的局部修正 |
|
||||
|
||||
因此,第一阶段不建议在抽象层面合并二者。更合理的做法是:
|
||||
|
||||
- 复用几何 footprint 生成逻辑
|
||||
- 保持独立对象语义
|
||||
- 保持独立 kernel 与独立 compact list
|
||||
|
||||
这样既能少写代码,也不会把接口语义搅乱。
|
||||
|
||||
## 对象层建议
|
||||
|
||||
`force_region` 建议作为一种新的 body-side 对象类型,而不是把普通刚体、sensor、局部 forcing 区域全部塞进同一语义分支。
|
||||
|
||||
### 推荐最低要求
|
||||
|
||||
| 字段 | 作用 |
|
||||
|---|---|
|
||||
| `obj_id` | 索引 action 与 compact list |
|
||||
| `geometry` | 圆形 footprint 来源 |
|
||||
| `state` | 可沿用通用容器,但第一阶段基本不用 |
|
||||
| `control` 或 action slot | 存 `fx, fy` |
|
||||
| `type tag` | 区分普通 body、sensor、force_region |
|
||||
|
||||
### 不必在第一阶段敲死的点
|
||||
|
||||
下面这些都可以留给 coder 选择:
|
||||
|
||||
- 是加 `is_force_region` 布尔字段,还是引入更清楚的 `role` / `kind` 标签
|
||||
- `fx, fy` 放在 `state`、`control`,还是直接只存在 `action` 打包层
|
||||
- `add_body("force_region", ...)` 还是单独加 `add_force_region(...)`
|
||||
|
||||
第一阶段更重要的是把数据链接起来,而不是先做完美 API。
|
||||
|
||||
## 几何层建议
|
||||
|
||||
`body/geometry/circle` 已经有生成 sensor cells 的逻辑。这里最自然的做法不是复制一份,而是把“圆形 footprint cell 枚举”抽成一个中性函数。
|
||||
|
||||
### 推荐抽象
|
||||
|
||||
建议在 circle geometry 中形成一个中性能力,例如:
|
||||
|
||||
- `build_footprint_cells()`
|
||||
|
||||
然后:
|
||||
|
||||
- `sensor` 使用这批 cells 作为采样点
|
||||
- `force_region` 使用同一批 cells 作为施力点
|
||||
|
||||
### 第一阶段不必决定的点
|
||||
|
||||
- footprint 是否严格包含圆内格点中心
|
||||
- 是否允许边界一圈采用不同准则
|
||||
- 是否为将来的 polygon/mesh 提前抽公共父类 helper
|
||||
|
||||
只要 footprint 定义在 host 和测试中保持一致即可。
|
||||
|
||||
## flag 处理建议
|
||||
|
||||
`force_region` 第一阶段**不应修改 flag field**。
|
||||
|
||||
原因很直接:
|
||||
|
||||
- 它不是 solid
|
||||
- 它不是 boundary condition
|
||||
- 它不是 sensor flag
|
||||
- 它不参与 channel topology 构造
|
||||
|
||||
因此它应当是“有 footprint,但不 overlay flag”的对象。
|
||||
|
||||
这意味着在 `build_flags()` 过程中:
|
||||
|
||||
- 普通 body 继续覆盖 obstacle / curved 标记
|
||||
- sensor 继续覆盖 sensor 标记
|
||||
- force_region 返回零 mask,或根本不进入该分支
|
||||
|
||||
两种实现都可以,留给 coder 决定。若追求语义更清楚,倾向于根本不让它参与 flag merge。
|
||||
|
||||
## compact list 设计建议
|
||||
|
||||
第一阶段建议新增一条与 sensor 平行的数据链:
|
||||
|
||||
- `force_region cells`
|
||||
- `force_region obj_id`
|
||||
|
||||
### 数据最小集
|
||||
|
||||
| 列 | 类型 | 作用 |
|
||||
|---|---|---|
|
||||
| `cells` | `uint32` | 被施力格点索引 |
|
||||
| `obj_id` | `int32` | 从 action 中取 `fx, fy` |
|
||||
|
||||
这与 `SensorSoA` 的结构完全同型,只是消费 kernel 不同。
|
||||
|
||||
### 可选实现
|
||||
|
||||
| 方案 | 特点 |
|
||||
|---|---|
|
||||
| 单独建 `ForceRegionSoA` | 语义清楚,后续好扩展 |
|
||||
| 复用 `SensorSoA` 结构实现新字段 | 改动少,但语义稍混 |
|
||||
|
||||
第一阶段更推荐单独建 `ForceRegionSoA`。这会多一点样板代码,但可读性更好,也避免后续 `sensor` 和 `force_region` 在 field 中混淆。
|
||||
|
||||
## action contract 建议
|
||||
|
||||
当前 `action` buffer 在 2D 下每对象为 `3 * dim = 6` 个 `float`,而现阶段只真正使用了最后一个 `omega`。这给了第一阶段一个很好的低成本入口:
|
||||
|
||||
- 不改 action buffer 总长度
|
||||
- 不改编译期 layout 宏
|
||||
- 先在已有 slot 中约定 `fx, fy` 的位置
|
||||
|
||||
### 一个可行的最小约定
|
||||
|
||||
| slot | 含义 |
|
||||
|---|---|
|
||||
| `0` | `fx` |
|
||||
| `1` | `fy` |
|
||||
| `5` | `omega` |
|
||||
|
||||
其余位置保留。
|
||||
|
||||
### 这里不必现在敲死
|
||||
|
||||
这部分可以留给 coder 做最终选择,只要满足两点:
|
||||
|
||||
- `force_region` kernel 能稳定读到 `fx, fy`
|
||||
- 现有 `CurvedBoundaryKernel` 继续稳定读到 `omega`
|
||||
|
||||
若 coder 认为显式 helper 更重要,可以新增类似 `action_force_x()` / `action_force_y()` 的 device helper,而不是在 kernel 中直接写魔数偏移。
|
||||
|
||||
## kernel 路线建议
|
||||
|
||||
第一阶段最合适的是新增一个独立 kernel,例如:
|
||||
|
||||
- `ForceRegionKernel`
|
||||
|
||||
它应当像 `SensorKernel` 一样按 compact list 启动,而不是并入 `OneStep`。
|
||||
|
||||
### 建议职责
|
||||
|
||||
每个线程处理一个 force-region cell:
|
||||
|
||||
1. 读该 cell 的线性索引 `k`
|
||||
2. 读所属对象 `obj_id`
|
||||
3. 从 `action` 中读 `fx, fy`
|
||||
4. 从当前 DDF 缓冲中加载该 cell 的 `f[NQ]`
|
||||
5. 计算 `rho, ux, uy`
|
||||
6. 按现有 Guo forcing 语义构造 `Fin`
|
||||
7. 对该 cell 的分布函数做 forcing 修正
|
||||
8. 写回 DDF
|
||||
|
||||
### 需要特别控制的事
|
||||
|
||||
第一阶段最好不要在这个 kernel 内部引入过多策略分支。尤其避免一开始就支持:
|
||||
|
||||
- 多个不同 forcing scheme
|
||||
- 复杂边界跳过逻辑
|
||||
- 非均匀空间 force
|
||||
- 区域内按距离衰减的 force
|
||||
|
||||
先只做“圆内均匀体力”。
|
||||
|
||||
## ForceRegionKernel 应该写在哪个时间位置
|
||||
|
||||
现有 `stepper` 顺序大致为:
|
||||
|
||||
1. curved boundary
|
||||
2. main step
|
||||
3. sensor
|
||||
|
||||
第一阶段建议将其改为:
|
||||
|
||||
1. curved boundary
|
||||
2. main step
|
||||
3. force region
|
||||
4. sensor
|
||||
|
||||
### 这样做的原因
|
||||
|
||||
- 主推进链尽量不动
|
||||
- force region 作为局部修正子步存在
|
||||
- sensor 若未来同时存在,可以读到施力后的局部结果
|
||||
|
||||
### 这里保留给 coder 的决策点
|
||||
|
||||
`ForceRegionKernel` 也可以放在 sensor 前后不同位置,但需要统一解释语义:
|
||||
|
||||
| 放置位置 | 语义 |
|
||||
|---|---|
|
||||
| 主 step 之后、sensor 之前 | sensor 读到施力后的状态 |
|
||||
| sensor 之后 | sensor 读到施力前主步结果 |
|
||||
|
||||
若第一阶段不做 sensor 联动,则这只是将来兼容性的选择。默认更建议放在 sensor 之前。
|
||||
|
||||
## DDF 更新方式的设计空间
|
||||
|
||||
这里是实现中最需要留白的部分。第一阶段文档不应把写法锁死,但应给出边界。
|
||||
|
||||
### 必须满足的原则
|
||||
|
||||
- 局部体力的数值语义尽量对齐现有 `forcing_guo.cuh`
|
||||
- 不应在 `ForceRegionKernel` 中引入与现有 collision family 矛盾的私有公式
|
||||
- 若采用 operator splitting,应在注释中明确说明它不是“主 collision 内联 forcing”
|
||||
|
||||
### 可接受的实现方向
|
||||
|
||||
| 方向 | 特点 |
|
||||
|---|---|
|
||||
| 直接按 Guo forcing 增量对 `f[i]` 做修正 | 最小、直观 |
|
||||
| 抽一个独立 helper,复用 `compute_guo_forcing()` | 与现有 forcing 公式最一致 |
|
||||
| 在 kernel 中根据 collision family 调整 prefactor | 更接近现有 `collide_*` 写法,但代码稍重 |
|
||||
|
||||
### 第一阶段更适合的判断
|
||||
|
||||
更推荐:
|
||||
|
||||
- 尽量复用 `compute_guo_forcing()`
|
||||
- 用很薄的一层 helper 完成 DDF 增量写回
|
||||
|
||||
但是否把 `c_tau = 1 - omega/2` 放在 kernel 里、helper 里,还是借新函数收口,可以留给 coder。
|
||||
|
||||
## 数值语义说明
|
||||
|
||||
第一阶段方案本质上是一个**局部 forcing 修正子步**,而不是严格在 `OneStep` 内逐格并入 collision forcing。这一点需要在实现处用注释写清楚。
|
||||
|
||||
### 这个选择的代价
|
||||
|
||||
- 语义上更像 operator splitting
|
||||
- 与全局 forcing 在热路径中的时序并不完全相同
|
||||
- 在强 forcing 情况下,可能与将来更严格实现有差异
|
||||
|
||||
### 这个选择的收益
|
||||
|
||||
- 主 kernel 改动极小
|
||||
- 不需要全场 owner map
|
||||
- 易于复用现有稀疏对象链路
|
||||
- 第一阶段很快能得到可用结果
|
||||
|
||||
对当前“先追求使用”的目标,这个 tradeoff 是合理的。
|
||||
|
||||
## 过滤规则建议
|
||||
|
||||
第一阶段应当保持规则简单。
|
||||
|
||||
### 建议规则
|
||||
|
||||
- 只对 `is_fluid(fl)` 的格点施力
|
||||
- 对 obstacle / wall / inlet / outlet 一律跳过
|
||||
- 与 curved body footprint 重叠时,由 cell flag 决定是否生效,而不是 force-region 自己猜
|
||||
|
||||
### 这里不必先完善的点
|
||||
|
||||
- actuator 与 actuator 的重叠叠加规则
|
||||
- 对 inlet/outlet 邻近区域的特殊处理
|
||||
- 对 sensor / force_region 同 id 绑定的逻辑
|
||||
|
||||
如果第一阶段允许多个 force region 存在,最简单的策略是先约束:
|
||||
|
||||
- footprint 不应重叠
|
||||
|
||||
或者:
|
||||
|
||||
- 重叠行为未定义,由上层调用者避免
|
||||
|
||||
## 配置与 API 建议
|
||||
|
||||
这里也不需要一次性定死。
|
||||
|
||||
### 最小外部接口至少应支持
|
||||
|
||||
- 新建一个圆形 `force_region`
|
||||
- 运行时更新它的 `fx, fy`
|
||||
|
||||
### 可选 API 形式
|
||||
|
||||
| 形式 | 特点 |
|
||||
|---|---|
|
||||
| `add_body("force_region", ...)` | 改动少,沿用现有入口 |
|
||||
| `add_force_region(...)` | 语义更清楚 |
|
||||
|
||||
### 运行时控制也有两种自然路线
|
||||
|
||||
| 形式 | 特点 |
|
||||
|---|---|
|
||||
| 扩展 `set_body(id, fx=..., fy=...)` | 改动小 |
|
||||
| 新增 `set_force_region(id, fx=..., fy=...)` | 语义清楚 |
|
||||
|
||||
第一阶段若追求少改代码,可优先沿用现有 `set_body()`。若 coder 认为 API 语义更重要,也可以新增专门入口。
|
||||
|
||||
## 对 `ObjectManager` 的建议
|
||||
|
||||
`ObjectManager` 已经偏大,因此第一阶段不建议再把很多新判断揉成一团。更合适的方式是:
|
||||
|
||||
- 在现有结构上增加一条最薄的新分支
|
||||
- 尽量与 `sensor` 的组织方式平行
|
||||
- 不在 `build_flags()` 中塞 force-region 的特例
|
||||
|
||||
### 在实现层面建议保持的分工
|
||||
|
||||
| 职责 | 位置 |
|
||||
|---|---|
|
||||
| 圆形 footprint 枚举 | `geometry` |
|
||||
| force-region SoA 打包 | `body/coupling` 或与其平行的位置 |
|
||||
| GPU buffer 生命周期 | `field` / SoA 容器 |
|
||||
| kernel 调度 | `stepper` |
|
||||
|
||||
这能避免 `manager` 再次回到“万能管理器”的方向。
|
||||
|
||||
## 对测试的建议
|
||||
|
||||
第一阶段至少需要三类测试。
|
||||
|
||||
### 1. 结构测试
|
||||
|
||||
- `force_region` 不改变 flag field
|
||||
- compact list cell 数与 footprint 预期一致
|
||||
- action 更新后 GPU buffer 内容可追踪
|
||||
|
||||
### 2. 数值冒烟测试
|
||||
|
||||
- 静止流场中施加恒定 `fx` 后,区域内流体沿 x 方向加速
|
||||
- `fx > 0` 与 `fx < 0` 响应方向相反
|
||||
- `fy` 路径同理
|
||||
|
||||
### 3. 回归保护
|
||||
|
||||
- 未添加 `force_region` 时,结果与当前版本一致
|
||||
- 添加 `force_region` 但 `fx=fy=0` 时,结果与当前版本一致或只存在舍入级差异
|
||||
|
||||
### 暂不要求的测试
|
||||
|
||||
- 与解析解严格比对
|
||||
- 多 region 重叠叠加
|
||||
- 强 forcing 的稳定性边界扫描
|
||||
|
||||
## 第一阶段完成后的理想状态
|
||||
|
||||
完成后,项目应具备以下能力:
|
||||
|
||||
- 能创建一个圆形局部施力区域
|
||||
- 能在运行时改变其 `fx, fy`
|
||||
- 不需要重新编译
|
||||
- 不需要引入全场 owner map
|
||||
- 不破坏现有 curved boundary 与 sensor 链路
|
||||
- 不迫使 `body` 与 `lbm` 做新的深度耦合
|
||||
|
||||
从架构角度看,第一阶段的关键成果不是“得到最完美的 actuator 抽象”,而是验证:
|
||||
|
||||
- 稀疏 cell list 路线足以支撑局部 forcing 研究
|
||||
- 现有系统能容纳一条新的 compact-list kernel 链路
|
||||
|
||||
## 第二阶段可能的演进方向
|
||||
|
||||
这些不属于当前实现范围,但第一阶段设计应避免把它们堵死:
|
||||
|
||||
- 读取 force-region 内平均速度或其他观测量
|
||||
- 将 `force_region` 与 `sensor` 的 footprint builder 进一步共用
|
||||
- 升级为更一般的 `region operator`
|
||||
- 改为在 `OneStep` 内逐格读取局部 force
|
||||
- 支持非圆形几何与 3D
|
||||
|
||||
第一阶段不需要为这些能力写完结构,只需要不要在命名和数据布局上把后路堵死。
|
||||
|
||||
## 建议的实现顺序
|
||||
|
||||
### 阶段 1
|
||||
|
||||
建立最小对象与 footprint 数据链。
|
||||
|
||||
- 新增 `force_region` 类型
|
||||
- 让 circle geometry 能产出中性 footprint cells
|
||||
- 新增 force-region SoA 与 GPU upload
|
||||
|
||||
### 阶段 2
|
||||
|
||||
接入运行时 action。
|
||||
|
||||
- 在 action slot 中为 `fx, fy` 留位置
|
||||
- 增加 host 侧更新接口
|
||||
- 保证不影响现有 `omega` 读取
|
||||
|
||||
### 阶段 3
|
||||
|
||||
新增局部 forcing kernel 并接入调度。
|
||||
|
||||
- 写 `ForceRegionKernel`
|
||||
- 接入 `stepper`
|
||||
- 明确与 sensor 的先后顺序
|
||||
|
||||
### 阶段 4
|
||||
|
||||
补最小测试与文档注释。
|
||||
|
||||
- 结构测试
|
||||
- 冒烟测试
|
||||
- 未启用时的回归保护
|
||||
|
||||
## 最终判断
|
||||
|
||||
第一阶段最稳妥的最小方案不是全场 owner map,也不是把 force-region 并进 sensor,而是:
|
||||
|
||||
- 把它当作一种新的稀疏 cell-list 对象
|
||||
- 复用圆形 footprint 生成逻辑
|
||||
- 通过独立 `ForceRegionKernel` 对局部 DDF 做 forcing 修正
|
||||
- 在接口和数据布局上只做最低必要改动
|
||||
|
||||
这条路线与现有代码风格最一致,也为后续更严格或更泛化的实现保留了余地。
|
||||
398
tests/audit/Force_region_validation.md
Normal file
398
tests/audit/Force_region_validation.md
Normal file
@ -0,0 +1,398 @@
|
||||
# Force region 验证清单
|
||||
|
||||
这份清单面向当前已经实现的最小版 `force_region`:
|
||||
|
||||
- 2D
|
||||
- 圆形 footprint
|
||||
- 运行时设置均匀 `fx, fy`
|
||||
- operator-split 的局部 Guo forcing 修正
|
||||
- 不与 sensor 绑定
|
||||
|
||||
当前最合理的验证思路不是一次追求“单一终极 benchmark”,而是分三层推进:
|
||||
|
||||
1. **代码回归层**:先确认新功能没有破坏旧链路
|
||||
2. **物理正确性层**:先做最稳的流体响应验证
|
||||
3. **研究使用层**:再进入圆柱尾迹中的局部强迫实验
|
||||
|
||||
文献上,LBM forcing 的核心依据仍是 Guo forcing 的离散处理 [Guo02d]。对 body-force 驱动的通道流,Poiseuille 流是最经典的基准之一,[Zou95b] 给出了平板间 Poiseuille 流的解析结果,[Guo02d] 也把 steady Poiseuille flow 作为 forcing 验证的一部分。对**空间变化** forcing 的风险与误差来源,[Sil20b] 给出了很直接的提醒:非均匀 body force 会把离散误差暴露得更明显,因此验证必须包含**网格、黏度、幅值**三个维度的稳健性检查。
|
||||
|
||||
## 一句话总策略
|
||||
|
||||
先不要一上来把 `force_region` 扔进圆柱尾迹做复杂控制。先用最简单、最容易解释的流场把它跑顺:
|
||||
|
||||
- 零强迫回归
|
||||
- 单方向局部加速
|
||||
- 长通道近似 Poiseuille
|
||||
- 再进圆柱尾迹
|
||||
|
||||
## 第一层
|
||||
|
||||
### 必做回归检查
|
||||
|
||||
这些检查不依赖文献,但必须先过。
|
||||
|
||||
| 编号 | 目的 | 设置 | 预期结果 | 是否阻塞 |
|
||||
|---|---|---|---|---|
|
||||
| R1 | 新功能不引入假障碍 | 只加 `force_region`,不加圆柱 | `n_curved == 0`,flag 场不出现 obstacle / curved 覆盖 | 是 |
|
||||
| R2 | 零强迫不改变解 | 加 `force_region`,但 `fx=fy=0` | 与无 `force_region` 基线一致,或只剩舍入级差异 | 是 |
|
||||
| R3 | force 设置后可持续生效 | `set_force()` 后步进,再触发一次 action 刷新链路 | force 不被静默清零 | 是 |
|
||||
| R4 | 符号方向正确 | 同一算例跑 `+fx` 和 `-fx` | 主流响应方向相反,幅值同量级 | 是 |
|
||||
|
||||
R2 很关键。它回答的是:当前实现是不是“只是多了一条 dormant 数据链”,而不是“只要建了 force_region 就改了解”。
|
||||
|
||||
## 第二层
|
||||
|
||||
### 物理正确性主验证
|
||||
|
||||
这部分建议按从简单到复杂的顺序做。
|
||||
|
||||
## Case A
|
||||
|
||||
### 静止流场中的单圆局部加速
|
||||
|
||||
这是最小物理 sanity check。
|
||||
|
||||
#### 设置建议
|
||||
|
||||
- 格子:`nx = 256`, `ny = 128`
|
||||
- 碰撞:先用 `SRT`,LES 关闭
|
||||
- 流动初值:入口速度设为 0 或足够小,先得到近静止背景流
|
||||
- 不加圆柱
|
||||
- force region:
|
||||
- 圆心 `(96, 64)`
|
||||
- 半径 `r = 12`
|
||||
- 先做 `fx > 0, fy = 0`
|
||||
- 强迫幅值:从小值开始,例如 `fx = 1e-6`, `5e-6`, `1e-5`
|
||||
- 每组运行到准稳态或至少足够长,使局部响应明显
|
||||
|
||||
#### 该看什么
|
||||
|
||||
- `ux` 在 force region 附近应整体偏正
|
||||
- 区域下游应形成沿 `+x` 延伸的动量尾迹
|
||||
- `uy` 的主量级应明显小于 `ux`
|
||||
- 将 `fx` 改成负值后,整幅图应沿 x 方向近似镜像
|
||||
|
||||
#### 预期结果
|
||||
|
||||
这个 case 没有一个成熟的“标准 benchmark 数字”,但应满足最基本物理响应:
|
||||
|
||||
- 加速方向与 force 方向一致
|
||||
- 幅值随 `fx` 单调增强
|
||||
- 小 force 下解的结构应平滑,不应出现棋盘格、局部爆点或非对称破缺
|
||||
|
||||
#### 判定标准
|
||||
|
||||
| 检查项 | 通过标准 |
|
||||
|---|---|
|
||||
| 方向性 | `fx > 0` 时主响应沿 `+x`,`fx < 0` 时相反 |
|
||||
| 单调性 | 增大 `fx` 后,局部 `ux` 峰值与尾迹强度单调增大 |
|
||||
| 对称性 | 在无背景横向偏置时,上下半区统计应近似对称 |
|
||||
| 稳定性 | 小幅强迫下不出现数值振荡或孤立噪点 |
|
||||
|
||||
这个 case 的作用不是给出论文级定量结论,而是先证明:**局部 forcing 至少在最简单流场中按直觉工作。**
|
||||
|
||||
## Case B
|
||||
|
||||
### 长通道中的近 Poiseuille 验证
|
||||
|
||||
这是最重要的文献支持主验证。
|
||||
|
||||
Poiseuille 流是 LBM forcing 的经典基准,[Guo02d] 用 steady Poiseuille flow 验证 forcing 处理,[Zou95b] 给出了平板间 Poiseuille 流的解析形式。对当前代码,最现实的做法不是强行构造完全理想的周期通道,而是在**长通道中用超大圆形 force region 近似全域恒定体力驱动**,只在远离入口和出口的位置取剖面比较。
|
||||
|
||||
#### 建议设置
|
||||
|
||||
- 格子:`nx = 1024`, `ny = 128`
|
||||
- 碰撞:先用 `SRT`;通过后再试 `TRT` / `MRT`
|
||||
- LES:关闭
|
||||
- 圆柱:不加
|
||||
- 入口速度:尽量设小,避免 inlet 自身主导流场
|
||||
- force region:
|
||||
- 圆心放在通道中段,例如 `(512, 64)`
|
||||
- 半径取足够大,使绝大多数通道内部 fluid cell 都被覆盖,例如 `r = 600`
|
||||
- 强迫方向:`fx > 0, fy = 0`
|
||||
- 强迫幅值:先从小值开始,避免压缩性误差,例如 `1e-6` 量级
|
||||
|
||||
#### 为什么这样设
|
||||
|
||||
当前 `force_region` 只有圆形 footprint,没有全域矩形 forcing,也没有 x 周期边界。因此最稳妥的近似是:
|
||||
|
||||
- 用超大圆把中部通道几乎全覆盖
|
||||
- 只在远离入口和出口的位置取横截面
|
||||
- 检查剖面是否接近抛物线
|
||||
|
||||
#### 该比对什么
|
||||
|
||||
对 fully developed Poiseuille,经典剖面是抛物线 [Zou95b]:
|
||||
|
||||
\[
|
||||
u(y) = u_{max}\left(1 - \frac{y^2}{L^2}\right)
|
||||
\]
|
||||
|
||||
其中中心线速度最大,壁面速度为 0。对当前代码,最重要的不是把解析常数拟合到最后一位,而是确认以下结构性特征:
|
||||
|
||||
- 中部横截面是单峰抛物形
|
||||
- 中心线最大
|
||||
- 贴壁速度接近 0
|
||||
- `uy` 远小于 `ux`
|
||||
- 沿 x 方向进入发展段后,剖面变化减小
|
||||
|
||||
#### 推荐取样方式
|
||||
|
||||
在以下几个 x 位置取横截面:
|
||||
|
||||
- `x = 256`
|
||||
- `x = 512`
|
||||
- `x = 768`
|
||||
|
||||
只要中后段两条剖面几乎重合,就说明流场已经接近 fully developed,可拿来与抛物线拟合。
|
||||
|
||||
#### 预期结果
|
||||
|
||||
- 中段与后段 `ux(y)` 应近似抛物线 [Guo02d, Zou95b]
|
||||
- 若 `fx` 增大,剖面整体抬高,但形状仍保持单峰抛物型
|
||||
- 中段以后,剖面随 x 的变化应减弱
|
||||
|
||||
#### 如何定量判定
|
||||
|
||||
建议做三个量:
|
||||
|
||||
| 指标 | 解释 | 通过建议 |
|
||||
|---|---|---|
|
||||
| 抛物线拟合残差 | 用最小二乘拟合 `u(y)=a-b(y-yc)^2` | 中后段残差明显小于入口附近 |
|
||||
| 发展段一致性 | 比较两个下游截面 | 两条曲线基本重合 |
|
||||
| 横向速度污染 | `max |uy| / max |ux|` | 远小于 1 |
|
||||
|
||||
#### 关于误差的解释
|
||||
|
||||
不要把这个 case 解释得过头。因为当前代码的边界条件并不是“理想周期 x + 纯 body-force 驱动”,所以更合理的表述是:
|
||||
|
||||
- 这是一个**近 Poiseuille** 验证
|
||||
- 它主要验证 `force_region` 是否能在长通道里生成正确的 fully-developed 流动结构
|
||||
- 它不是对解析解逐点 machine-accuracy 的测试
|
||||
|
||||
另外,[Zou95b] 指出 bounce-back 边界会引入一阶壁面误差。因此如果壁面附近有明显偏差,不必立刻把它归咎于 `force_region` 本身;重点看的是中部剖面形状与下游收敛行为 [Zou95b]。
|
||||
|
||||
## Case C
|
||||
|
||||
### 幅值扫描
|
||||
|
||||
这个 case 用来回答:当前 operator-split forcing 在多大范围内是“线性可解释”的。
|
||||
|
||||
#### 设置建议
|
||||
|
||||
复用 Case B,只扫描 `fx`:
|
||||
|
||||
- `1e-6`
|
||||
- `2e-6`
|
||||
- `5e-6`
|
||||
- `1e-5`
|
||||
- `2e-5`
|
||||
|
||||
#### 预期结果
|
||||
|
||||
在小 force 区间内:
|
||||
|
||||
- 中心线速度大致随 `fx` 单调增加
|
||||
- 抛物剖面形状保持不变,仅幅值放大
|
||||
- 若开始出现明显非对称、密度波动或剖面畸变,说明已进入“不再适合作为线性基准”的区间
|
||||
|
||||
#### 该记录的量
|
||||
|
||||
- 中心线最大速度
|
||||
- 下游某截面的体积流量
|
||||
- `rho` 偏离 1 的最大值
|
||||
|
||||
这一步的目的不是证明某个严格比例律,而是找到一个**数值上舒服的工作区间**。后面做圆柱尾迹控制时,应优先选这个区间内的 force 幅值。
|
||||
|
||||
## Case D
|
||||
|
||||
### 网格与黏度稳健性
|
||||
|
||||
这是空间变化 forcing 必须做的检查,[Sil20b] 对这一点尤其值得参考。该文的核心提醒不是“所有非均匀 forcing 都会错”,而是:**空间变化 body force 更容易把离散误差暴露出来**,因此需要明确看网格和黏度敏感性 [Sil20b]。
|
||||
|
||||
#### 推荐做法
|
||||
|
||||
选用 Case B 或 Case A 中一个已经表现稳定的设置,然后做:
|
||||
|
||||
### 网格扫描
|
||||
|
||||
- `256 x 64`
|
||||
- `512 x 128`
|
||||
- `1024 x 256`
|
||||
|
||||
保持物理几何比例一致。
|
||||
|
||||
### 黏度扫描
|
||||
|
||||
固定一个小 force 幅值,扫两到三个 `nu`。
|
||||
|
||||
#### 预期结果
|
||||
|
||||
- 网格加密后,主剖面与主流结构应收敛
|
||||
- 黏度变化会改变速度幅值和扩散尺度,但不应引入新的奇怪畸变
|
||||
- 如果某个设置对黏度特别敏感,优先检查强迫是否过强、局部 Mach 数是否过高、或 region 是否离边界太近
|
||||
|
||||
#### 如何解读
|
||||
|
||||
[Sil20b] 的重点是在 TRT 下讨论空间变化 force 的离散误差来源,并指出某些 force 展开会引入额外不一致项。当前实现采用的是一个 operator-split 的局部 Guo 修正,而不是把空间变化 force 直接并入 `OneStep` 的逐格 collision。因此这一步更应被视为:
|
||||
|
||||
- 对当前实现的**数值稳健性检查**
|
||||
- 而不是与 [Sil20b] 的离散系统逐条同构比较
|
||||
|
||||
## 第三层
|
||||
|
||||
### 研究使用前的圆柱尾迹验证
|
||||
|
||||
在前两层都通过后,再进入最关心的 case:圆柱尾迹中的局部 forcing。
|
||||
|
||||
这里最重要的不是先追一个“标准答案数值”,而是先建立**可解释的响应地图**。
|
||||
|
||||
## Case E
|
||||
|
||||
### 圆柱后方局部推流
|
||||
|
||||
可用你已经验证过的固定圆柱 baseline,先选最稳的低 Re 工况,例如二维层流涡街基线。
|
||||
|
||||
#### 建议设置
|
||||
|
||||
- 使用你当前已经验证可靠的 2D 固定圆柱基线
|
||||
- 先选一个自然涡街清晰、但还不太剧烈的 Re
|
||||
- force region 放在圆柱后方近尾迹区
|
||||
|
||||
例如:
|
||||
|
||||
- 圆柱中心 `(xc, yc)`
|
||||
- force region 中心可先试:
|
||||
- `(xc + 2D, yc)`
|
||||
- `(xc + 3D, yc)`
|
||||
- 半径先从 `0.3D` 到 `0.8D` 扫
|
||||
- 先做 `fx > 0, fy = 0`
|
||||
|
||||
#### 该看什么
|
||||
|
||||
- 尾迹中心线速度是否回升
|
||||
- 近尾迹回流区是否缩短
|
||||
- `Cl` 振幅是否降低
|
||||
- `Cd_mean` 是否变化
|
||||
- 涡脱落频率 `St` 是否偏移
|
||||
- streakline / vorticity 图是否出现更对称或更拉长的尾迹结构
|
||||
|
||||
#### 预期结果
|
||||
|
||||
对于圆柱尾迹中的外加 forcing,不同 forcing 形式都说明了同一个大方向:**局部强迫会改写尾迹结构、拖曳、升力波动与脱落频率**。[Kim05b] 虽然研究的是壁面 blowing/suction 而不是体力区,但其结果清楚表明:分布式 forcing 可以显著抑制或改变 Kármán 涡街,从而改变 mean drag 与 lift fluctuation [Kim05b]。
|
||||
|
||||
对当前 `force_region`,更合理的预期是:
|
||||
|
||||
- 小 `fx > 0`:尾迹被顺流“拉长”,回流区可能缩短
|
||||
- 足够合适的位置和幅值下:`Cl_rms` 下降,尾迹更接近对称
|
||||
- 过强 forcing:可能引入新的不稳定结构,而不是单调“变好”
|
||||
|
||||
#### 判定方式
|
||||
|
||||
建议不要只看一张图,至少同时记录:
|
||||
|
||||
| 量 | 作用 |
|
||||
|---|---|
|
||||
| `Cd_mean` | 看平均阻力变化 |
|
||||
| `Cl_rms` | 看升力波动是否被抑制 |
|
||||
| `St` | 看脱落频率是否移动 |
|
||||
| vorticity / streakline 图 | 看结构是否更对称、更拉长、或出现新模态 |
|
||||
|
||||
#### 这个 case 的定位
|
||||
|
||||
这是**研究型验证**,不是严格 benchmark。
|
||||
|
||||
更准确地说,它回答的是:
|
||||
|
||||
- `force_region` 是否足够稳定和可控,能够进入尾迹控制实验
|
||||
- 不同位置、半径、幅值是否产生可解释的流场响应
|
||||
|
||||
而不是:
|
||||
|
||||
- 某个单一数字是否必须匹配文献到几个百分点
|
||||
|
||||
## 推荐的执行顺序
|
||||
|
||||
建议按下面顺序推进。
|
||||
|
||||
| 顺序 | 算例 | 作用 |
|
||||
|---|---|---|
|
||||
| 1 | R1-R4 | 先清掉实现错误 |
|
||||
| 2 | Case A | 验证最基本物理响应 |
|
||||
| 3 | Case B | 做文献支持最强的主验证 |
|
||||
| 4 | Case C | 找到安全幅值区间 |
|
||||
| 5 | Case D | 看网格与黏度稳健性 |
|
||||
| 6 | Case E | 进入圆柱尾迹研究 |
|
||||
|
||||
## 每个算例建议保存的输出
|
||||
|
||||
为了后面快速回看,建议每个 case 固定保存下面这些量。
|
||||
|
||||
| 输出 | 是否必需 | 说明 |
|
||||
|---|---|---|
|
||||
| 配置文件快照 | 必需 | 保证能复现实验 |
|
||||
| `fx, fy` 与 region 几何 | 必需 | force center, radius, amplitude |
|
||||
| 一张 `ux` 场图 | 必需 | 看主流响应 |
|
||||
| 一张 vorticity 图 | 推荐 | 看结构变化 |
|
||||
| 一条下游横截面 `ux(y)` | 必需 | Poiseuille 与 channel 类验证最关键 |
|
||||
| `rho` 最大偏离 | 推荐 | 监控压缩性污染 |
|
||||
| 若有圆柱:`Cd_mean`, `Cl_rms`, `St` | 必需 | 尾迹控制最关键 |
|
||||
|
||||
## 最低通过线
|
||||
|
||||
如果时间紧,只要先达到下面这三条,就可以开始把 `force_region` 用进研究实验:
|
||||
|
||||
1. **零强迫回归通过**
|
||||
2. **静止流场中局部加速方向正确、幅值单调**
|
||||
3. **长通道中下游横截面能稳定收敛到近抛物剖面**
|
||||
|
||||
做到这三条,说明这个新功能至少已经:
|
||||
|
||||
- 不破坏旧功能
|
||||
- 有正确方向性
|
||||
- 能在最经典的 body-force 场景中生成合理结构
|
||||
|
||||
## 如何解读失败
|
||||
|
||||
### 若 Case A 失败
|
||||
|
||||
优先怀疑:
|
||||
|
||||
- action slot 读写
|
||||
- force region footprint
|
||||
- `is_fluid(flag[k])` 过滤
|
||||
- `ForceRegionKernel` 写回 DDF 的路径
|
||||
|
||||
### 若 Case B 失败
|
||||
|
||||
优先怀疑:
|
||||
|
||||
- force region 是否真的覆盖了足够长的通道中段
|
||||
- inlet/outlet 是否过强地主导了解
|
||||
- force 幅值是否太大
|
||||
- 取样截面是否还在入口发展段
|
||||
|
||||
### 若 Case D 失败
|
||||
|
||||
优先怀疑:
|
||||
|
||||
- spatially varying forcing 本身的离散误差被放大
|
||||
- 当前 operator-split 修正对该参数区间不够稳
|
||||
- 需要收缩到更小 force、更低 Mach、更细网格
|
||||
|
||||
## 最后的建议
|
||||
|
||||
当前 `force_region` 最适合先被当作一个**研究工具**,而不是一开始就当作“已严格 benchmark 的标准物理模块”。最稳妥的路线是:
|
||||
|
||||
- 用 [Guo02d] 和 [Zou95b] 支撑主验证思路
|
||||
- 用 [Sil20b] 提醒自己必须做稳健性检查
|
||||
- 用 [Kim05b] 作为圆柱尾迹控制的高层参考,理解应该关注哪些响应量
|
||||
|
||||
这样做,既能尽快把功能用于研究,也不会把验证叙事说得过头。
|
||||
|
||||
## 参考依据
|
||||
|
||||
- Guo, Zheng, Shi 讨论了 forcing term 的离散效应,并将 steady Poiseuille flow 作为数值验证的一部分 [Guo02d]
|
||||
- Zou, Hou, Doolen 给出了平板间 Poiseuille 流的解析解,并指出 bounce-back 壁面会引入一阶误差 [Zou95b]
|
||||
- Silva 分析了空间变化 body force 在 LBM 中的离散误差来源,说明非均匀 forcing 必须做网格与黏度稳健性检查 [Sil20b]
|
||||
- Kim 和 Choi 表明分布式 forcing 可以显著改变圆柱尾迹、drag、lift fluctuation 和 shedding mode,可作为圆柱尾迹研究阶段的高层参照 [Kim05b]
|
||||
Loading…
Reference in New Issue
Block a user