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 / FccOutFrame;plant_* 不知道总线协议;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 单子引擎。
讨论:这个方案有两个根本缺陷:
- 拓扑分裂时(一级分离),
AvionicsState需要被"分裂",需要写分裂逻辑,而分裂后两个 body 各自的 FCC、引擎、IMU 天然是 per-body 的概念; - HIL_dyn 部署里
AvionicsState的所有字段都不在本机,存在却没有意义,破坏了PlantScope的"按需实例化"语义。
正确做法:Avionics 组件状态直接挂在 RocketBody 里,与物理状态平级,在 BodyRWS pipeline 里一起演化。没有独立的 AvionicsSystem RWS 引擎,取而代之是 avionics_body_step(DynOutFrame) → BodyRWS<DynInFrame>,与 physics_body_step 同级。
裁定:废弃 AvionicsState / AvionicsEnv / BusState。RocketBody 直接携带 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,信道是IDynChannelFCC ↔ 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-body:DynInFrame / DynOutFrame 必须是 per-body,才能在拓扑分裂时自然处理——vector 从 length=1 变成 length=2,每个 body 天然拥有自己那份,不需要写"分裂 Frame"的逻辑。
关于 AvionicsState/Env:有两个根本缺陷——
- 分裂时整个 AvionicsState 需要被分裂,要写额外的分裂规则;
- HIL_dyn 部署中 AvionicsState 的所有字段都不在本机,结构存在但没有意义。
正确方案:Avionics 组件状态进 RocketBody(per-body 全状态容器)。没有独立的 avionics_step(AvionicsEnv, AvionicsState) 签名,取而代之是 avionics_body_step(DynOutFrame) → BodyRWS<DynInFrame>,与 physics_body_step 完全对称,都在 World tick 里 per-body 并行调用。
关于 BusState:RocketBody.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 四级体系),含目录结构、工程实践类比、典型用例
这四节都是可独立读懂的完整章节,不是清单条目。
两轮讨论中达成的命名/设计裁定速查表
| 议题 | 原始方案 | 最终裁定 |
|---|---|---|
| 基础层命名 | truth | types + 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) |
BodyStageAlgebra | class(一个 static 方法) | 自由函数 dynamics::algebra::evolve_topology |
TopologyEvent / BodySpawn | 分离事件的包装层 | 删除,直接用 StageOp + evolve_topology |
AssembledPlant 反射树 | 可序列化 C++ 对象,供前端读取 | 废弃,前端自读 YAML |
BodyStageTag | C++ enum | using 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_id和uint32_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 | OrbitalParamsReachedevolve_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_nodes | RocketBody.icus(per-body) |
| BusMonitor 处理 | RuntimeContext.monitor* | Bus 内部 Writer Monad |
| ID / Stage 类型 | uint16_t / uint32_t | enum class(强类型) |
| FCC Layer 1 命名 | Imperative Shell | Declarative Strategy |
| FCC Layer 2 命名 | Functional Core | Imperative Interpreter |
| FccState 内容 | 协方差/PID/task_timers | NavState/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/,论据是"它们都是物理规律"。
讨论:物理规律实际上分为两类,混在一起是类型论错误:
- 普适的场(Field):重力场 g(x)、大气场 p(h)、风场 v(x, t)。它们是关于位置/时间的函数,不属于任何具体物体,但作用于所有物体。
- 对象专属的本构关系(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_core 比 dynamics 更好。
讨论:核心问题不是名字,而是名字背后的承诺。如果叫 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积分它的spatialavionics/devices推进它的engines/servos/imusbus持有它的BusBufferfcc占用它的fcc字段
放在任何一个子域里,都强迫该子域依赖其他所有子域,违反"无知性"承诺。
讨论了三种方案:
- 放在
dynamics_core/state/(v0 一度倾向):dynamics_core 被迫 include avionics + bus + fcc,破坏 universal 承诺。否决。 - 放在
runtime/:runtime 本应是薄的生命周期层,把业务定义塞进来违反职责切割。否决。 - 拆分:把物理状态 (
PhysicalBody) 留在dynamics_core/state/,复合体 (RocketBody) 放在更高层。部分采纳。
最终方案:引入 simulation/ 跨域绑定层。
dynamics_core/state/保留物理状态原语(SpatialState / InertialState / AuxState)simulation/state/装复合体RocketBody(聚合物理状态 + devices + bus + fcc)simulation/是唯一允许聚合所有业务子域的层
类比:dynamics_core 像 STL 的 <algorithm>,simulation 像具体的 std::vector<MyType>。底层提供算法,上层提供类型实例化。
裁定: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_propsplant/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/ 退化为非常薄的一层,只剩三件事:
- Loader:YAML 解析(包括 environment、plant、fcc、mission、deployment)
- Assembler:调用 Loader、按 PlantScope 构造
sim::WorldEnv与初始sim::WorldState,返回SimulationInstance - Runner:以
SimulationInstance为输入,跑 main loop,调用sim::world_tick - 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<Body> |
| 物理状态原语归属 | 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/)专门承担这种归属。正视真实存在的架构压力,不要藏在某个子域里。