Skip to content

PCR 架构讨论记录

> 本文记录三轮深度讨论中提出的问题、批评与最终裁定。 > 它是 PCR_Master_Blueprint.md推理溯源,回答"为什么这样设计"。 > 阅读顺序:先看 Master Blueprint 理解"是什么",再看本文理解"为什么"。


第一轮讨论(12 个问题)


Q1 · RuntimeTables 是否有必要?WorldEnv 能否直接承担?

原始草稿的问题:Master 引入了 RuntimeTables 作为独立概念,与 WorldEnv 并列,含义模糊,关系不清。

讨论RuntimeTables 的唯一动机是"热路径不做字符串查找"。这个需求 WorldEnv 完全可以内含:只要其内部用 std::vector<T> + uint32_t 下标索引,装配期冻结,运行期不调 path_index,就已经是"运行时表"。引入一个额外的名字只会制造混乱。

裁定:废弃 RuntimeTables 这个名字。WorldEnv 内部的 vector<BodyAsset> / vector<EngineType> 等即是实现。path_index(string→uint32)只在装配期使用,运行期不调用。Master §7.6 记录此裁定。


Q2 · 架构图缺失 FCC 与 AvionicsSystem

原始草稿的问题:"一页架构"图里只有 World Tick 三相,FCC 完全没有出现;AvionicsSystem 作为闭环子系统的概念也没有体现。这相当于把大楼的地基画漏了。

讨论:FCC 不是 World Tick 的某个 Phase,而是一个独立的、以自己频率(50Hz)运行的 RWS 引擎,通过 FccInFrame / FccOutFrame 与 Bus 交互,与 Plant Physics 通过 DynInFrame / DynOutFrame 交互。FCC + Bus + Sensors + Actuators 共同构成"电气闭环子系统",它可以独立于 Plant Physics 运行(FCC 模飞测试场景)。

裁定:§1 全部重写为"四视角 + 系统运行图",明确 AvionicsSystem 与 Plant Physics 的双闭环结构,以及两者之间唯一通道是 DynInFrame / DynOutFrame(per-body)。


Q3 · 依赖图错误:plant_* / fcc / dynamics_core 不应依赖 bus

原始草稿的问题:依赖图画成"plant_* / fcc / dynamics_core 都依赖 bus",这违反了项目的核心解耦原则。

讨论:FCC 不知道 Bus 是什么,它只看 FccInFrame / FccOutFrameplant_* 不知道总线协议;dynamics_core 不知道总线。这些子系统互不依赖,通过 contracts(中立数据包层,header-only)对话。contracts 是真正的"中立区":任何人都可以 #include 它,但没有人拥有它,它里面没有运行逻辑。

同时提出了一张更有价值的"运行关系图"(计算流图),而不仅仅是链接时依赖图。Closed_Loop_System.md §0 的四层图是那张图的原型。

裁定:重画依赖图,引入 contracts 中立层(bus / fcc / plant_* / dynamics_core 平行,都只依赖 contracts + 基础层)。§2.1 更新。


Q4 · "truth" 命名与基础层的拆分

原始草稿的问题:把 Vec3 / Matrix / Quat / Angle / Time / Frame / RWS 全部归入名为 truth 的底层库,这个名字与项目语境无关,且混淆了三类性质不同的东西。

讨论

  • Vec3 / Matrix / Quat / Angle / Time / InterpTable:纯数学类型,真正的"基础类型"
  • Frames / 坐标变换 / WGS84:依赖数学类型,但属于几何/坐标系层
  • RWS / Free / Writer:单子工具箱,语义维度独立于数学类型

这三类东西性质完全不同,放在一起会产生错误的心智模型。

裁定:基础层三分:types(数学类型)/ frame(坐标系)/ monad(单子工具箱)。"truth" 名字废弃。contracts 作为第四个基础性的 header-only 层(DMZ,即跨子系统数据包的中立定义区)。§2.1 更新。


Q5 · AvionicsState / AvionicsEnv / BusState 是否有必要?

原始草稿的问题:提议了 AvionicsState { fcc, bus, sensors, actuators } 和对应的 AvionicsEnv,把 AvionicsSystem 封装成一个独立的 RWS 单子引擎。

讨论:这个方案有两个根本缺陷:

  1. 拓扑分裂时(一级分离),AvionicsState 需要被"分裂",需要写分裂逻辑,而分裂后两个 body 各自的 FCC、引擎、IMU 天然是 per-body 的概念;
  2. HIL_dyn 部署里 AvionicsState 的所有字段都不在本机,存在却没有意义,破坏了 PlantScope 的"按需实例化"语义。

正确做法:Avionics 组件状态直接挂在 RocketBody 里,与物理状态平级,在 BodyRWS pipeline 里一起演化。没有独立的 AvionicsSystem RWS 引擎,取而代之是 avionics_body_step(DynOutFrame) → BodyRWS<DynInFrame>,与 physics_body_step 同级。

裁定:废弃 AvionicsState / AvionicsEnv / BusStateRocketBody 直接携带 engines[] / servos[] / icus[] / imus[] / gpss[] / fcc / bus(per-body 全状态容器)。§2.3 / §7.7 更新。


Q6 · Plant Scope 概念是否被放弃?

原始草稿的问题:Master 对 Plant Scope 仅一行带过,份量严重不足。

讨论:Plant Scope 是 PCR 架构的核心机制——"Plant 物理真相始终唯一,但不同部署下实例化的子集不同"。它是 SIL/HIL 无缝切换的实现载体:通过 PlantScope YAML 字段,同一份 C++ 代码,在不同机器上实例化不同子组件,不改任何业务逻辑。

裁定PlantScope 提级为独立小节(§2.5),给出完整的结构定义和三种部署的 YAML 示例。


Q7 · Stage 与拓扑代数:TopologyEvent / BodySpawn 是过度设计

原始草稿的问题:引入了 TopologyEvent / BodySpawn 作为分离事件的中间结构,把代数产出封装成"事件",再由"TopologyEvolver"消费,多了一层不必要的抽象。

讨论:现有 PhysicalRegistry.cpp:118-124 里的 apply_topology_op(StageOp) 已经是正确的设计:StageOp 是算符,直接作用于 vector<RocketBody>,产出演化后的新 vector。命名为"Body"本来就和 COMPOSITION 对应,和 World 对应。StageOp::SeparationOp 就是分裂算符,无需包装成 TopologyEvent

ICU FSM 的输出是 DiscreteEvent,进入 DynInFrame.events,由 World 层末尾解释成 StageOp,再调 evolve_topology。ICU 不直接修改 World 状态。

裁定:删除 TopologyEvent / BodySpawn。保留现有 StageOp 代数(NoOp / TransitionOp / SeparationOp / IgnitionOp)。数据流:FCC IcuCmd → Bus → ICU FSM → DiscreteEvent → StageOp → evolve_topology。§2.6 / §7.2 更新。


Q8 · 三个具体情况的澄清

8a · AssembledPlant 反射树已放弃 前端应该自己根据 YAML 文件生成所需的 JSON/数据结构,C++ 端不需要可序列化的反射树。AssembledPlant 退化为非正式代称。裁定:废弃,§7.5 记录。

8b · PCR_PoC_Refactoring_Plan 中的总线行为细节 PoC 文档对总线行为(延时队列、语义透传、SIL 同 tick 路由等)有具体细化,这些内容应当被保留。裁定:相关内容整合进 §2.2 类型契约(bus::BusBuffer + bus::IBus)和 §7.1(Bus 裁定)。

8c · C-Distillation 暂置 当前处于 PoC 阶段,目标是干净的语法抽象,不是性能优化。C-Distillation 是后续目标,当前不约束设计细节。裁定:软化为建议(避免运行期虚继承),不作硬性约束。§7.9 记录。


Q9 · 推力计算流程在新框架下的归位

原始草稿的问题:Master 严重低估了推力管线的跨域复杂度,既没有说清楚哪个阶段属于哪个子系统,也没有说清楚 EngineEffect 这个边界对象如何流动。

讨论:推力计算横穿四个域:

  • Stage 1-2(配置加载 + CSV 插值)→ runtime::Assembler / types
  • Stage 3(Engine FSM,evaluate_phase + evaluate_vacuum_output)→ Avionics(RocketBody.engines[]
  • Stage 4(thrust pipeline:perturb → pressure → resolve)→ plant::physics
  • Stage 5(PhysicalRegistry 累加)→ dynamics_core

EngineEffect 是 Stage 3 → Stage 4 的边界对象,定义在 contracts,两侧互不直接依赖。

裁定:新增 §4(推力跨域数据流),完整呈现五阶段归位、摄动修正代数结构、喷管面积 TODO。


Q10 · FCC Free Monad 必须作为 Controller 协作方式的完整说明

原始草稿的问题:Master 几乎没有描述 FCC 内部架构,只在整改清单里提了一行"FCC 复活",严重低估了它的份量。

讨论:FCC Free Monad 是项目花了大力气完成的内容,它的三层架构(命令式外壳 / 函数式核心 / 数据驱动调度)是整个 Controller 的协作机制;FccInterpreter 作为轻量 HAL 的设计、算法纯函数化、调度表 YAML 化,都是经过深思熟虑的工程决策,不是实现细节。一份合格的重构 Master 必须把这些说清楚,因为任何新成员接手 FCC 开发,都需要理解这个框架。

裁定:新增完整的 §3(Controller Protocol),含:三层架构图、FccOp 代数、Interpreter 设计、与 Bus 的契约、与外部的隔离保证。


Q11 · src/dynamics/ 在八层中怎么放?

原始草稿的问题:Master 没有回答这个关键的实操问题。现有 src/dynamics/ 混了三类东西(核心算法 / 资产配置 / 物理管线),没有说清楚各自的迁移目标。

讨论

  • dynamics/ode/ / dynamics/state/ / dynamics/algebra/ / PhysicalRegistry.*:原地保留(只做小范围修正)
  • dynamics/config/(资产定义):数据结构移到 plant/model/,YAML 加载逻辑移到 runtime/loader/
  • dynamics/pipeline/(Drag / Thrust / Gravity / Prober):迁到 plant/physics/(Wave 1)

裁定:新增 §5.5(src/dynamics/ 迁移归位表),9 行逐条标注目标目录和 Wave 编号。Wave 0 专门处理目录归位(无逻辑变化)。


Q12 · TDD 应该采用火箭试验语义,而不是函数级断言

原始草稿的问题:列的测试用例都是函数签名级的 assert(test_drag_returns_correct_force_vector),这不是仿真验证的正确思维方式,也不符合航天工程的实践。

讨论:真实火箭研发的测试文化是"试验任务书"风格:输入条件、约束条件、期望产出,对应台架试验(bench)、子系统联试(subsystem)、6DoF 仿真(trajectory)、任务场景(mission)四个层次。每条测试都有工程语义,不只是验证一个函数的计算过程。

裁定:§8.3 全部重写为 TDD 四级体系(bench / subsystem / trajectory / mission),每级给出典型用例和对应的工程实践类比。


第二轮讨论(5 个问题)


Q13 · AvionicsSystem 闭环不简单:FCC 模飞测试是独立测试模式

问题:把 AvionicsSystem 说成"本身闭环"容易让人认为它自给自足、不需要 Plant Physics。但电气闭环和物理闭环是两回事:IMU 固定在桌上,没有发动机推力,惯组不会移动。

讨论:AvionicsSystem 的"闭环"是电气意义上的闭环(FCC → Bus → Actuator 状态机 → 状态被读回 → FCC),不是物理意义上的闭环(需要推力 → 加速度 → 速度 → 位置 → IMU 感知这条链路)。电气闭环跑起来就是"FCC 模飞测试"——这在工程上是一项真实的、有意义的测试,用于验证时序、调度表、状态机正确性,而不是退化状态。

裁定avionics_dry_flight 是一等公民的测试模式,在 §1.3 和 §6 Wave 2 守门中显式体现。§7.10 单独裁定。


Q14 · HIL 在新架构下如何实现?

问题:把 AvionicsSystem 视为独立闭环子系统后,SIL 似乎变简单了,但 HIL 感觉变难了。"同一套抽象的不同部署切片"这句话说得对,但怎么做?

讨论:HIL 的本质不是"重新设计一套架构",而是"在两个数据包的边界上换信道"。

  • AvionicsSystem ↔ Plant Physics 之间:DynInFrame / DynOutFrame,信道是 IDynChannel
  • FCC ↔ Bus+Devices 之间:FccInFrame / FccOutFrame,信道是 IFccChannel

SIL 用 InMemoryDynChannel(C++ 函数返回值);HIL_dyn 用 UdpDynChannel(物理机收发 UDP);HIL_fcc 用真实接口卡读写。代码逻辑一字不改,只换信道实现。

| 部署         | DynInFrame 信道      | DynOutFrame 信道     |
| sil_monolithic | 内存函数返回值       | 内存函数返回值       |
| hil_dyn       | UDP 收               | UDP 发               |
| hil_fcc       | (本机不跑物理)     | (本机不跑物理)     |

裁定:§2.5 引入 IDynChannel / IFccChannel 信道接口,§1.3 给出三种部署模式的对比表。


Q15 · 四层架构图中每层视角的深层含义

问题Closed_Loop_System.md §0 的四层图有更深刻的含义,原草稿并未完全理解。

讨论(逐层澄清):

FCC 视角:只看数据帧,不知道 Bus 协议、设备型号、物理环境。FccInFrame 本身就模拟了 Bus 的部分功能(数据帧化)。

Bus 视角:只看 (src, dst, payload) 三元组。既不关心计算,也不关心任何环境状态。Bus 是邮局,只投递,不阅读。

Device 视角:持有自身状态,接收来自 Bus 的命令,按 Mealy 状态机演化,产出输出。Device 不主动采样 World 状态,只接受"喂给它的真值"(来自 DynOutFrame)。ICU/Engine/Servo/GPS/IMU 全部按此建模。

World 视角:驱动环境采样,读取设备输出(DynInFrame),管理拓扑事件(StageOp),执行物理计算(与具体硬件参数完全解耦),积分,把积分真值返回给 Sensor。

核心解耦:发动机推力曲线变化 = 换发动机(plant_model 的 YAML 变了),但推力计算 pipeline(plant_physics)一字不变。配方换了,流水线不变。

裁定:§1 整体以"四视角"为主轴重构,运行图在四视角基础上展开,不再用"三相 tick"作为主叙事线。


Q16 · DynInFrame / DynOutFrame 应该是 per-body 的;AvionicsState 是否必要?

问题:原草稿把 DynInFrame / DynOutFrame 设计为 world 级(vector<EngineEffect> + vector<BodyTruth>),这是 World tick 视角的偷懒,不符合 body-centric 本质。同时质疑 AvionicsState / AvionicsEnv / BusState 是否真有必要,是否会影响 HIL 兼容性。

讨论

关于 per-bodyDynInFrame / DynOutFrame 必须是 per-body,才能在拓扑分裂时自然处理——vector 从 length=1 变成 length=2,每个 body 天然拥有自己那份,不需要写"分裂 Frame"的逻辑。

关于 AvionicsState/Env:有两个根本缺陷——

  1. 分裂时整个 AvionicsState 需要被分裂,要写额外的分裂规则;
  2. HIL_dyn 部署中 AvionicsState 的所有字段都不在本机,结构存在但没有意义。

正确方案:Avionics 组件状态进 RocketBody(per-body 全状态容器)。没有独立的 avionics_step(AvionicsEnv, AvionicsState) 签名,取而代之是 avionics_body_step(DynOutFrame) → BodyRWS<DynInFrame>,与 physics_body_step 完全对称,都在 World tick 里 per-body 并行调用。

关于 BusStateRocketBody.bus 持有 bus::BusBuffer(轻量延时队列),只处理该 body 内部的总线消息缓冲,不跨 body(分离后两段火箭物理上不再连接)。

裁定:§2.2 / §2.3 / §7.7 / §7.8 全部更新。


Q17 · §9-§12 的内容必须完整体现在 Master 中,不能只是整改清单

问题:原整改清单里提到"§9 推力五阶段归位"、"§10 FCC Free Monad"、"§11 dynamics/ 迁移"、"§12 TDD 语义",但实际只是清单条目,细节都被丢失了。

讨论:这四块内容是本轮讨论中思考最深入的部分,也是任何后来者接手时最需要理解的内容。把它们摘要成清单等于把最有价值的东西扔掉了。

裁定

  • §9 → 独立章节 §4(推力跨域数据流),含五阶段归位、摄动代数、喷管面积 TODO、边界对象 EngineEffect 的角色
  • §10 → 独立章节 §3(Controller Protocol),含三层架构图、FccOp 代数、Interpreter 轻量 HAL 设计、与 Bus 的契约、隔离保证五小节
  • §11 → §5.5(src/dynamics/ 迁移归位表),9 行逐条
  • §12 → §8.3(TDD 四级体系),含目录结构、工程实践类比、典型用例

这四节都是可独立读懂的完整章节,不是清单条目。


两轮讨论中达成的命名/设计裁定速查表

议题原始方案最终裁定
基础层命名truthtypes + frame + monad + contracts
中立数据层contracts(header-only,无运行逻辑)
热路径数据结构RuntimeTables(独立概念)WorldEnv 内部 vector<T> + uint32
Per-body 帧粒度World 级 vector<EngineEffect>Per-body DynInFrame { body_id, ... }
Avionics 系统状态AvionicsState { fcc, bus, sensors, actuators }直接挂在 RocketBody
Avionics RWS 引擎avionics_step(AvionicsEnv, AvionicsState)avionics_body_step(DynOutFrame) → BodyRWS<DynInFrame>
BusState独立结构bus::BusBuffer(轻量延时队列,per-body,挂在 RocketBody)
BodyStageAlgebraclass(一个 static 方法)自由函数 dynamics::algebra::evolve_topology
TopologyEvent / BodySpawn分离事件的包装层删除,直接用 StageOp + evolve_topology
AssembledPlant 反射树可序列化 C++ 对象,供前端读取废弃,前端自读 YAML
BodyStageTagC++ enumusing BodyStageTag = uint16_t(mission YAML 加载)
C-Distillation硬性约束(必须可机械降级到 C)后续目标,当前不约束设计细节
Bus 接口bus::BusChannel(旧)vs pcr::bus::IBus(新)并存bus::IBus 唯一,删 BusChannel.h + SignalTag
HIL 实现机制架构层面的分支逻辑信道抽象(IDynChannel / IFccChannel),只换实现,不改逻辑
FCC 模飞测试PoC 副产品 / 降级状态一等公民测试模式(avionics_dry_flight
TDD 导向函数签名级 assert试验任务书风格(bench / subsystem / trajectory / mission 四级)
update_inertial_properties 魔法常数engine_mass=50, I=m·10·I3删整个函数,从 inertia_table 查表(与 main 一致)
WorldEnv 的 const 问题const_cast<WorldEnv&> 在 Reader 里改 bus分离 WorldEnv(只读)和 RuntimeContext(可变),显式传参
closed_loop_10s.cpp弹道骨架,名称误导改名 ballistic_smoke_test.cpp,新建 closed_loop_full.cpp

第三轮讨论(10 个问题)

第二轮共识达成后,写入 Master 时再次发现深层错误。这一轮触及架构最底层的物理建模哲学。


Q18 · path_index 不该常驻 WorldEnv

问题:Master §2.4 在 WorldEnv 内保留 std::unordered_map<std::string, uint32_t> path_index 字段并标注"仅装配期使用,运行期禁用"。

讨论:标注"运行期禁用"等于承认这个字段不该常驻——它的生命周期就是 Assembler 的运行期。让它常驻 WorldEnv 是字段污染。

裁定:删除字段。path_index 改为 Assembler::assemble() 内部的局部 std::unordered_map,装配结束 RAII 析构。WorldEnv 里彻底没有 string→uint 索引。Master §2.4 / §7.7 更新。


Q19 · ICU 不属于 WorldEnv

问题:Master 在 WorldEnv 里放 vector<plant::model::IcuNode> icu_nodes

讨论:ICU 是航电系统的一部分,挂在 Bus 上接收 FCC 指令、发出离散事件。把它放在 WorldEnv 里与 §2.6 "ICU 通过 DiscreteEvent 与 World 解耦" 自相矛盾。分离后,每个子 body 应有自己的 ICU 实例,per-body 而非 world-global。

裁定

  • ICU 的类型规格IcuSpec,含转移表 / 延时常量)保留在 plant::model::,作为静态资产
  • ICU 的实例状态IcuState,含计时器 / 待发事件)挂在 RocketBody.icus[],per-body
  • WorldEnv 里彻底没有 ICU 实例

进一步推论:WorldEnv 不持有任何运行时实例(不持有 Bus 实例、不持有 IcuNode 实例、不持有 ServoState)。WorldEnv 只持有静态资产规格和全局常量。Master §2.4 / §2.3.2 更新。


Q20 · BusMonitor 应为 Bus 内部,使用 Writer Monad

问题:Master 把 BusMonitor*IBus* 并列放进 RuntimeContext,作为两个独立的可变副作用容器。

讨论:BusMonitor 的本质是"路由过程的可观测产物"——Bus 工作时顺带产生的日志/统计。这正是 Writer Monad 的形式。让它独立成指针挂在 RuntimeContext 上是错位。

裁定

  • BusLog 作为 Monoid(含 RoutingTrace / DropEvent 等)
  • BusM<A> = Writer<BusLog, A>IBus::publish / IBus::poll 返回 BusM<...>
  • BusM<...> 在 BodyRWS 中通过 lift_writer 注入到 DynLog,BusLog 自然汇聚到 World tick 总日志
  • RuntimeContext 简化为只剩 IBus*

Master §2.4 / §2.7 / §7.1 更新。


Q21 · PoC 阶段应使用强类型 enum,不用 uint16_t / uint32_t

问题:Master 第二版广泛使用 uint16_t stage_tag / uint32_t body_id 等裸整数类型,理由是"YAML 驱动的扩展性"。

讨论:这种泛化在 PoC 阶段牺牲类型安全:

  • uint16_t stage_tag 谁都能 +1,谁都能赋值 65535,编译器一声不吭
  • uint32_t body_iduint32_t engine_id 看起来一样,可以互换调错不报

PoC 阶段的关键是"算法和架构对不对",不是"YAML schema 自由不自由"。强类型 enum 的代价是"加新 stage 要改 C++ 并重编",但这恰恰是 PoC 阶段应有的安全网。

裁定

  • 状态/类型枚举:enum class FccStage : uint16_t {} / enum class WorldStage : uint16_t {} / enum class EnginePhase : uint8_t {}
  • 实例 ID:enum class BodyId : uint32_t {} / enum class EngineId : uint32_t {}
  • YAML 加载通过 string_to_enum<E>(s) 显式转换,未匹配启动期报错

Master §2.2 / §2.6 / §7.6 更新。


Q22 · Layer 1 是 Declarative Strategy,不是 Imperative Shell(命名搞反)

问题:Master §3.1 把 Free Monad DSL 称为 "Layer 1 命令式外壳(Imperative Shell)"。

讨论:Free Monad DSL 的核心特征恰恰是声明式(描述"做什么",不描述"怎么做")。Imperative 的是 Interpreter 那一层(具体执行 IO 副作用)。命名搞反了。

参考依据:04_Data_Driven_Scheduling_and_RWS.md 中 "Functional Core, Imperative Shell" 标语里,"Imperative Shell" 指的是 Interpreter,不是 DSL。

裁定

  • Layer 1 → Declarative Strategy(声明式策略,Free Monad DSL)
  • Layer 2 → Imperative Interpreter(命令式解释器,FccInterpreter)
  • Layer 3 → Pure Functional Core(函数式核心,GNC 算法纯函数)

Master §3.1 重写。


Q23 · FccState 内容写错了,应是 GNC 三模块的 Mealy state

问题:Master 列的 FccState 内容("协方差矩阵 / PID 积分项 / task_timers / pending_events / bias 估计")是从早期 Mock 文档抄过来的,不是真实 FCC 的状态构成。

讨论:真实 FCC 是 GNC 三个模块各自的 Mealy 状态机,且模块间相互引用对方的 state

  • Navigation:双子样补偿中间量、上拍 IMU 增量备份、当前导航解、关机时刻预测
  • Guidance:当前最优程序角、关机余量、入轨判据中间量;引用 nav.
  • Control:PID 积分项、上拍偏差、SCU 命令缓存;引用 guide.program_angle

裁定:FccState 重写为 NavState / GuideState / CtrlState 三模块组合。模块间相互引用关系明确写入文档。Master §3.6 重写。


Q24 · Dispatcher 架构错误,FCC 自有 Stage 状态机,与 WorldStage 独立

问题:Master 把"调度"放在 Functional Core 下面作为 "Layer 3 Dispatcher",混淆了多速率调度(50Hz vs 1Hz)和阶段状态机(上升段 / 主动段 / 关机段)。

讨论:FCC 内部有自己的 Stage(FccStage),与 World 的 Stage(WorldStage)独立:

  • WorldStage:火箭整体飞行阶段,YAML 驱动,由 ICU DiscreteEvent + Mission 事件机推进
  • FccStage:FCC 内部状态,由 FCC 自己的关机/入轨计算驱动(关机余量到 → PreShutdown)

两者间接关联:FccStage 切换会通过 ICU 命令最终触发 WorldStage 演化,但不直接绑定

正确架构:每个 FccStage 对应一个独立的 Free<FccOp, void> strategy,调度即"FccStage 转移 → 选新 strategy → 执行"。多速率(50Hz vs 1Hz)由各算法在自己的 task_timer 内部判断,不上升到架构层。

裁定:删除 "data-driven dispatcher 查任务表" 叙事。FccStage 状态机本身就是调度器。Master §3.7 重写。


Q25 · FccStage 转移代数化(YAML 驱动)

问题:FccStage 转移如果用 if-else 硬编码,YAML 无法指定"哪个阶段用什么 guide / shutdown criterion"。

讨论:所有制导方案、关机方式都和飞行阶段绑定,必须支持数据驱动。代数化是唯一办法。

  • FccStageOp = NoOp | EnterStage | ShutdownMarginZero | AltitudeAbove | AccelerationAbove | OrbitalParamsReached
  • evolve_fcc_stage(core, op) 自由函数(与 dynamics::algebra::evolve_topology 对仗)
  • stages.yaml 列出每个 FccStage 的 nav_algo / guide_algo / ctrl_algo 和转移条件

裁定:FccStage 代数化,与 WorldStage 代数完全对仗。Master §3.7 / §7.3 / §8.4 更新。


Q26 · 双重实体论:物理实体 vs 电子器件

问题:Master 之前把 SCU 与 ServoMech、ECU 与 EngineMech 混为一谈,没有区分"机械装置"和"电子器件"。

讨论:火箭上的"硬件"分两类:

  • 物理实体(属于 Body,归 Plant 域):伺服机构、发动机本体、栅格舵机构、IMU 物理元件
  • 电子器件(属于 AvionicsSystem,归 Avionics 域):SCU、ECU、ICU、IMU 单机、GPS 接收机、FCC

它们的关系是 采样-驱动对

  • SCU 采样真实摆角,得到数字摆角;驱动伺服目标,得到机械动作
  • SCU 报告的摆角可以与真实摆角不一致——这就是故障建模的根本动机

重要推论:"AvionicsSystem 自身闭环"是错误措辞。完整闭环必须包含 ServoMech / EngineMech / 大气环境 / 重力。"FCC 模飞测试"也不是 Avionics 单跑——它是 Avionics + 静态 Plant:物理实体被冻结但仍存在并被 Avionics 采样。

裁定:建立"双重实体论"作为架构核心原则。Master §1.2 / §7.15 强调闭环必须含物理;§2.3 重写 RocketBody 形态。


Q27 · device-as-unified-entity(不是 PlantHardware/BodyAvionics 容器分割)

问题:双重实体论的初稿提议把 RocketBody 拆成 PlantHardware { engines, servos, fins } + BodyAvionics { ecus, scus, fin_ctrls, imus, gpss, icus } 两个子结构。

讨论:这是"按程序员视角切",不是"按火箭视角切"。真实火箭里:

  • 一台发动机 = 燃烧室 + ECU,机械和电子是焊在一起的物理对象
  • 一个伺服 = 液压机构 + SCU,是一个物理对象
  • 一个 IMU = 敏感元件 + 数字处理板,是一个物理对象

把"机械"塞 PlantHardware、把"电子"塞 BodyAvionics,强行切碎了一体的物理对象。

正确建模:每个 device 是 RocketBody 的一个完整实体,自身同时持有 mech(如有)+ 电子面:

  • Engine { id, install, spec*, EngineMech mech, EcuState ecu }
  • Servo { id, install, spec*, ServoMech mech, ScuState scu }
  • Fin { id, install, spec*, FinMech mech, FinCtrlState ctrl }
  • ImuUnit { id, install, spec*, ImuState state }(仅电子,物理输入是 body.aux)
  • GpsUnit { id, install, spec*, GpsState state }(仅电子,物理输入是 body.spatial)
  • Icu { id, spec*, IcuState state }(纯电子)

采样/驱动是设备内部的私事:Servo 的 SCU 采样自家 mech,驱动自家 mech。不需要跨容器查找。**容器统一 + 接口隔离(透传 step 函数)**才是正确平衡。

裁定:删除 PlantHardware / BodyAvionics 容器分割。RocketBody 直接持有 vector<Engine> / vector<Servo> / vector<Fin> / vector<ImuUnit> / vector<GpsUnit> / vector<Icu>。Master §2.3 / §7.5 更新。


Q28 · Device 操作不需要 algebra+interpreter 模式

问题:Master 草案把 SCU 的动作建模为 std::variant<SampleServoAngle, AcceptCommand, DriveServoTarget, PublishStatus> algebra + Interpreter 模式。

讨论:这是把 Bus 和 FCC 的成功模式套到 device 身上的错位类比。

  • Bus 用 variant 是因为:多消息类型在同一信道流动 + 多物理协议(1553B/TTE/InMemory)需要不同编解码
  • FCC 用 Free Monad 是因为:策略要 YAML 驱动 + Sim/RTOS 部署需不同 Interpreter 执行

但 SCU 没有任何这两个问题:操作集合固定,fidelity 模式(透传/真实/故障)是局部决策,不存在跨边界解释。

裁定:Device step 函数是普通函数 + Config 结构。Fidelity::{Transparent, Realistic, Fault} 由函数内部 if 切换。

  • 不引入 algebra
  • 不引入 visitor / interpreter
  • 透传是函数行为,不是架构层级
algebra+interpreter?原因
Bus✅ 用多消息类型 + 多物理协议
FCC✅ 用策略 YAML 驱动 + 多部署 interpreter
Device❌ 不用操作固定 + fidelity 局部决策

Master §2.3.3 / §7.4 更新。三层 interpreter 对仗修正为两层。


Q29 · IMU / GPS 必须作为正式 device 进入设备层

问题:Master 草案中说 "IMU 借用 AuxState,不需要额外 ImuMech 字段"。

讨论:这是把"IMU 物理输入恰好是 body.aux"和"不需要 IMU device"混为一谈。真实 IMU 工作链:

body.aux.{omega_b, spec_force_b}  ← 物理事实

转换比率(脉冲数/(rad·s)、脉冲数/(m·s⁻²))← 由实际标定

数字脉冲数增量(Δθ_pulses, ΔV_pulses)  ← IMU 输出
       ↓ Bus
FCC 接收 IMU 标定系数 ← 由地面注入

反算 (Δθ, ΔV) in rad / m·s⁻¹  ← FCC 内部

PoC 简化:跳过"脉冲数 ↔ 物理量"双向转换(取系数=1),但 IMU device 必须形式上存在——它有自己的 state(积分增量、bias、双子样备份)、自己的 step 函数。

GPS 同理:step 应该把 spatial.pos_ecf 转 WGS84 LLA + 加噪声 → BusMessage。PoC 阶段噪声=0、跳过双向转换,但 device 完整存在。

裁定:IMU/GPS 作为正式 device 进入设备层,物理输入显式从 body.aux / body.spatial 读取。Master §2.3.2 完整描述。


第三轮裁定速查表

议题原始方案第三轮裁定
path_index 位置WorldEnv 字段Assembler 局部变量,RAII
ICU 实例位置WorldEnv.icu_nodesRocketBody.icus(per-body)
BusMonitor 处理RuntimeContext.monitor*Bus 内部 Writer Monad
ID / Stage 类型uint16_t / uint32_tenum class(强类型)
FCC Layer 1 命名Imperative ShellDeclarative Strategy
FCC Layer 2 命名Functional CoreImperative Interpreter
FccState 内容协方差/PID/task_timersNavState/GuideState/CtrlState 三模块 Mealy
FCC 调度模式Data-driven dispatcher 查表per-FccStage strategy + FccStageOp 代数
FccStage 与 WorldStage一个 stage 概念两个独立代数(FccStageOp + StageOp)
物理硬件 vs 电子器件混为一谈双重实体论(mech + 电子面)
RocketBody 容器分割PlantHardware / BodyAvionics 二分device-as-unified-entity(每 device 含 mech + 电子面)
Device 操作建模algebra + interpreter普通 step 函数 + Config(fidelity if 切换)
IMU / GPS 形式借用 AuxState/SpatialState正式 device 进设备层
AvionicsSystem 闭环自身闭环必须含 PlantHardware + Physics
Interpreter 三层对仗Bus / FCC / Device 三对仗Bus / FCC 两对仗(Device 不需要)

贯穿三轮讨论的核心设计哲学

以下五点是讨论中反复出现、被反复强调的根本原则:

1. 视角隔离胜过统一 每层只看自己该看的东西。FCC 不知道发动机型号,Bus 不知道物理计算,Device 不主动采样环境。这种"被设计的无知"不是缺陷,是解耦的证明。

2. 配方与流水线分离plant_model(YAML 资产)是配方,plant_physics(计算 pipeline)是流水线。换发动机推力曲线只改配方,流水线一字不变。这条原则贯穿了推力、气动、惯量的所有计算模块。

3. main 分支是物理真相,pcr-arch 是目录结构 重构不是重写,是迁移:把 main 分支已经跑通的物理逻辑放到正确的命名空间和目录里,改 include 路径,改 namespace,不改计算逻辑。黄金轨迹(tests/golden/main_baseline.csv)是一切重构的物理安全网。

4. 双重实体论:物理实体与电子器件并存于同一 device 内 火箭上的每个设备同时拥有物理面(mech,机械/液压/光学)和电子面(数字 Mealy 机)。这两面焊接在同一物理对象上,不应在容器层切碎,但通过显式 sample/drive 接口约束耦合。容器统一 + 接口隔离

5. 抽象工具的代价感知 algebra + interpreter 不是免费的。它解决两类问题:跨边界解释(Bus 多协议)、跨部署解释(FCC Sim vs RTOS)。Device 没有这两类问题,强行套用是过度设计。用对工具,比用花哨工具更重要


第四轮讨论(架构精炼:环境/对象分离与跨域绑定层)

> 触发:v0 Blueprint 在 dynamics/ 与 dynamics_core/ 命名混乱、RocketBody 归属暧昧、风场/重力场放在 plant/ 这些问题上被用户的连续追问暴露。本轮裁定大幅修订 §2。


Q30 · plant 是物理规律的全部吗?风场/重力场属于哪里?

问题:v0 把 "风场、重力场、大气模型" 统统放进 plant/,论据是"它们都是物理规律"。

讨论:物理规律实际上分为两类,混在一起是类型论错误

  1. 普适的场(Field):重力场 g(x)、大气场 p(h)、风场 v(x, t)。它们是关于位置/时间的函数,不属于任何具体物体,但作用于所有物体。
  2. 对象专属的本构关系(Constitutive Relation):气动系数表 Cd(Mach, α, β)、推力曲线 T(t_burn, p_amb)、质量惯量 I(t_burn)。它们绑定到具体几何体/发动机型号

数学物理上的术语:环境是 field,对象是 constitutive。两者的可变性 profile 也完全不同——地球大气在地球范围内基本是常量,但火箭型号每次任务都会换。

裁定

  • 引入 src/environment/ 顶层目录,装 Atmosphere / GravityField / WindField
  • src/plant/model/ 只装对象专属的 spec
  • 两者由 sim::WorldEnv 共同聚合,但版本独立、目录独立、可替换性独立

反方观点:路径变长,初学者难找。

反驳:把两个可变性 profile 不同的东西混在一起,迟早会因为"为了换一个气动模型却被迫改动大气模型代码"而暴露。分离的代价是一个目录路径,混合的代价是后期 refactor。

Master §2.3 / §2.4 / §7.15 记录此裁定。


Q31 · dynamics 还是 dynamics_core?这个改名是不是吹毛求疵?

问题:v0 用 src/dynamics/,其内部混居着 pipeline/(Thrust/Drag)、state/(RocketBody)、ode/(积分器)。用户提出 dynamics_coredynamics 更好。

讨论:核心问题不是名字,而是名字背后的承诺。如果叫 dynamics/,就允许里面装"和动力学有关的一切",包括气动力计算。但气动力是 对象专属的本构关系,不属于"通用动力学引擎"。它的正确归宿是 plant/physics/

_core 这个后缀是一种承诺标记:它声明这个目录"只装核心引擎、不装具体业务"。这与函数式编程中的 universal vs specific 的分层完全对应:

  • _core ↔ universal kernel(不知道具体业务)
  • plant/avionics/fcc/ ↔ specific instantiation

裁定

  • dynamics_core/ 替代 dynamics/
  • 其内容严格限定为:ode/(运动方程+积分器)、algebra/(StageOp+evolve_topology)、pipeline/(RWS<Env,State,Log> 模板 + Forces monoid)、state/(SpatialState/InertialState/AuxState 等物理状态原语)
  • 编译期一票否决:dynamics_core/ 不得 include 任何业务子域

验证手段grep -r "plant/\|avionics/\|fcc/\|bus/\|environment/\|simulation/" src/dynamics_core/ 必须零命中。

Master §2.5 / §7.16 记录此裁定。


Q32 · RocketBody 归属之争:放哪里都尴尬

问题RocketBody 同时被多个子域操作:

  • dynamics_core 积分它的 spatial
  • avionics/devices 推进它的 engines/servos/imus
  • bus 持有它的 BusBuffer
  • fcc 占用它的 fcc 字段

放在任何一个子域里,都强迫该子域依赖其他所有子域,违反"无知性"承诺。

讨论了三种方案

  1. 放在 dynamics_core/state/(v0 一度倾向):dynamics_core 被迫 include avionics + bus + fcc,破坏 universal 承诺。否决
  2. 放在 runtime/:runtime 本应是薄的生命周期层,把业务定义塞进来违反职责切割。否决
  3. 拆分:把物理状态 (PhysicalBody) 留在 dynamics_core/state/,复合体 (RocketBody) 放在更高层。部分采纳

最终方案:引入 simulation/ 跨域绑定层

  • dynamics_core/state/ 保留物理状态原语(SpatialState / InertialState / AuxState
  • simulation/state/ 装复合体 RocketBody(聚合物理状态 + devices + bus + fcc)
  • simulation/ 是唯一允许聚合所有业务子域的层

类比:dynamics_core 像 STL 的 &lt;algorithm&gt;simulation 像具体的 std::vector&lt;MyType&gt;。底层提供算法,上层提供类型实例化。

裁定:RocketBody 归 src/simulation/state/

Master §2.6.1 / §7.5 / §7.17 记录此裁定。


Q33 · 双层 RWS 的咬合机制:Probe 是什么?

问题:v0 描述了 WorldRWS 与 BodyRWS 双层结构,但没说清两者之间如何具体咬合。如果 BodyRWS 直接读 WorldEnv,那 BodyEnv 这个概念形同虚设;如果 BodyRWS 读不到 WorldEnv,那它的 Reader 内容从哪来?

讨论:缺失的是 Prober 概念

数学物理上,单体动力学评估只需要"此时此地"的局部上下文:

  • 当前位置的大气压(不是全局大气表)
  • 当前马赫数(不是 atmosphere model)
  • 当前质量(不是 inertia table)

Probe 函数就是降维算子

probe : WorldEnv × RocketBody × Time → BodyEnv

它把全局 Reader 切片成局部 Reader。

裁定

  • simulation/probe/ 装所有 prober 函数:probe_aero / probe_traj / probe_mass_props
  • plant/physics/compute_thrust(BodyEnv, RocketBody) 只读 BodyEnv,不直接读 WorldEnv
  • 换 environment / 换 plant assets 都只影响 prober 实现,不影响 physics pipeline

这个模式让 plant/physics/ 彻底无知于 environment 大表的存在。它只看 BodyEnv.aero.static_pressure 这种数值。换地球→月球只改 prober,pipeline 一字不变。

Master §2.6.3 / §4.3 / §7.19 记录此裁定。


Q34 · WorldEnv 应该在 runtime/ 还是 simulation/?

问题:v0 说 WorldEnv 在 runtime/,理由是"它是运行时装配产物"。

讨论:装配是动作,归属是名词。runtime::Assembler 负责装配 WorldEnv 这个动作,但 WorldEnv 这个类型定义应该住在哪里?

WorldEnv 含有:

  • env::Atmosphere(来自 environment/
  • plant::model::EngineSpec[](来自 plant/
  • frame::FrameConfig(来自 frames/

它是一个跨域聚合类型。和 RocketBody 一样面临相同的归属问题。

裁定

  • sim::WorldEnv类型定义放在 src/simulation/env/
  • runtime::Assembler::assemble() 负责构造它
  • runtime 不持有 WorldEnv 的定义,只 include 并构造它

这与 RocketBody 的处理保持一致:跨域复合体类型住 simulation,runtime 只做生命周期。

Master §2.6.2 / §2.7 / §7.18 记录此裁定。


Q35 · runtime/ 到底剩什么?

问题:把 WorldEnv 移到 simulation/、把 Assembler 业务逻辑拆出去之后,runtime/ 还有什么?

裁定:runtime/ 退化为非常薄的一层,只剩三件事:

  1. Loader:YAML 解析(包括 environment、plant、fcc、mission、deployment)
  2. Assembler:调用 Loader、按 PlantScope 构造 sim::WorldEnv 与初始 sim::WorldState,返回 SimulationInstance
  3. Runner:以 SimulationInstance 为输入,跑 main loop,调用 sim::world_tick
  4. IDynChannel:跨进程信道抽象(SIL/HIL 切换),由 Runner 持有

runtime 不持有任何业务定义(不定义 RocketBody、不定义 WorldEnv 类型),只做生命周期管理。

Master §2.7 / §7.18 记录此裁定。


Q36 · 顶层目录的依赖箭头方向

问题:v0 的依赖图把 dynamics_core 画在与 bus / plant_* 并列的位置,箭头模糊。

裁定:清晰的有向无环图:

                          runtime


                         simulation        ← 唯一允许聚合所有业务子域
                    ┌────┬───┴────┬────┬───┬────────┐
                    ▼    ▼        ▼    ▼   ▼        ▼
              dynamics_core fcc avionics plant bus environment
                    │       │       │      │ │   │       │
                    └───────┴───────┴──────┴─┴───┴───────┘


                                contracts            ← header-only 中立
                                  /  |  \
                                 ▼   ▼   ▼
                              types frames monad
  • 业务子域之间互不依赖(除 avionics→plant 用于 ECU 读 EngineMech 例外)
  • 业务子域只能依赖 contracts / types / frames / monad
  • simulation 是顶层聚合
  • runtime 在 simulation 之上

Master §2.1 / §6.0.2 记录此裁定。


第四轮裁定速查表

议题v0 方案v1.0 裁定
风场/重力场归属plant/(与气动表混居)独立 environment/ 目录
动力学核心命名dynamics/(含 pipeline + state)dynamics_core/(universal kernel)
pipeline/ 内容(Thrust/Drag)dynamics/pipeline/plant/physics/(对象本构)
RocketBody 归属plant/model/dynamics/state/simulation/state/(跨域复合体)
WorldEnv 归属runtime/simulation/env/(runtime 只做装配)
Probe 函数归属没有明确位置simulation/probe/(双层 RWS 咬合齿轮)
双层 RWS 实例化描述不清晰dynamics_core 提供模板,simulation 实例化
WorldStage 代数class StageMachine模板化自由函数 evolve_topology&lt;Body&gt;
物理状态原语归属dynamics/state/dynamics_core/state/(与 RocketBody 分离)
runtime/ 内容WorldEnv + Assembler + Runner只剩 Loader / Assembler / Runner / IDynChannel

贯穿四轮讨论的核心设计哲学(v1.0 增补版)

1. 视角隔离胜过统一 每层只看自己该看的东西。FCC 不知道发动机型号,Bus 不知道物理计算,Device 不主动采样环境。被设计的无知是解耦的证明

2. 配方与流水线分离plant/model(YAML 资产)是配方,plant/physics(计算 pipeline)是流水线。换发动机推力曲线只改配方,流水线一字不变。

3. main 分支是物理真相,pcr-arch 是目录结构 重构不是重写,是迁移:把 main 分支已经跑通的物理逻辑放到正确的命名空间和目录里。

4. 双重实体论:物理实体与电子器件并存于同一 device 内 火箭上的每个设备同时拥有物理面(mech)和电子面(数字 Mealy 机)。容器统一 + 接口隔离

5. 抽象工具的代价感知 algebra + interpreter 解决跨边界/跨部署解释问题。Device 没有这两类问题,强行套用是过度设计。

6.【v1.0 新增】场与对象的本质分离 普适的场(environment)与对象的本构(plant)是两种不同性质的物理规律。它们的目录、版本、可替换性都必须独立。别把不同 profile 的可变性塞进同一个箱子

7.【v1.0 新增】Universal 与 Specific 的清晰分层dynamics_core 是 universal kernel(不知道火箭长什么样),plant / avionics / fcc / environment / bus 是 specific 子域(每个只关心自己的领域),simulation 是跨域绑定层(唯一允许聚合)。命名上 _core 后缀是承诺标记。

8.【v1.0 新增】跨域复合体必须有合法的归属RocketBody 这种"同时被多个子域操作"的复合体,不能塞进任何单一子域(否则违反该子域的无知性)。必须有一个跨域绑定层(simulation/)专门承担这种归属。正视真实存在的架构压力,不要藏在某个子域里