Skip to content

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 noteWorldStageenum class : uint16_tStageOp 蒸馏为 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:强类型相位枚举

cpp
// 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:拓扑变换代数

cpp
// 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.stateIdle 翻到 Ignition。实际推力由 plant/physics/Thrust 在下个 tick 根据 fsm 状态计算。

3.2 contracts::BodyId 强类型

cpp
// 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 里的东西

  • RestartEngineOpAbortMissionOp——这些是 mission-specific,应通过 FccStageOp 表达后再翻译
  • UpdateAeroModelOp——气动模型由 RocketBody.world_stage 间接决定(同一 stage 用同一 aero model)
  • ChangeMassOp——质量变化是连续积分的副产物(d_mass_dt),不是离散事件

4. evolve_topology:纯函数变换

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

维度WorldStageFccStage
归属库dynamics_core/algebra/fcc/stage/
代数StageOp = variant<NoOp, Transition, Separation, Ignition>FccStageOp = variant<NoOp, Transition, ScheduleEvent, EmitDiscreteEvent>
演化函数evolve_topologyevolve_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

text
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(极稀薄)+ gravity
  • PAYLOAD_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 应用)

FCCDynamics
① 工厂fcc/pipelines/(intra-FCC,层级无知simulation/pipeline/factories/跨域工厂
② 编译fcc/firmware/compile_fccsimulation/pipeline/compile_dynamics
③ 触发runtime::Assemblerruntime::Assembler
④ ticksimulation::FccTicksimulation::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/,本文档只示意)

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

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

cpp
// 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 流程

text
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_eventsimulation/ (Blueprint §3.6 line 761)——它是跨域代码,决定 BoltFired 翻译为 SeparationOp 还是 NoOp(取决于本仿真是否启用分离)

8. 反模式

反模式为什么不行
FCC 直接调用 evolve_topology越过 simulation 解释器;破坏双代数化解耦
FccStageWorldStage 合并为同一个 enum违反 §7.3 双代数化;故障注入场景无法表达
evolve_topology 内调用积分器拓扑变换应纯;积分由 body_tick 在下个 tick 完成
Integrator 内部 if (world_stage == SPENT) ...dynamics_core 不该知道 WorldStage 决定何种力源;用 Forces 表达
单一 body_tick_kernelswitch (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 给 FCCdynamics 不知道 FCC;状态变化通过 DiscreteEvent 反向通知
SeparationOp 直接传 RocketBody 实例StageOp 字段应是 ID + 配置;实例化由 evolve_topology 完成

9. 测试策略

测试目标级别描述
NoOp 是 identityunitevolve_topology(bs, NoOp{}) == bs
TransitionOp 幂等unit应用两次等价于一次
SeparationOp 守恒unit分离前后总质量、总动量、总能量守恒(瞬态)
IgnitionOp 只改 fsmunit应用前后除指定 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 蒸馏阶段
WorldStageenum class : uint16_ttypedef uint16_t WorldStage; + #define WS_PRE_LAUNCH 0 ...
StageOpstd::variant<4 alternatives>tagged union with uint8_t tag
evolve_topologystd::visit 泛型switch (op.tag)
CompiledDynamics.per_stagestd::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_event06_Simulation/World_Tick_Event_Routing.md(待写)
  • Bus 协议(MECO / BoltFired payload) → 03_Avionics_and_Bus/Semantic_Bus_Pattern.md
  • 现有代码 → src/dynamics/algebra/StageOp.hBodyStageAlgebra.cpp(待 Wave 0 迁移到 src/dynamics_core/algebra/
  • Blueprint §2.9(拓扑代数)、§7.3(双代数化)、§2.6.4(跨域判据)