Skip to content

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. 问题陈述

仿真需要同时表达两件根本不同的事情:

  1. 全局编排:时间推进、多体并行、离散事件路由、拓扑演化、总线路由。
    • 关心"所有 RocketBody 的集合"和"全局时钟"。
    • 频率:一般为最低公倍周期(如 phys_dt = 1ms)。
  2. 单体物理:在某个 RocketBody 上跑 avionics → physics → integrate 这条 pipeline。
    • 关心"这一个 body 自己的事实"。
    • 可以安全并行,因为不同 body 间在一个 tick 内只通过 World 末尾的事件/总线路由层耦合。

把这两件事塞进同一个 Monad 是错的:

  • 单 Monad → 力学算子可以直接看到 WorldState → 不同 body 之间通过共享状态相互污染 → 不可并行。
  • 单 Monad → 力学算子可以看到全局大表 → 换大气模型要改算子签名 → 跨域耦合爆炸。

Dual-Layer RWS 用两个独立实例化的 RWS 解决这个问题:

ReaderStateLog频率实例化点
WorldRWSWorldEnvWorldStateWorldLogphys_dt(如 1ms)simulation/pipeline/WorldTick.cpp
BodyRWSBodyEnvRocketBodyBodyLogphys_dt(每个 body 一份)simulation/pipeline/BodyTick.cpp

两层通过降维算子(probe)和升维算子(merge)相连。详见 §4。


2. 类型定义

cpp
// 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⟩

角色类型内容
Readersim::WorldEnv装配后冻结的全局只读资产库:FrameConfig、Atmosphere、GravityField、WindField、所有 *Spec 表
Statesim::WorldState可变全局状态:std::vector<RocketBody> bodies + Time current_time
Logsim::WorldLogMonoid:std::vector<event_history> + std::vector<bus_traces> + body_log_aggregate

约束

  • WorldEnvruntime::Assembler::assemble() 中一次性装配后永不修改const& 引用透传到所有下游)。
  • WorldState 是 RWS 链的唯一可变态——所有 body 演化结果都要 put 回去。
  • WorldLog 是 Monoid,合并多 body 日志靠 +=

3.2 BodyRWS = ⟨BodyEnv, RocketBody, BodyLog⟩

角色类型内容
Readersim::BodyEnv降维产物TrajCtx + AeroCtx + MassPropsCtx + asset_ptr + 全局场指针
Statesim::RocketBody单个箭体的完整状态(spatial + inertial + aux + devices + bus_buffer + fcc)
Logsim::BodyLogMonoid:force_traces + bus_log + fcc_log + event_emissions

约束

  • BodyEnvprobe_* 函数从 (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)

cpp
// 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)

cpp
// 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,算子直接读 WorldStatebody 间通过共享 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 实例,与本节双层平级。

维度WorldRWSBodyRWSFccRWS
归属simulation/simulation/fcc/
ReaderWorldEnvBodyEnvFccEnv(控制器自己的配置 / nominal 气动)
StateWorldStateRocketBodyFccState(GNC mealy)
频率phys_dtphys_dtfcc_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,结果等价于串行。

条件

  1. WorldEnv 在 tick 期间不修改(const&)→ 多线程读安全。
  2. 不同 body 的 BodyEnv独立的栈对象(probe 输出,无共享指针指向可变数据,仅指向 const WorldEnv 子结构)。
  3. 不同 body 的 RocketBodyws.bodies[i] 的独立元素 → 多线程可同时写各自下标。
  4. BodyLog 各自累积,最后通过 Monoid += 升维到 WorldLog → 顺序无关(Monoid 交换性此处不必要,只需结合律)。
  5. 没有任何 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并行不安全;耦合不同 bodyBodyLog.emitted_events,World 末尾串行处理
把 FCC 状态塞进 BodyRWS StateFCC 不能独立部署(HIL 必死)FCC 自己一个 FccRWS,via FccInFrame/FccOutFrame
WorldRWS 直接跑 compute_thrust越级;力学算子吃 WorldEnv 大表先 probe → BodyEnv → 再交给 BodyRWS
dynamics_core/ 实例化 RWS&lt;BodyEnv, ...&gt;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&lt;Log&gt;::telllog_append(BodyLog*, Trace*)log 是结构体数组
for_each_body 并行嵌入式 RTOS 任务表或裸核 SMPC 实现可以是静态任务表
probe::assemble_body_envC 函数,按字段填 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),mock body_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 末尾的归属)