Topology Algebra: WorldStage / StageOp / 双代数化
> Aligned with PCR Master Blueprint v1.0 — see Blueprint §2.9(拓扑代数 algebra)、§7.3(FccStage 与 WorldStage 双代数化)、§2.6.4(跨域判据)。 > 职责:本文定义 dynamics_core/algebra/ 的拓扑代数:WorldStage 强类型枚举、StageOp sum type、evolve_topology 自由函数。同时规定与 dynamics 侧 per-WorldStage 预编译 BodyRWS pipeline 的协作方式——并明确为什么这套预编译与 FCC 侧四段式不完全对称。 > 核心约束: > 1. WorldStage 的演化只由 evolve_topology 完成;任何子域不得直接修改 RocketBody.world_stage > 2. WorldStage 与 FccStage 完全独立——FCC 决策不直接驱动 WorldStage,二者通过 DiscreteEvent + ICU 间接耦合 > 3. dynamics 的 per-WorldStage pipeline 工厂归 simulation/pipeline/factories/(跨域),不在 dynamics_core/ > C-Distillation note:WorldStage 是 enum class : uint16_t;StageOp 蒸馏为 tagged union;evolve_topology 蒸馏为 switch on tag。
1. 为什么需要拓扑代数
火箭飞行不是单一 RocketBody 的连续积分——它会经历结构突变:
- 助推器分离 → 一个 body 变两个
- 整流罩抛罩 → 改变气动模型 + 减质量
- 发动机点火 / 关机 → 改变力源拓扑
- 落地 / 坠毁 → body 进入"已落级"状态,停止积分
这些都是离散的拓扑事件。如果让积分器自己处理这些事件,会出现:
Integrator内部switch (event_kind)→ universal 承诺破灭- 状态突变在 RK4 子步中段发生 → 数值不一致
- 跨 body 协调(如分离后两个 body 各自飞)→ 责任不清
解法:把拓扑变化抽象为代数——evolve_topology(bodies, StageOp) → bodies',与连续积分严格分离。每个 tick 末尾执行一次拓扑变换;变换内部是确定性、纯函数。
2. WorldStage:强类型相位枚举
// dynamics_core/algebra/StageOp.h
namespace dynamics::algebra {
enum class WorldStage : uint16_t {
PRE_LAUNCH = 0,
COMPOSITE_FLIGHT = 1, // 整箭复合飞行
STAGE_1_SPENT = 2, // 一级耗尽(已分离)
CORE_COAST = 3, // 芯级滑行
PAYLOAD_ORBIT = 4,
// YAML 可扩展;具体取值集合由 mission 决定
};
}为什么用 uint16_t:
- 强类型防呆(不会与
FccStage互换) - 蒸馏后是 16-bit 整数,跟
enum class字节布局一致 - 数值集合可由 YAML 注入新值,不破坏 ABI
归属:dynamics_core/algebra/ —— 它是 dynamics 的代数原语,universal kernel 的一部分。
3. StageOp:拓扑变换代数
// dynamics_core/algebra/StageOp.h
namespace dynamics::algebra {
struct NoOp {};
struct TransitionOp {
contracts::BodyId body;
WorldStage next;
};
struct SeparationOp {
contracts::BodyId source; // 分离前的源 body
contracts::BodyId new_body; // 分离后新增的 body
std::vector<contracts::EngineId> detached_engines;
AvionicsAction avionics_for_new; // 新 body 的航电策略
};
struct IgnitionOp {
contracts::BodyId body;
contracts::EngineId engine;
};
using StageOp = std::variant<NoOp, TransitionOp, SeparationOp, IgnitionOp>;
enum class AvionicsAction { None, ActivateClone, Continue };
// 演化:bodies × StageOp → bodies'
std::vector<dynamics::RocketBody> evolve_topology(
const std::vector<dynamics::RocketBody>& bodies,
const StageOp& op);
}3.1 四种 StageOp 解读
| Op | 语义 | bodies 数量变化 |
|---|---|---|
NoOp | 不变 | 0 |
TransitionOp | 某个 body 的 WorldStage 变化(如 SPENT → COAST) | 0 |
SeparationOp | 从源 body 裂变出一个新 body;指定哪些 engine 跟着走 | +1 |
IgnitionOp | 指定 body 的指定 engine 点火(仅状态机标志) | 0 |
> ⚠️ 注意:IgnitionOp 不直接计算推力——它只把 EngineFsm.state 从 Idle 翻到 Ignition。实际推力由 plant/physics/Thrust 在下个 tick 根据 fsm 状态计算。
3.2 contracts::BodyId 强类型
// contracts/Ids.h
namespace contracts {
enum class BodyId : uint32_t;
enum class EngineId : uint32_t;
}强类型避免 body_id == engine_id 的误用。evolve_topology 的算法在 dynamics_core/algebra/,类型 contracts::* 来自 contracts/——dynamics_core 不依赖 plant。
3.3 不在 StageOp 里的东西
- ❌
RestartEngineOp、AbortMissionOp——这些是 mission-specific,应通过FccStageOp表达后再翻译 - ❌
UpdateAeroModelOp——气动模型由RocketBody.world_stage间接决定(同一 stage 用同一 aero model) - ❌
ChangeMassOp——质量变化是连续积分的副产物(d_mass_dt),不是离散事件
4. evolve_topology:纯函数变换
// dynamics_core/algebra/evolve_topology.cpp
std::vector<RocketBody> dynamics::algebra::evolve_topology(
const std::vector<RocketBody>& bodies,
const StageOp& op)
{
return std::visit([&bodies](const auto& concrete) -> std::vector<RocketBody> {
using T = std::decay_t<decltype(concrete)>;
if constexpr (std::is_same_v<T, NoOp>) {
return bodies;
}
else if constexpr (std::is_same_v<T, TransitionOp>) {
auto next = bodies;
for (auto& b : next) {
if (b.id == concrete.body) {
b.world_stage = concrete.next;
}
}
return next;
}
else if constexpr (std::is_same_v<T, SeparationOp>) {
auto next = bodies;
// 1. 找到 source;2. 复制为 new_body;3. 把 detached_engines 从 source 移到 new_body
// 4. 新 body 的 spatial 继承自 source(同一瞬态);之后各自积分
// ...
return next;
}
else if constexpr (std::is_same_v<T, IgnitionOp>) {
auto next = bodies;
for (auto& b : next) {
if (b.id == concrete.body) {
for (auto& eng : b.engines) {
if (eng.id == concrete.engine) {
eng.fsm.state = EngineFsm::State::Ignition;
}
}
}
}
return next;
}
}, op);
}特性:
- 纯函数:
(bodies, op) → bodies',无副作用 - 不调用积分器
- 不知道为什么这个 op 来了(来源解释在
simulation::interpret_event)
5. 与 FccStage 的双代数化(Blueprint §7.3)
| 维度 | WorldStage | FccStage |
|---|---|---|
| 归属库 | dynamics_core/algebra/ | fcc/stage/ |
| 代数 | StageOp = variant<NoOp, Transition, Separation, Ignition> | FccStageOp = variant<NoOp, Transition, ScheduleEvent, EmitDiscreteEvent> |
| 演化函数 | evolve_topology | evolve_stage |
| 关心 | 火箭物理拓扑(body 数量、engine 归属) | FCC 任务相位(用哪套调度表 / pipeline) |
| 触发源 | DiscreteEvent → interpret_event → StageOp(在 simulation/) | TaskCondition YAML 驱动(在 fcc/) |
| 触发频率 | 仅在离散事件命中(数秒到数百秒) | 每拍检查 transitions(10–50ms) |
| 共用同一代数? | ❌ 不允许 | ❌ 不允许 |
5.1 为什么必须独立
如果合并:
- FCC 决策直接决定物理拓扑 → 故障注入测试无法做("FCC 命令分离但火工品哑火"无法表达)
- 物理拓扑变化倒灌 FCC 调度 → universal kernel 知道 FCC 概念
独立后:
- FCC 失效场景:FCC 仍按
MECO+T转入 Coast 调度,但物理上分离失败 → 仿真能复现"火箭带着燃尽的助推器一起滑行"的灾难模式 - 物理拓扑灾难场景:分离后某 body 失稳,但 FCC 调度按原计划 → 复现"FCC 不知道自己已脱离主体"
5.2 间接耦合:DiscreteEvent + ICU
FCC 决策 (FccStage 转换)
↓
EmitDiscreteEvent (FccStageOp) → FccOutFrame.events.push(MECO)
↓
Bus 投递 MECO → ICU (Ignition Control Unit) 接收
↓
ICU 翻译 → EngineMech::ShutdownEngine 物理动作
↓ ↓
plant 侧执行 engine 关机 Bus 上同时投递 DiscreteEvent → DynInFrame
↓
sim::world_tick 末尾收集 events
↓
sim::interpret_event(MECO) → StageOp::TransitionOp{next=Coast}
↓
dynamics::algebra::evolve_topology(bodies, op)
↓
新 WorldStage(在 RocketBody 上)关键解耦:
- FCC 永远不调用
evolve_topology - WorldStage 转换由 simulation 解释 DiscreteEvent 决定,不由 FCC 直接决定
- FCC 不知道分离后 body 数量、不知道气动模型是否切换
- dynamics_core 不知道 MECO 是 FCC 决策还是地面指令——它只看
StageOp
详见 04_FCC/FCC_State_Machine.md §3.4 与 06_Simulation/World_Tick_Event_Routing.md(待写)。
6. Per-WorldStage 预编译 BodyRWS Pipeline
6.1 动机
每个 WorldStage 下,单体的力学组合不同:
COMPOSITE_FLIGHT:thrust + aero + gravity(全部主发动机推力,整箭气动)STAGE_1_SPENT:仅 aero + gravity(无推力,气动模型已切到 spent 配置)CORE_COAST:aero(极稀薄)+ gravityPAYLOAD_ORBIT:仅 gravity(无气动)
如果让单一 body_tick_kernel 内部 if (stage == X) 分支:
- 编译期无法 inline
- 每个 body 每 tick 都做无效分支
- 跟 FCC 侧反模式同源(详见
04_FCC/Free_Monad_DSL.md§9)
解法:与 FCC 同构,per-WorldStage 预编译 BodyRWS<std::monostate> pipeline,按 body.world_stage 做 O(1) 派发。
6.2 与 FCC 四段式的关键不对称(Blueprint §2.6.4 应用)
| 段 | FCC | Dynamics |
|---|---|---|
| ① 工厂 | fcc/pipelines/(intra-FCC,层级无知) | simulation/pipeline/factories/(跨域工厂) |
| ② 编译 | fcc/firmware/compile_fcc | simulation/pipeline/compile_dynamics |
| ③ 触发 | runtime::Assembler | runtime::Assembler |
| ④ tick | simulation::FccTick | simulation::BodyTick + WorldTick |
> 为什么 dynamics 的 ① 也在 simulation/,而不是 dynamics_core/pipelines/? > > dynamics 的 body pipeline 工厂内部要调用: > - plant::physics::compute_thrust_contribution (plant/) > - plant::physics::compute_aero_contribution (plant/) > - plant::physics::compute_gravity_contribution(plant/ + environment/) > - dynamics_core::ode::integrate_rk4 (dynamics_core/) > > 这是跨多个子域的组合。Blueprint §2.6.4 判据:跨域 → simulation/。所以 dynamics 没有"层级无知工厂"——它的整个段①从一开始就在 simulation/。 > > FCC 的差异:FCC 内部是闭环的(GNC 算法只依赖 fcc/algorithms/),所以段①可以保持 intra-FCC,层级无知地放在 fcc/pipelines/。
这个不对称是正确应用判据的结果,不是架构裂痕。
6.3 工厂示例(位于 simulation/,本文档只示意)
// simulation/pipeline/factories/composite_flight_body_pipeline.cpp
// 跨域工厂:组合 plant + dynamics_core + environment 调用
BodyPipeline sim::pipeline::factories::make_composite_flight_pipeline(
const WorldEnv& wenv)
{
return [&wenv](RocketBody body, Time t, Time dt) -> RocketBody {
// 跨域调用:plant/physics + dynamics_core
auto forces = plant::physics::compute_thrust_contribution(/*...*/)
+ plant::physics::compute_aero_contribution(/*...*/)
+ plant::physics::compute_gravity_contribution(/*...*/);
auto y = dynamics::pack_state(body.spatial, body.inertial);
auto y_next = dynamics::ode::integrate_rk4(y, forces, body.inertial.inertia, dt.sec());
dynamics::unpack_state(y_next, body.spatial, body.inertial);
body.aux = dynamics::compute_aux_instant(y, forces);
return body;
};
}
// simulation/pipeline/factories/coast_body_pipeline.cpp
BodyPipeline sim::pipeline::factories::make_coast_pipeline(const WorldEnv& wenv) {
return [&wenv](RocketBody body, Time t, Time dt) -> RocketBody {
// 无推力;只算 aero + gravity
auto forces = plant::physics::compute_aero_contribution(/*...*/)
+ plant::physics::compute_gravity_contribution(/*...*/);
// ... integrate ...
return body;
};
}6.4 CompiledDynamics
// simulation/pipeline/CompiledDynamics.h
namespace sim::pipeline {
constexpr size_t kWorldStageCount = static_cast<size_t>(WorldStage::_Count);
using BodyPipeline = std::function<RocketBody(RocketBody, Time, Time)>;
struct CompiledDynamics {
std::array<BodyPipeline, kWorldStageCount> per_stage;
};
CompiledDynamics compile_dynamics(const WorldEnv& wenv);
}compile_dynamics 启动期一次性构造 per_stage 表,由 runtime::Assembler 调用。详见 06_Simulation/Body_World_Tick.md(待写)。
6.5 派发:BodyTick 内部 O(1)
// simulation/pipeline/BodyTick.cpp
WorldRWS<std::monostate> body_tick(const CompiledDynamics& cd,
contracts::BodyId body_id,
Time t, Time dt) {
return world_get() >>= [&cd, body_id, t, dt](const WorldState& w) {
auto& body = find_body(w, body_id);
const BodyPipeline& p = cd.per_stage[(size_t)body.world_stage];
auto next = p(body, t, dt);
return world_modify([next](auto w) { /* 写回 next */ return w; });
};
}对称性:cd.per_stage[(size_t)body.world_stage] 与 FCC 的 compiled.per_stage[(size_t)s.current_stage] 同形态——这是 Blueprint §1 全局对称性的具体体现。
7. 完整 tick 流程
WorldTick (10ms 主循环):
① 跨 body 并行 body_tick:
for each body in parallel:
pipeline = compiled_dynamics.per_stage[body.world_stage]
body' = pipeline(body, t, dt)
(内部:plant probe → compute forces (Monoid) → integrate_rk4)
② 收集所有 body 的 DiscreteEvent → DynInFrame.events
③ sim::interpret_event(events) → StageOp 列表
MECO → TransitionOp{body=core, next=STAGE_1_SPENT}
BoltFired → SeparationOp{source=core, new_body=booster, ...}
LandingTouchdown → TransitionOp{body=core, next=LANDED}
④ dynamics::algebra::evolve_topology(bodies, op) 串行应用
bodies' = evolve_topology(bodies, op_1)
bodies'' = evolve_topology(bodies', op_2)
...
⑤ WorldState 推进时钟
⑥ 投递 BusManager.encode_from_world(bodies'')注意:
- ① 在每个 body 的当前 WorldStage 下执行(pipeline 派发)
- ③④ 在 tick 末尾执行——拓扑变化不在积分中段触发
- 解释器
sim::interpret_event在simulation/(Blueprint §3.6 line 761)——它是跨域代码,决定 BoltFired 翻译为 SeparationOp 还是 NoOp(取决于本仿真是否启用分离)
8. 反模式
| 反模式 | 为什么不行 |
|---|---|
FCC 直接调用 evolve_topology | 越过 simulation 解释器;破坏双代数化解耦 |
把 FccStage 与 WorldStage 合并为同一个 enum | 违反 §7.3 双代数化;故障注入场景无法表达 |
evolve_topology 内调用积分器 | 拓扑变换应纯;积分由 body_tick 在下个 tick 完成 |
Integrator 内部 if (world_stage == SPENT) ... | dynamics_core 不该知道 WorldStage 决定何种力源;用 Forces 表达 |
单一 body_tick_kernel 内 switch (world_stage) | 与 FCC 反模式同源;用 compiled.per_stage[] 派发 |
dynamics body pipeline 工厂放在 dynamics_core/pipelines/ | 工厂跨域调用 plant;违反 universal 承诺;归 simulation/pipeline/factories/ |
RocketBody.world_stage 被 plant 直接写 | 只允许 evolve_topology 写;其他写入路径破坏单一来源 |
在 evolve_topology 内部 broadcast 给 FCC | dynamics 不知道 FCC;状态变化通过 DiscreteEvent 反向通知 |
| SeparationOp 直接传 RocketBody 实例 | StageOp 字段应是 ID + 配置;实例化由 evolve_topology 完成 |
9. 测试策略
| 测试目标 | 级别 | 描述 |
|---|---|---|
NoOp 是 identity | unit | evolve_topology(bs, NoOp{}) == bs |
TransitionOp 幂等 | unit | 应用两次等价于一次 |
SeparationOp 守恒 | unit | 分离前后总质量、总动量、总能量守恒(瞬态) |
IgnitionOp 只改 fsm | unit | 应用前后除指定 engine.fsm 外所有字段不变 |
| 双代数独立 | integration | 注入"FCC 命令分离但 BoltFired event 不触发",验证 WorldStage 不变、FccStage 已转 |
| Per-WorldStage 派发正确 | integration | 把某 body 强置 STAGE_1_SPENT,验证调用的是 make_coast_pipeline 构造的闭包,无推力贡献 |
evolve_topology 纯函数 | unit | 同输入多次调用结果相同(无 hidden state) |
| StageOp 顺序依赖 | bench | 一拍内若有多个 StageOp,串行应用顺序与时间戳一致 |
10. C-Distillation 视角
| 类型 | C++ 阶段 | C 蒸馏阶段 |
|---|---|---|
WorldStage | enum class : uint16_t | typedef uint16_t WorldStage; + #define WS_PRE_LAUNCH 0 ... |
StageOp | std::variant<4 alternatives> | tagged union with uint8_t tag |
evolve_topology | std::visit 泛型 | switch (op.tag) |
CompiledDynamics.per_stage | std::array<std::function, N> | static const BodyPipeline per_stage[N] in .rodata |
| 派发 | cd.per_stage[(size_t)stage] | cd.per_stage[stage](已是整数索引) |
代数与派发结构完全保留;表面 syntax 退化。
11. Cross References
- Universal ODE Kernel(被 pipeline 调用的积分器)→
Universal_ODE_Kernel.md - Forces Monoid(pipeline 内累加) →
Forces_Monoid.md - FccStage 状态机(与 WorldStage 双代数化)→
04_FCC/FCC_State_Machine.md§3 - Pipeline 四段式架构 →
04_FCC/Pipeline_Factory_and_Compilation.md - BodyTick / WorldTick 跨域实例化 →
06_Simulation/Body_World_Tick.md(待写) - DiscreteEvent 路由与
interpret_event→06_Simulation/World_Tick_Event_Routing.md(待写) - Bus 协议(MECO / BoltFired payload) →
03_Avionics_and_Bus/Semantic_Bus_Pattern.md - 现有代码 →
src/dynamics/algebra/StageOp.h、BodyStageAlgebra.cpp(待 Wave 0 迁移到src/dynamics_core/algebra/) - Blueprint §2.9(拓扑代数)、§7.3(双代数化)、§2.6.4(跨域判据)