CelerisLab/tests/audit/Force_region.md
Frank14f b110591433 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>
2026-06-01 18:07:55 +08:00

489 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 修正
- 在接口和数据布局上只做最低必要改动
这条路线与现有代码风格最一致,也为后续更严格或更泛化的实现保留了余地。