Dual-Layer RWS Pattern
> Status: NEW · 已对齐 PCR Master Blueprint v1.0 > 范畴: simulation/pipeline/ > 依赖: dynamics_core/ (RWS template, SpatialState/InertialState/AuxState, StageOp), contracts/ > 被依赖: runtime/Runner
1. 问题陈述
仿真需要同时表达两件根本不同的事情:
- 全局编排:时间推进、多体并行、离散事件路由、拓扑演化、总线路由。
- 关心"所有 RocketBody 的集合"和"全局时钟"。
- 频率:一般为最低公倍周期(如 phys_dt = 1ms)。
- 单体物理:在某个
RocketBody上跑 avionics → physics → integrate 这条 pipeline。- 关心"这一个 body 自己的事实"。
- 可以安全并行,因为不同 body 间在一个 tick 内只通过 World 末尾的事件/总线路由层耦合。
把这两件事塞进同一个 Monad 是错的:
- 单 Monad → 力学算子可以直接看到
WorldState→ 不同 body 之间通过共享状态相互污染 → 不可并行。 - 单 Monad → 力学算子可以看到全局大表 → 换大气模型要改算子签名 → 跨域耦合爆炸。
Dual-Layer RWS 用两个独立实例化的 RWS 解决这个问题:
| 层 | Reader | State | Log | 频率 | 实例化点 |
|---|---|---|---|---|---|
| WorldRWS | WorldEnv | WorldState | WorldLog | phys_dt(如 1ms) | simulation/pipeline/WorldTick.cpp |
| BodyRWS | BodyEnv | RocketBody | BodyLog | phys_dt(每个 body 一份) | simulation/pipeline/BodyTick.cpp |
两层通过降维算子(probe)和升维算子(merge)相连。详见 §4。
2. 类型定义
// dynamics_core/monad/RWS.h ← 普适模板,已存在
template<typename E, typename S, typename Log, typename A>
class RWS {
public:
// run: (E, S) -> (A, S', Log)
std::tuple<A, S, Log> run(const E& env, S state) const;
template<typename F> auto bind(F f) const; // >>=
template<typename B> auto then(RWS<E, S, Log, B>) const; // >>
};
// simulation/pipeline/RWSTypes.h ← v1 实例化点(跨域)
namespace sim {
using BodyRWS = monad::RWS<BodyEnv, RocketBody, BodyLog, /* A */>;
using WorldRWS = monad::RWS<WorldEnv, WorldState, WorldLog, /* A */>;
} // namespace sim关键点:
monad::RWS模板普适,在dynamics_core/monad/中定义,不依赖任何业务子域。sim::BodyRWS/sim::WorldRWS是实例化结果,因为它们绑定了跨域类型(BodyEnv引用 atmosphere/wind/gravity,RocketBody是跨域复合体)——所以必然在simulation/。- 两层共享同一个
monad::RWS模板,因此>>=/>>/pure/ask/get/put/tell操作对两层完全同形。
3. 三元组的语义
3.1 WorldRWS = ⟨WorldEnv, WorldState, WorldLog⟩
| 角色 | 类型 | 内容 |
|---|---|---|
| Reader | sim::WorldEnv | 装配后冻结的全局只读资产库:FrameConfig、Atmosphere、GravityField、WindField、所有 *Spec 表 |
| State | sim::WorldState | 可变全局状态:std::vector<RocketBody> bodies + Time current_time |
| Log | sim::WorldLog | Monoid:std::vector<event_history> + std::vector<bus_traces> + body_log_aggregate |
约束:
WorldEnv在runtime::Assembler::assemble()中一次性装配后永不修改(const&引用透传到所有下游)。WorldState是 RWS 链的唯一可变态——所有 body 演化结果都要put回去。WorldLog是 Monoid,合并多 body 日志靠+=。
3.2 BodyRWS = ⟨BodyEnv, RocketBody, BodyLog⟩
| 角色 | 类型 | 内容 |
|---|---|---|
| Reader | sim::BodyEnv | 降维产物:TrajCtx + AeroCtx + MassPropsCtx + asset_ptr + 全局场指针 |
| State | sim::RocketBody | 单个箭体的完整状态(spatial + inertial + aux + devices + bus_buffer + fcc) |
| Log | sim::BodyLog | Monoid:force_traces + bus_log + fcc_log + event_emissions |
约束:
BodyEnv由probe_*函数从(WorldEnv, RocketBody, t)一次性降维产出,整个 BodyTick 内部不再回头看 WorldEnv 大表。RocketBody是单体唯一状态——device FSM、physics integration、bus buffer 全部在这里演化。- 不同 body 的 BodyRWS 之间没有任何共享可变状态,因此
for_each_body可以安全并行。
详见 WorldEnv_Assembly.md §3 / §5 与 RocketBody_Composite.md。
4. 双层耦合:降维 + 升维
两层通过两个算子相连:
World 层 降维(probe) Body 层
─────────────────────────────────────────────────────
┌──────────────┐
WorldEnv ─────┐ │ probe_traj │
├▶│ probe_aero │─────▶ BodyEnv
RocketBody ─────┘ │ probe_mass │
└──────────────┘
│
▼
(BodyTick: avionics → physics → integrate)
│
▼
┌──────────────┐
WorldState ◀──────│ merge │◀───── (RocketBody', BodyLog)
└──────────────┘
升维4.1 降维(World → Body)
// simulation/probe/Probe.h
namespace sim::probe {
BodyEnv assemble_body_env(const WorldEnv& we, const RocketBody& body, Time t) {
return BodyEnv {
.traj = probe_traj(we, body, t),
.aero = probe_aero(we, body, t),
.mass_props = probe_mass_props(we, body, t),
.asset = &we.get_asset(body.body_id),
.atmosphere = &we.atmosphere,
// ... 其余指针 ...
};
}
} // namespace sim::probe意义:probe_* 是双层之间的接口。它把"全局只读真相"切成"该 body 当前需要知道的局部值"。
- 上游:
compute_thrust不直接读WorldEnv.atmosphere,只读BodyEnv.aero.static_pressure。 - 下游:换大气模型只动
probe_aero,pipeline 一行不改。
4.2 升维(Body → World)
// simulation/pipeline/WorldTick.cpp(核心循环片段)
for (size_t i = 0; i < ws.bodies.size(); ++i) {
BodyEnv benv = probe::assemble_body_env(we, ws.bodies[i], ws.current_time);
auto [body_out, body_new, body_log] = body_tick(dt).run(benv, ws.bodies[i]);
ws_new.bodies[i] = std::move(body_new); // 升维:写回 body
world_log += lift_body_log(body_log); // Monoid 累加
}意义:
- 每个 body 的演化结果(
RocketBody')回填到WorldState.bodies[i]。 - 每个 body 的
BodyLog通过lift_body_log(语义对齐)累加进WorldLog。 - 整个升维操作没有跨 body 干扰——这是并行合法性的根本。
详见 Body_World_Tick.md §3 / §4。
5. 为什么必须双层(不能单层)
| 候选方案 | 致命问题 |
|---|---|
| A. 单层 WorldRWS,算子直接读 WorldState | body 间通过共享 state 隐式耦合;并行不安全;力学算子能看到所有 body → 跨域爆炸 |
| B. 单层 BodyRWS,全局放在 Reader 里 | 全局变更(拓扑演化、总线路由、事件分发)需要"修改 Reader"——Reader 不可变假设破产 |
| C. 完全无 Monad,纯函数链 | 日志、状态、配置三流交织,签名爆炸;没有 Writer Monoid → 日志合并要手写每条 |
| ✓ Dual-Layer RWS | 全局编排与单体演化各自闭环;并行安全;Reader 各自不可变;Writer 各自 Monoid |
6. 与 FCC 的对称与非对称
fcc::FccRWS = monad::RWS<FccEnv, FccState, FccLog, A> 是第三个 RWS 实例,与本节双层平级。
| 维度 | WorldRWS | BodyRWS | FccRWS |
|---|---|---|---|
| 归属 | simulation/ | simulation/ | fcc/ |
| Reader | WorldEnv | BodyEnv | FccEnv(控制器自己的配置 / nominal 气动) |
| State | WorldState | RocketBody | FccState(GNC mealy) |
| 频率 | phys_dt | phys_dt | fcc_dt(如 50Hz) |
| 跨域? | 是(合法聚合) | 是(合法聚合) | 否(FCC 严格隔离) |
| 由谁调用 | Runner 主循环 | WorldTick 内嵌 | BodyTick 内嵌 + Bus 解码 |
关键非对称:FCC 的 RWS 在 fcc/ 实例化(合法,因为 FCC 不跨域);World/Body 的 RWS 在 simulation/ 实例化(合法,因为它们必然跨域)。这与 Pipeline Factory 的段①归属非对称是同一个判据(Blueprint §2.6.4)。
详见 04_FCC/Pipeline_Factory_and_Compilation.md §5。
7. 三层 Monad 的协同时序
─── Runner 主循环(phys_dt = 1ms)──────────────────────────────
│
├── (每 tick) WorldRWS.run(world_env, world_state)
│ │
│ ├── for each RocketBody body_i:
│ │ │
│ │ ├── probe(world_env, body_i, t) → BodyEnv
│ │ │
│ │ ├── BodyRWS.run(body_env, body_i)
│ │ │ │
│ │ │ ├── avionics_step (设备 FSM, IMU/GPS 量化)
│ │ │ │
│ │ │ ├── if scheduler.should_fcc_tick(t):
│ │ │ │ FccRWS.run(fcc_env, body.fcc.state) ◄── 第三层
│ │ │ │
│ │ │ ├── plant::physics::compute_* → Forces
│ │ │ │
│ │ │ └── dynamics_core::integrate_rk4 → new SpatialState
│ │ │
│ │ ├── ws.bodies[i] ← body_new
│ │ └── world_log += lift(body_log)
│ │
│ ├── collect all DiscreteEvents from bodies
│ ├── interpret_event(event) → StageOp
│ └── algebra::evolve_topology(ws.bodies, op)
│
└── 输出 (WorldOut, WorldState', WorldLog)注意:FCC 频率 < phys_dt 时,多数 tick 中不进入 FccRWS 分支(should_fcc_tick 返回 false)。详见 Body_World_Tick.md §6。
8. 并行合法性证明(非正式)
命题:在一个 World tick 内,for each body_i 可以并行执行 BodyRWS,结果等价于串行。
条件:
WorldEnv在 tick 期间不修改(const&)→ 多线程读安全。- 不同 body 的
BodyEnv是独立的栈对象(probe 输出,无共享指针指向可变数据,仅指向 const WorldEnv 子结构)。 - 不同 body 的
RocketBody是ws.bodies[i]的独立元素 → 多线程可同时写各自下标。 BodyLog各自累积,最后通过 Monoid+=升维到WorldLog→ 顺序无关(Monoid 交换性此处不必要,只需结合律)。- 没有任何 BodyTick 内部写入 WorldState —— 拓扑演化、事件路由、bus 路由全部延迟到 World tick 末尾的串行阶段。
结论:BodyRWS 并行合法。任何破坏上述五条之一的改动都会让并行失效。
> 反例(反模式):在 BodyRWS 中直接 world_state.event_history.push_back(...) —— 这违反条件 5,且引入数据竞争。正确做法:把事件放进 BodyLog.emitted_events,World tick 末尾统一收集。
9. 反模式
| 反模式 | 后果 | 正确做法 |
|---|---|---|
BodyRWS 直接读 WorldEnv.atmosphere | 算子签名跨域爆炸;换模型要改算子 | 只读 BodyEnv.aero,由 probe 降维 |
BodyRWS 直接写 WorldState | 并行不安全;耦合不同 body | 写 BodyLog.emitted_events,World 末尾串行处理 |
| 把 FCC 状态塞进 BodyRWS State | FCC 不能独立部署(HIL 必死) | FCC 自己一个 FccRWS,via FccInFrame/FccOutFrame |
WorldRWS 直接跑 compute_thrust | 越级;力学算子吃 WorldEnv 大表 | 先 probe → BodyEnv → 再交给 BodyRWS |
在 dynamics_core/ 实例化 RWS<BodyEnv, ...> | dynamics_core 必须依赖 BodyEnv → 跨域 → 普适承诺破产 | RWS 模板留在 dynamics_core;实例化在 simulation/ |
| 把 probe 放进 BodyRWS pipeline | 算子吃 WorldEnv → 又回到单层模型 | probe 在 BodyRWS 之前调用,产出 BodyEnv |
10. C-Distillation 路径
| C++ 抽象 | C 蜕化 | 备注 |
|---|---|---|
WorldRWS 模板 | world_tick(WorldEnv*, WorldState*, WorldLog*, dt) C 函数 | Reader/State/Log 三指针签名 |
BodyRWS 模板 | body_tick(const BodyEnv*, RocketBody*, BodyLog*, dt) | 同上 |
RWS::bind (>>=) | 编译期内联展开为顺序 C 调用 | 模板被压平 |
Writer<Log>::tell | log_append(BodyLog*, Trace*) | log 是结构体数组 |
for_each_body 并行 | 嵌入式 RTOS 任务表或裸核 SMP | C 实现可以是静态任务表 |
probe::assemble_body_env | C 函数,按字段填 struct | 零开销 |
详见 09_Cross_Cutting/C_Distillation.md。
11. 测试策略
11.1 单元层(dynamics_core)
monad/RWS 模板自身的 functor / monad 律(associativity / left identity / right identity)—— 与具体业务无关。
11.2 组件层(simulation)
- BodyRWS 独立测试:固定
BodyEnv、固定RocketBody,跑body_tick(dt),验证(RocketBody', BodyLog)字段。 - WorldRWS 独立测试:mock probe(返回固定
BodyEnv),mockbody_tick(返回固定输出),验证升维与拓扑演化逻辑。 - probe 独立测试:固定
WorldEnv+RocketBody,验证BodyEnv字段数值。
11.3 集成层
整循环 Runner.run(instance) 在 SIL 模式下跑 1s,验证 WorldState 在期望误差内。
详见 08_Verification/Test_Strategy.md。
12. 引用
- Blueprint §1.2(系统运行图)、§2.6(Simulation 跨域绑定层)、§2.6.1–§2.6.4
RocketBody_Composite.md(State 容器结构)WorldEnv_Assembly.md(Reader 装配与 probe 降维)Body_World_Tick.md(pipeline 实现细节)04_FCC/Free_Monad_DSL.md§7(FCC 第三层 RWS 与本节双层的协同)05_Dynamics_Core/Topology_Algebra.md(拓扑演化在 World tick 末尾的归属)