Assembler & Runner — 生命周期管理
> Status: NEW · 已对齐 PCR Master Blueprint v1.0 §2.7 > 范畴: runtime/{Assembler, Runner, SimulationInstance}.{h,cpp} > 依赖: simulation(唯一) > 被依赖: 各 main/*.cpp
1. 问题陈述
runtime/ 在 v0 中持有了业务定义——WorldEnv 直接定义在 runtime/WorldEnv.h、装载大气模型 lambda、持有 BusMonitor 实例。这违背了"runtime 是纯生命周期管理者"的原则,导致:
- runtime 被强制依赖 plant/bus/environment 的实现细节
- HIL 部署时无法切片(runtime 装的"全部"必须 enter 进程)
- main 函数互相重复装配代码
v1.0 裁定(§7.18):runtime/ 退化为非常薄的一层,只负责:
- 装配(Assembler):读 YAML → 构造冻结的
sim::WorldEnv+ 初始sim::WorldState - 运行(Runner):拥有 main loop,反复调用
sim::world_tick - 退出:析构 RAII 资源、Flush 日志
业务定义全部归 simulation/(WorldEnv/WorldState/RocketBody)或下游业务库(plant/env/bus/...)。
2. SimulationInstance:装配产物
// runtime/SimulationInstance.h
namespace runtime {
struct SimulationInstance {
sim::WorldEnv env; // 装配后冻结(指针稳定)
sim::WorldState state; // 初始 t=0 状态(可被 Runner 演化)
PlantScope scope; // 部署切片
LogConfig log_config; // 日志策略(路径、采样率)
};
} // namespace runtime关键属性:
env是 Reader,构造后 Runner 不再写入。state是 Runner 的循环变量(实际由world_tick演化)。scope在 Runner 构造时决定信道选择(详见IDynChannel_SIL_HIL.md与PlantScope.md)。log_config不属于WorldEnv(业务无关),保留在 SimulationInstance 内由 Runner 解释。
> 反例:把 LogConfig 塞进 sim::WorldEnv 中。 > 正解:LogConfig 是 runtime 的事,不污染业务装配产物。
3. Assembler:装配流水线
3.1 接口
// runtime/Assembler.h
namespace runtime {
class Assembler {
public:
// 唯一入口:从 run_xxx.yaml 装配整个 SimulationInstance
SimulationInstance assemble(const std::string& run_config_path);
private:
// 装配期局部状态(RAII,装配结束析构)
std::unordered_map<std::string, uint32_t> path_index_;
// 阶段函数(按顺序调用)
void load_environment_(const YAMLNode& root, sim::WorldEnv& env);
void load_plant_assets_(const YAMLNode& root, sim::WorldEnv& env);
void instantiate_world_state_(const YAMLNode& root,
const sim::WorldEnv& env,
sim::WorldState& state);
void load_scope_(const YAMLNode& root, PlantScope& scope);
void load_log_config_(const YAMLNode& root, LogConfig& log);
};
} // namespace runtime3.2 装配阶段
| # | 阶段 | 输入 | 输出 | 关键规则 |
|---|---|---|---|---|
| ① | load_environment_ | world/earth.yaml、world/atmosphere.csv | env::Atmosphere/GravityField/WindField 写入 WorldEnv | 大气模型用强类型对象(不是 std::function) |
| ② | load_plant_assets_ | rocket_v1/*.yaml、fcc_v1/aero/*.yaml | plant::model::*Spec vector 写入 WorldEnv.plant_assets | 必须 reserve(N),禁止后续 push_back(指针稳定性) |
| ③ | instantiate_world_state_ | mission.yaml(初始位置/姿态/拓扑) | sim::RocketBody × N,spec 指针指向 ② 的 vector | RocketBody 持有 spec 的非拥有指针(参考 06_Simulation/RocketBody_Composite.md §3) |
| ④ | load_scope_ | sim/run_xxx.yaml: plant_scope | PlantScope 结构 | 选择 dyn/bus 信道类型 |
| ⑤ | load_log_config_ | sim/run_xxx.yaml: logging | LogConfig 结构 | 日志路径、是否按 stage 分流 |
3.3 装配流程图
assemble(run_xxx.yaml):
┌────────────────────────────────────────────────┐
│ A. YAML 解析(io::YAMLReader 全图加载) │
│ ├─ world.yaml (env) │
│ ├─ rocket_v1.yaml (plant assets) │
│ ├─ fcc_v1.yaml (controller assets) │
│ ├─ mission.yaml (initial conditions) │
│ └─ run_xxx.yaml (deployment + logging) │
├────────────────────────────────────────────────┤
│ B. ① load_environment_ │
│ env::Atmosphere atm = parse_atm(...); │
│ env.atmosphere = std::move(atm); │
├────────────────────────────────────────────────┤
│ C. ② load_plant_assets_ │
│ env.plant_assets.engines.reserve(N); │
│ for each engine_yaml: emplace_back(...) │
│ (path_index_ 在此填充,装配结束析构) │
├────────────────────────────────────────────────┤
│ D. ③ instantiate_world_state_ │
│ state.bodies.reserve(M); │
│ for each body in mission: │
│ body.engines[i].spec = &env.assets[k]; │
├────────────────────────────────────────────────┤
│ E. ④ ⑤ load_scope_ + load_log_config_ │
└────────────────────────────────────────────────┘
return SimulationInstance{env, state, scope, log_config}3.4 关键不变量
| 不变量 | 强度 | 理由 |
|---|---|---|
Assembler 返回后 env 不再被 Assembler 修改 | 必须 | Reader 语义;Runner 也必须遵守 |
Assembler 内的 path_index_ 在返回时析构 | 必须 | Blueprint §7.7:path_index 装配期 RAII,禁止持久化 |
env.plant_assets.* vector 容量在 ② 之后冻结 | 必须 | RocketBody 持有的 spec 指针稳定性 |
| Assembler 不创建任何 Bus / DynChannel 实例 | 必须 | 信道实例属于 Runner(生命周期与 main loop 一致) |
Assembler 不创建任何 RocketBody.fcc 内部状态 | 必须 | FCC 实例在 Runner 选定部署后实例化 |
Assembler 不知道 bus::IBus 的具体实现 | 必须 | scope 决定信道,不属于 env |
> 反例:Assembler 内 env.bus = std::make_unique<TransparentBus>(...)。 > 正解:Runner 根据 scope.bus_kind 决定实例化哪种 IBus。
3.5 v0 → v1 迁移摘要
| v0 行为 | v1 行为 |
|---|---|
runtime::assemble_run 返回 WorldEnv(含大气 lambda + bus) | Assembler::assemble 返回 SimulationInstance(env + state + scope + log) |
大气 = std::function<double(double)> | env::Atmosphere 强类型对象(详见 02_Physical_World/Environment_Fields.md) |
BusMonitor 由 Assembler 装配并塞入 env.monitor | BusMonitor 不存在(Writer Monad 内化进 BusLog) |
env.body_assets 是 std::map<string, BodyAsset> | env.plant_assets 是 std::vector<*Spec>(路径稳定 + 索引派发) |
LogConfig 在 WorldEnv 内 | LogConfig 在 SimulationInstance 内 |
4. Runner:main loop
4.1 接口
// runtime/Runner.h
namespace runtime {
class Runner {
public:
explicit Runner(SimulationInstance instance);
~Runner(); // RAII 关闭信道、Flush 日志
int run(); // 阻塞至仿真完成,返回 exit code
private:
SimulationInstance instance_;
std::unique_ptr<bus::IBus> bus_; // scope.bus_kind 决定
std::unique_ptr<IDynChannel> dyn_channel_; // scope.dyn_transport 决定
sim::MultiRateScheduler scheduler_;
LogWriter log_writer_;
};
} // namespace runtime4.2 构造期
Runner::Runner(SimulationInstance inst)
: instance_(std::move(inst))
{
// 1. 根据 scope 实例化信道
bus_ = make_bus(instance_.scope); // InMemory / 1553B / TTE
dyn_channel_ = make_dyn_channel(instance_.scope); // InMemory / Udp
// 2. 调度器
scheduler_.phys_period = instance_.env.scheduler_config.phys_period;
scheduler_.fcc_period = instance_.env.scheduler_config.fcc_period;
// 3. 日志
log_writer_.open(instance_.log_config);
}关键:Runner 是第一个也是唯一一个实例化 bus/dyn_channel 的地方。一旦构造完成,BodyTick 内部对 IBus 的写入会被全局路由(见 06_Simulation/Body_World_Tick.md §3.2 C 阶段)。
4.3 main loop
int Runner::run() {
auto& env = instance_.env;
auto& state = instance_.state;
Time end_time = instance_.env.mission_config.end_time;
while (state.current_time < end_time) {
Time dt = scheduler_.phys_period;
// 调用 simulation 层的 world_tick
auto [wout, new_state, wlog] =
sim::world_tick(dt).run(env, std::move(state));
state = std::move(new_state);
// 日志落地(Writer Monad 输出 → 文件)
log_writer_.write(wlog);
// 信道交互(HIL 模式下 send/recv DynFrame)
if (dyn_channel_) {
dyn_channel_->send(make_dyn_in_frames(wout));
auto recv = dyn_channel_->recv();
apply_dyn_out_frames(state, recv);
}
}
log_writer_.flush();
return 0;
}4.4 关键不变量
| 不变量 | 强度 |
|---|---|
Runner 只调用 sim::world_tick,不直接调用 dynamics::ode::rk4 | 必须 |
| Runner 只在 main loop 之外(构造/析构)实例化信道 | 必须 |
Runner 不修改 env(Reader 语义) | 必须 |
state 是 Runner 的唯一可变载体 | 必须 |
world_tick 的 Writer 输出在 main loop 内立即写盘(不积压) | 强烈建议 |
4.5 v0 → v1 迁移摘要
| v0 行为 | v1 行为 |
|---|---|
closed_loop_10s.cpp 直接调用 PhysicalRegistry::tick | Runner 调 sim::world_tick;PhysicalRegistry 被裁撤 |
| main 函数自己拼装 dyn channel | Runner 构造期通过 scope 装配 |
| BusMonitor 通过 stdout 打印 | bus::BusLog(Writer)累加 → log_writer_.write() 写盘 |
5. 装配 → 运行的完整时序
main(argc, argv):
runtime::Assembler asm;
SimulationInstance inst = asm.assemble("data/input/sim/run_sil_monolithic.yaml");
// ├─ env: 冻结 (Reader)
// ├─ state: 初始拓扑 (RocketBody × N)
// ├─ scope: {dyn=InMemory, bus=InMemory, fcc=true, physics=true}
// └─ log_config: paths + filters
runtime::Runner runner(std::move(inst));
// ├─ 实例化 InMemoryDynChannel
// ├─ 实例化 InMemoryBus
// ├─ scheduler 配置(phys=1kHz, fcc=50Hz)
// └─ 打开日志文件
return runner.run();
// while (t < end): world_tick(dt); log; channel io
// 析构: 信道关闭,日志 flush详细每个阶段产物见 IDynChannel_SIL_HIL.md 与 PlantScope.md。
6. 反模式
| 反模式 | 后果 | 正确做法 |
|---|---|---|
Assembler 内创建 bus::TransparentBus 并塞 env | 业务定义污染 runtime;HIL 切换困难 | 信道由 Runner 构造期根据 scope 创建 |
WorldEnv 内包含 LogConfig | 业务装配与日志策略耦合;HIL 节点之间需共享 env 但日志策略不同 | LogConfig 在 SimulationInstance 内 |
path_index_ 升格为 WorldEnv::path_index 字段 | 装配期局部状态泄漏到 Reader;冻结失败 | 装配期 RAII(unordered_map 成员),结束析构 |
Runner 直接调 dynamics_core::rk4 | 绕过 simulation 的 tick 编排;事件路由失效 | 必须走 sim::world_tick |
多个 main_*.cpp 各自重复装配代码 | 漂移;测试不一致 | 统一走 Assembler;变化的只是 run_xxx.yaml |
装配后 vector push_back 新 spec | 指针失效;segfault | reserve(N) 在装配开头;之后只 emplace_back 不超容量 |
| Runner 在 main loop 内 reopen 日志 | 多次开关文件 IO;性能崩溃 | open 在构造,flush 在析构 |
Assembler 返回多个对象(std::pair<env, state>) | 接口僵硬;增加 scope 字段需要改全部 caller | 统一 SimulationInstance 结构 |
7. C-Distillation 路径
| C++ 抽象 | C 蜕化 |
|---|---|
SimulationInstance 结构体 | C struct(plain data) |
Assembler::assemble 阶段函数 | 静态 init array:init_env[], init_assets[], init_state[] |
std::unique_ptr<bus::IBus> | bus_kind enum + 静态 dispatch table(函数指针表) |
std::unique_ptr<IDynChannel> | dyn_transport enum + 静态 dispatch table |
path_index_ unordered_map | 编译期 string→uint 表(codegen by Python) |
| YAML parse(runtime) | offline codegen:YAML → header constexpr 数组 |
Runner::run while loop | 裸 RTOS task:while (t < end) world_tick_c(...) |
参见 09_Cross_Cutting/C_Distillation.md(待写)与 Blueprint §7.12。
8. 测试策略
8.1 单元层
Assembler::assemble给定一组小 YAML,验证env.atmosphere.density_at(0)与期望一致path_index_在assemble返回后无法访问(编译期作用域)
8.2 组件层
Assembler×run_sil_monolithic.yaml:验证state.bodies.size() == mission 配置 body 数、每个 body 的 spec 指针非空且指向 env 内Runner::run(1 步):验证 state 在 dt 后推进 dt 时间
8.3 集成层
- 三种部署 YAML(sil_monolithic / hil_dyn / hil_fcc)都能成功
Assembler::assemble + Runner ctor(不实际 run) - 1s 平凡仿真:Assembler + Runner.run() 退出码 0,日志文件非空
详见 08_Cross_Cutting/Testing_Framework.md。
9. 引用
- Blueprint §2.7(Runtime:生命周期管理)、§7.18(runtime 退化裁定)、§7.7(WorldEnv 严格只读 + path_index 装配期 RAII)、§7.15(Environment 与 Plant 严格分离)、§2.8(PlantScope)
06_Simulation/WorldEnv_Assembly.md(被 Assembler ② ③ 调用的装配函数)06_Simulation/Body_World_Tick.md(Runner.run 主循环调用的 world_tick)07_Runtime/IDynChannel_SIL_HIL.md(信道实例由 Runner 构造)07_Runtime/PlantScope.md(scope 决定信道选择)07_Runtime/PCR_Configuration.md(YAML 文件结构)