Body / World Tick — Multi-Rate Orchestration
> Status: NEW · 已对齐 PCR Master Blueprint v1.0 > 范畴: simulation/pipeline/{BodyTick, WorldTick, Scheduler, EventInterpreter}.{h,cpp} > 依赖: 全部业务子域(合法跨域聚合) > 被依赖: runtime/Runner
1. 问题陈述
WorldTick 与 BodyTick 是仿真的热循环骨架。它们负责:
- 多速率调度:phys_dt(如 1ms)与 fcc_dt(如 20ms)的协调。
- 降维-演化-升维:probe → BodyRWS → 写回 WorldState。
- 并行单体演化:N 个 RocketBody 的 BodyTick 可独立并行。
- 事件路由:BodyTick 产出的
DiscreteEvent在 World 末尾解释成StageOp。 - 拓扑演化:调用
dynamics::algebra::evolve_topology完成分级/分离。 - 总线路由:BusBuffer 的跨 body 消息通过
bus::IBus串行投递。
本节给出完整 pipeline 与每个阶段的实现要点。
2. BodyTick:单体演化 pipeline
2.1 函数签名
// simulation/pipeline/BodyTick.h
namespace sim {
// 输入:(BodyEnv, RocketBody) → 输出:(BodyOut, RocketBody', BodyLog)
BodyRWS<BodyOut> body_tick(Time dt);
struct BodyOut {
contracts::DynOutFrame dyn_out; // 给 FCC 看的物理真值(已封装)
std::vector<contracts::EngineEffect> engine_effects;
std::vector<contracts::FinDeflection> fin_deflections;
std::vector<contracts::DiscreteEvent> emitted_events;
};
} // namespace sim2.2 完整 pipeline(伪代码)
BodyRWS<BodyOut> body_tick(Time dt) {
return body_ask() >>= [dt](const BodyEnv& env) {
return body_get() >>= [dt, &env](RocketBody body) -> BodyRWS<BodyOut> {
BodyOut out;
BodyLog log;
// ─── ① Avionics 阶段(设备 FSM + IMU/GPS 量化 + ICU 计时) ───
for (auto& eng : body.engines) {
auto effect = avionics::device::ecu::step(
eng.ecu, eng.mech, body.bus, *eng.spec, env.aero, dt);
out.engine_effects.push_back(effect);
}
for (auto& sv : body.servos) {
avionics::device::scu::step(sv.scu, sv.mech, body.bus, *sv.spec, dt);
}
for (auto& fin : body.fins) {
auto defl = avionics::device::fin_ctrl::step(
fin.ctrl, fin.mech, body.bus, *fin.spec, env.aero, dt);
out.fin_deflections.push_back(defl);
}
for (auto& imu : body.imus) {
avionics::device::imu::step(
imu.state, body.aux.spec_force_b, body.aux.omega_b,
body.bus, *imu.spec, env.current_time, dt);
}
for (auto& gps : body.gpss) {
avionics::device::gps::step(
gps.state, body.spatial.pos_ecf, body.bus, *gps.spec, env.current_time, dt);
}
for (auto& icu : body.icus) {
auto events = avionics::device::icu::step(
icu.state, body.bus, *icu.spec, env.current_time, dt);
for (auto& e : events) out.emitted_events.push_back(e);
}
// ─── ② FCC tick(可选;多速率) ───
// 由外层 WorldTick 注入 fcc_should_tick 标志;详见 §6
if (body.fcc.has_value() && env.fcc_should_tick) {
// 2.1 Bus → FccInFrame
auto fcc_in = decode_bus_to_fcc_in(body.bus, env.current_time);
// 2.2 调用 FCC(自己的 RWS)
auto [fcc_out, fcc_new, fcc_log] =
body.fcc->tick(fcc_in, dt);
body.fcc->state = fcc_new;
// 2.3 FccOutFrame → Bus
encode_fcc_out_to_bus(body.bus, fcc_out, env.current_time);
log.fcc_log = fcc_log;
}
// ─── ③ Plant Physics 阶段(force computer,Forces Monoid 累加) ───
// 注:这里调用的是 simulation/pipeline/factories/make_*_body_pipeline 编译好的
// CompiledDynamics.per_stage[body.world_stage]
auto pipeline = compiled_dynamics.per_stage[body.world_stage];
auto [forces, body_after_devices, force_log] = pipeline(out.engine_effects).run(env, body);
body = body_after_devices;
log += force_log;
// ─── ④ Dynamics_core 积分阶段 ───
auto [next_spatial, next_inertial, next_aux, integ_log] =
dynamics::ode::integrate_rk4(env, body.spatial, body.inertial, forces, dt);
body.spatial = next_spatial;
body.inertial = next_inertial;
body.aux = next_aux;
log += integ_log;
// ─── ⑤ 封装 DynOutFrame(给 FCC 看的真值) ───
out.dyn_out = contracts::DynOutFrame {
.body_id = body.body_id,
.pos_ecf = env.frame.lic_to_ecf(body.spatial.pos_lic),
.vel_ecf = env.frame.lic_to_ecf_vel(body.spatial.vel_lic, body.spatial.pos_lic),
.att_q = body.spatial.q_lic_body,
.omega_b = body.aux.omega_b,
.spec_force_b = body.aux.spec_force_b,
.total_mass = body.inertial.mass,
.centroid_b = body.inertial.centroid,
};
return body_put(body) >> body_tell(log) >> body_pure(out);
};
};
}2.3 阶段顺序契约
| 阶段 | 输入 | 输出 | 副作用 |
|---|---|---|---|
| ① Avionics step | Bus 上一周期命令 + body 物理真值 | EngineEffect / FinDefl / Events + 设备 FSM 推进 | 写 body.bus |
| ② FCC tick | Bus 上 IMU/GPS payload | FccOutFrame | 读写 body.bus + body.fcc.state |
| ③ Plant Physics | EngineEffect + body 物理真值 + BodyEnv.aero/traj/mass | Forces(Monoid) | 无 |
| ④ Dynamics integrate | Forces + body.spatial/inertial | next spatial/inertial/aux | 无 |
| ⑤ DynOutFrame 封装 | body 演化后状态 | DynOutFrame | 无(纯打包) |
强契约:
- ①必须在③之前(EngineEffect 是③的输入)。
- ②与①顺序:①先 publish 物理感测到 bus,②再 poll → 同 tick 内 FCC 看到本 tick 的 IMU/GPS。
- ③④必须连续,中间不能插任何修改 RocketBody 的步骤。
- ⑤纯打包,不修改 body。
> 反例:在④之后再做 avionics step。设备命令会延迟一个 tick,造成控制时序漂移。
3. WorldTick:全局编排
3.1 函数签名
// simulation/pipeline/WorldTick.h
namespace sim {
WorldRWS<WorldOut> world_tick(Time dt);
struct WorldOut {
std::vector<contracts::DynOutFrame> per_body_out; // 每个 body 一份
std::vector<contracts::StageOp> applied_ops; // 本 tick 应用的拓扑算子
};
} // namespace sim3.2 完整 pipeline(伪代码)
WorldRWS<WorldOut> world_tick(Time dt) {
return world_ask() >>= [dt](const WorldEnv& we) {
return world_get() >>= [dt, &we](WorldState ws) -> WorldRWS<WorldOut> {
WorldOut wout;
WorldLog wlog;
// ─── A. 时间推进 ───
ws.current_time = ws.current_time + dt;
bool fcc_should_tick = scheduler.should_fcc_tick(ws.current_time);
// ─── B. 并行单体演化 ───
std::vector<BodyOut> body_outs(ws.bodies.size());
std::vector<contracts::DiscreteEvent> all_events;
#pragma omp parallel for if (allow_parallel)
for (size_t i = 0; i < ws.bodies.size(); ++i) {
BodyEnv benv = probe::assemble_body_env(we, ws.bodies[i], ws.current_time);
benv.fcc_should_tick = fcc_should_tick;
auto [bout, body_new, blog] = body_tick(dt).run(benv, ws.bodies[i]);
ws.bodies[i] = std::move(body_new);
body_outs[i] = std::move(bout);
wlog += lift_body_log(blog);
}
// 串行收集
for (size_t i = 0; i < body_outs.size(); ++i) {
wout.per_body_out.push_back(body_outs[i].dyn_out);
for (auto& e : body_outs[i].emitted_events) all_events.push_back(e);
}
// ─── C. 总线路由(跨 body 消息) ───
// 把每个 RocketBody.bus 中 dst 不在本 body 的消息 publish 到全局 IBus
// 同时从 IBus 拉外部消息进各 body.bus
for (auto& body : ws.bodies) route_bus_local_to_global(body.bus, *external_bus);
for (auto& body : ws.bodies) route_bus_global_to_local(*external_bus, body.bus);
// ─── D. 事件解释 + 拓扑演化 ───
for (const auto& event : all_events) {
contracts::StageOp op = interpret_event(event, ws);
if (std::holds_alternative<algebra::NoOp>(op)) continue;
ws.bodies = dynamics::algebra::evolve_topology(ws.bodies, op);
wout.applied_ops.push_back(op);
wlog.applied_stage_ops.push_back(op);
}
return world_put(ws) >> world_tell(wlog) >> world_pure(wout);
};
};
}3.3 阶段顺序契约
| 阶段 | 必要前提 |
|---|---|
| A. 时间推进 | 必须在 B 之前;BodyEnv.current_time 依赖此 |
| B. 并行 BodyTick | 必须在 C/D 之前;C/D 依赖 BodyTick 的输出 |
| C. 总线路由 | 必须在 B 之后;BusBuffer 在 BodyTick 中写入 |
| D. 拓扑演化 | 必须在 B 之后;事件来源于 BodyTick |
| C 与 D 同顺序:先 C 后 D | 设计选择(D 可能产生新 body,新 body 的初始 bus 应该是干净的) |
4. 升维:BodyLog → WorldLog
lift_body_log(BodyLog) → WorldLog 把单体日志按类别合并进全局日志:
WorldLog lift_body_log(const BodyLog& bl) {
return WorldLog {
.force_traces = bl.force_traces, // 直接 copy
.bus_traces = bl.bus_traces,
.fcc_traces = bl.fcc_traces,
.emitted_events = bl.emitted_events,
.applied_stage_ops = {}, // World 层自己填
};
}并合通过 WorldLog::operator+=(Monoid)完成:
WorldLog& operator+=(WorldLog& a, const WorldLog& b) {
a.force_traces.insert(a.force_traces.end(), b.force_traces.begin(), b.force_traces.end());
a.bus_traces.insert(...);
// ... 字段累加 ...
return a;
}Monoid 律:结合律((a + b) + c == a + (b + c))+ 单位元(空 log)。详见 05_Dynamics_Core/Forces_Monoid.md §3。
5. 事件解释器:DiscreteEvent → StageOp
// simulation/pipeline/EventInterpreter.h
namespace sim {
contracts::StageOp interpret_event(
const contracts::DiscreteEvent& event,
const WorldState& ws);
} // namespace sim职责:把"硬件事件"翻译成"拓扑操作"。
contracts::StageOp interpret_event(const DiscreteEvent& e, const WorldState& ws) {
using Kind = contracts::DiscreteEvent::Kind;
switch (e.kind) {
case Kind::EngineShutdown:
// 单纯关机不改拓扑
return algebra::NoOp{};
case Kind::BoltFired:
// 火工品触发 → 后续 StageSeparation 事件会来
return algebra::NoOp{};
case Kind::StageSeparation:
// 分级:从 source body 析出 new body
return algebra::SeparationOp {
.source = e.source_body,
.new_body = derive_new_body_id(e),
.detached_engines = ws.get_engines_to_detach(e.source_body, e.payload_id),
.avionics_for_new = ws.get_avionics_action(e.source_body, e.payload_id),
};
case Kind::FinDeploy:
// 栅格舵展开 → world_stage 转换
return algebra::TransitionOp {
.body = e.source_body,
.next = algebra::WorldStage::REENTRY,
};
}
return algebra::NoOp{};
}关键归属:
- 事件来源:ICU FSM(avionics 子域)
- 事件接收:DynInFrame.events(contracts/)
- 事件解释:
sim::interpret_event(simulation/,唯一允许跨域聚合层) - 事件应用:
dynamics::algebra::evolve_topology(dynamics_core/,普适代数)
> 这是 Blueprint §2.9 数据流的完整闭环。 > simulation/ 决定一个 BoltFired 事件是变成 SeparationOp 还是 NoOp——这是策略,不是普适物理。
详见 03_Avionics_and_Bus/Sensor_Modeling.md(ICU FSM)和 05_Dynamics_Core/Topology_Algebra.md(StageOp 代数)。
6. 多速率调度
6.1 Scheduler
// simulation/pipeline/Scheduler.h
namespace sim {
struct MultiRateScheduler {
Time fcc_period { Time::from_hz(50) }; // FCC 默认 50Hz
Time phys_period { Time::from_hz(1000) }; // 物理默认 1kHz
Time last_fcc_tick { Time::zero() };
bool should_fcc_tick(Time now) {
if (now - last_fcc_tick >= fcc_period) {
last_fcc_tick = now;
return true;
}
return false;
}
};
} // namespace sim6.2 时序示意
phys tick (1ms): │1│2│3│4│5│6│7│8│9│...│20│21│...│40│41│...
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │
FCC tick (50Hz): ★ ★ ★ ← 每 20 个 phys tick 一次
│ │ │
fcc_should_tick: T F F F F F F F F F...T F F...F T F...关键:
- phys_dt 一定整除 fcc_dt(否则会出现"跳过 FCC tick"或"漂移")。
- FCC 频率由 YAML 配置(
run_xxx.yaml: scheduler.fcc_hz: 50),不是硬编码。 - HIL 模式下 FCC 在另一台机器,scheduler 只控制 plant 端何时把数据打包发出。
6.3 频率匹配示例
| 用例 | phys_hz | fcc_hz | phys_dt | fcc_dt | 每多少 phys 一次 FCC |
|---|---|---|---|---|---|
| 常规 SIL | 1000 | 50 | 1ms | 20ms | 20 |
| 高频 HIL | 1000 | 100 | 1ms | 10ms | 10 |
| 高保真 phys | 4000 | 50 | 250μs | 20ms | 80 |
| 低频 sim 调试 | 100 | 25 | 10ms | 40ms | 4 |
6.4 反例
- fcc_hz = 33, phys_hz = 1000 → fcc_dt = 30.30..ms,不整除 → 漂移
- 每个 phys tick 都 should_fcc_tick 返回 true → FCC 50Hz 控制环退化成 1kHz → 增益爆炸
7. 并行化
7.1 默认串行
WorldTick 默认串行调用 BodyTick(简洁性 + 易调试)。
7.2 启用并行
// 配置
struct ScheduleConfig {
bool allow_parallel_body_tick = false;
int body_tick_thread_count = 0; // 0 = auto
};启用条件(详见 Dual_Layer_RWS.md §8):
- WorldEnv 装配后只读。
- 每个 BodyEnv 是独立栈对象。
- BodyTick 不修改 WorldState(事件、日志通过 BodyOut/BodyLog 出栈)。
- 总线路由放在 BodyTick 之外(C 阶段串行)。
7.3 内部实现
OpenMP / TBB / 手工线程池都可以。推荐 OpenMP(compile-time switch):
#pragma omp parallel for if (config.allow_parallel_body_tick) num_threads(N)
for (size_t i = 0; i < ws.bodies.size(); ++i) {
// ... BodyTick ...
}> 不要 在 BodyTick 内部再起线程(嵌套并行造成 oversubscription)。一层并行足够。
8. 完整 tick 流程图
runtime::Runner::run_loop():
while (!finished):
world_tick(phys_dt).run(world_env, world_state)
│
▼
┌──── WorldTick (simulation/) ────────────────────┐
│ A. 时间推进 │
│ ws.current_time += dt │
│ fcc_should_tick = scheduler.should_*(t) │
│ │
│ B. for each body_i (可并行): │
│ ┌── probe::assemble_body_env ──────────┐ │
│ │ TrajCtx + AeroCtx + MassPropsCtx │ │
│ │ + asset_ptr + atmosphere_ptr + ... │ │
│ └────────────┬─────────────────────────┘ │
│ ▼ │
│ ┌──── BodyTick (simulation/) ─────────┐ │
│ │ ① Avionics step (设备 FSM × N) │ │
│ │ ② FCC tick (若 should_tick) │ │
│ │ ③ Plant Physics (Forces Monoid) │ │
│ │ ④ Dynamics integrate (RK4) │ │
│ │ ⑤ DynOutFrame 封装 │ │
│ └────────────┬─────────────────────────┘ │
│ ▼ │
│ ws.bodies[i] = body_new │
│ world_log += lift(body_log) │
│ │
│ C. 总线路由 (串行) │
│ local BusBuffer ←→ global IBus │
│ │
│ D. 事件解释 + 拓扑演化 │
│ for event in all_events: │
│ op = interpret_event(event, ws) │
│ ws.bodies = evolve_topology(ws.bodies, op)│
│ │
│ return (WorldOut, ws_new, world_log) │
└──────────────────────────────────────────────────┘9. 不变量与契约
| 契约 | 强度 | 检查点 |
|---|---|---|
| BodyTick 内部不修改 WorldState | 必须 | 并行合法性根本依赖 |
| BodyTick 阶段顺序:avionics → fcc → physics → integrate | 必须 | 时序契约 |
| probe 在 BodyTick 入口调用一次 | 必须 | 各算子时间一致性 |
| WorldTick 顺序:A → B → C → D | 必须 | 拓扑事件依赖 BodyTick 输出 |
| 拓扑演化只在 World 末尾发生 | 必须 | per-stage 预编译派发的前提 |
| FCC tick 频率 整除 phys 频率 | 必须 | 调度无漂移 |
| 总线路由发生在 BodyTick 之后 | 必须 | 跨 body 消息一致性 |
| body.fcc 可为空(分离的子箭) | 设计 | BodyTick.②跳过即可 |
| BodyOut.emitted_events 收集而非直接修改 ws | 必须 | 并行 + 事件顺序确定性 |
10. 与三种部署模式的适配
| 部署 | BodyTick | WorldTick |
|---|---|---|
| avionics_dry | ①②跑,③④跳过(物理冻结) | A B(仅 avionics)C,D 通常 NoOp |
| sil_monolithic | ①②③④⑤ 全跑 | A B C D 全跑 |
| hil_dyn | ①跑,②的 FCC 调用换成 DynChannel send/recv | A B C D 全跑 |
| hil_fcc | plant 端跑 ①③④⑤;FCC 在 RTOS | plant 端 A B C D,FCC 端独立 |
> HIL 的本质是信道替换:DynChannel 替换 FCC↔Plant 数据流,IBus 替换 FCC↔Device 数据流。BodyTick / WorldTick 骨架不变(Blueprint §1.3)。
详见 07_Runtime/Assembler.md 与 07_Runtime/IDynChannel.md(待写)。
11. 反模式
| 反模式 | 后果 | 正确做法 |
|---|---|---|
BodyTick 中段调用 probe_aero("我换了高度,重算一次") | 不同算子看到不同环境;时间不一致 | probe 在入口 once,整 tick 共享 |
BodyTick 直接修改 world_state.bodies[i] | 越层 + 并行不安全 | 通过 BodyRWS State 操作 body 局部 |
BodyTick 内部直接调用 algebra::evolve_topology | 拓扑演化要求 ws.bodies 全集 | 出栈到 BodyOut.emitted_events,World 末尾统一 |
| FCC tick 在 ①之前调用 | FCC 看不到本 tick 的传感量化结果 | ② 必须在 ① 之后 |
| 跨 body 直接共享 Bus 句柄 | 与 BusBuffer 设计矛盾;并行竞争 | BusBuffer 在 RocketBody 内;跨 body 走 IBus 串行 |
| WorldTick 中重新装配 WorldEnv | 严重违背"装配冻结"契约;指针失效 | WorldEnv 在 Runner 构造时装配 once |
| 把 scheduler 放到 BodyTick 内 | 各 body 调度不一致;FCC 频率不可控 | scheduler 在 WorldTick (A 阶段) 决定 |
| 在并行 for 中读写共享 vector | 数据竞争 | 每个 thread 写 body_outs[i](独立下标),事件最后串行收集 |
直接 compute_thrust(world_env, body) | 算子跨域;签名爆炸 | 算子签名 (BodyEnv, RocketBody),由 probe 降维 |
BodyTick 内直接 ws.event_history.push_back | 数据竞争 | 写 BodyLog.emitted_events,World 升维 |
12. C-Distillation 路径
| C++ 抽象 | C 蜕化 |
|---|---|
WorldRWS<WorldOut> / BodyRWS<BodyOut> 模板 | world_tick(WorldEnv*, WorldState*, WorldLog*, WorldOut*, dt) C 函数 |
body_tick(dt).run(env, body) | body_tick_c(const BodyEnv*, RocketBody*, BodyOut*, BodyLog*, dt) |
for_each_body 并行 | RTOS 任务表或裸核 SMP |
std::vector<DiscreteEvent> | 编译期上限静态数组 + count |
interpret_event(变体匹配) | switch case + StageOp tagged union |
| MultiRateScheduler | 计数器 + 整数比较 |
lift_body_log | memcpy + count 累加 |
OpenMP #pragma omp parallel for | 任务表派发 |
详见 09_Cross_Cutting/C_Distillation.md。
13. 测试策略
13.1 单元层
interpret_event(DiscreteEvent)各 Kind 分支返回正确 StageOp。Scheduler::should_fcc_tick在 N 个 phys_dt 后恰好返回 1 次 true。
13.2 组件层
- BodyTick 单步:固定 BodyEnv + 固定 RocketBody,跑 1 步,验证 5 阶段输出。
- BodyTick 阶段顺序:用桩验证
compute_thrust在ecu::step之后调用。 - 多 body BodyTick 并行 = 串行:单测验证给定相同输入,并行/串行结果 bit-identical。
13.3 集成层
- 1s SIL:验证 1000 tick 后 spatial 在期望误差内。
- 阶段分离:mock ICU 在 t=10s 触发
StageSeparation→ 验证ws.bodies.size()从 1 变 2。 - 多速率:phys 1kHz + FCC 50Hz,1s 内 FCC tick 计数 = 50。
详见 08_Verification/Test_Strategy.md。
14. 引用
- Blueprint §1.2(完整 tick 流程图)、§2.6.4(双层 RWS 实例化与多速率)、§2.9(事件 → StageOp 数据流)、§3.2(FCC ↔ simulation 装组拆卸)
Dual_Layer_RWS.md(RWS 三元组与升维降维)RocketBody_Composite.md(BodyTick 操作的 State)WorldEnv_Assembly.md(probe 函数实现)04_FCC/Pipeline_Factory_and_Compilation.md(fcc.tick 内部为 per-stage 预编译派发)05_Dynamics_Core/Topology_Algebra.md(StageOp 与 evolve_topology)05_Dynamics_Core/Forces_Monoid.md(force computer 累加)03_Avionics_and_Bus/Semantic_Bus_Pattern.md(BusBuffer ↔ IBus 路由)03_Avionics_and_Bus/Sensor_Modeling.md(设备 step 函数实现)