WorldEnv Assembly & Probe Descent
> Status: NEW · 已对齐 PCR Master Blueprint v1.0 > 范畴: simulation/env/, simulation/probe/ > 依赖: environment/{Atmosphere, GravityField, WindField}, plant/model/*Spec, contracts/*Id, frames/ > 被依赖: simulation/pipeline/, runtime/Assembler
1. 问题陈述
仿真需要两类只读上下文:
- WorldEnv(全局):装配后冻结,整个仿真生命周期共享——atmosphere、gravity、wind、所有 *Spec 表、frame config。
- BodyEnv(局部):每个 tick 为每个 body 现场降维——TrajCtx + AeroCtx + MassPropsCtx + 资产指针。
这两类的设计核心都是一句话:"配方与流水线分离"。
- 算子(thrust、aero、gravity 计算)不直接读 WorldEnv 大表。
- 算子只读 BodyEnv 的扁平字段(如
static_pressure: double)。 - 换大气模型只动
probe_aero函数,pipeline 一行不改。
本节描述:装配(一次性)+ 降维(每 tick)+ 不变量。
2. WorldEnv:全局只读资产库
2.1 类型布局
// simulation/env/WorldEnv.h
namespace sim {
struct WorldEnv {
// ── 全局常量与坐标系(来自 frames/ + types/) ──
frame::FrameConfig frame; // ECF / LIC 锚点、地球自转
Constants constants; // GM, J2, R_earth, etc.
// ── 普适物理场(来自 environment/) ──
env::Atmosphere atmosphere; // pressure_at(h), density_at(h), ...
env::GravityField gravity_field; // g(pos_ecf)
env::WindField wind_field; // wind(pos_ecf, t)
// ── 对象本构资产(来自 plant/model/,按 type 索引) ──
std::vector<plant::model::BodyAsset> body_assets;
std::vector<plant::model::EngineSpec> engine_specs;
std::vector<plant::model::ServoSpec> servo_specs;
std::vector<plant::model::FinSpec> fin_specs;
std::vector<plant::model::ImuSpec> imu_specs;
std::vector<plant::model::GpsSpec> gps_specs;
std::vector<plant::model::IcuSpec> icu_specs;
std::vector<plant::model::AeroSpec> aero_specs;
// 查找接口
const plant::model::BodyAsset& get_asset(contracts::BodyId id) const;
};
} // namespace sim> WorldEnv 严格只读。它只持有 spec 数据(配方),不持有运行时实例。所有 device instance 属于 RocketBody(详见 RocketBody_Composite.md)。
2.2 强制属性
| 属性 | 强制理由 |
|---|---|
| 装配后不可修改 | spec 指针稳定性约束(详见 §6);多线程读安全 |
| 不持有 RocketBody / WorldState | 单源;State 与 Env 严格分离(RWS Reader 不可变) |
| 不持有 IBus / FCC instance | bus 在 runtime/Runner;FCC 在 RocketBody.fcc |
| 不持有 path_index 等装配期临时表 | path_index 在 Assembler 局部 RAII,装配结束析构(Blueprint §2.6.2 注) |
| 跨域可包含 environment + plant 头文件 | Blueprint §2.6 判据:simulation/ 是唯一合法跨域聚合层 |
2.3 与旧 dynamics::WorldEnv 的区别
| 维度 | v0(runtime::WorldEnv) | v1(sim::WorldEnv) |
|---|---|---|
| 归属 | runtime/ | simulation/env/ |
| 持有 atmosphere | std::function<double(double)> 句柄 | env::Atmosphere 强类型对象 |
| 持有 spec | std::map<string, BodyAsset> | std::vector<...Spec> + 按 type_id 索引 |
| 持有 bus | 是(含 bus、bus_cycle) | 否(bus 在 runtime) |
| 持有 path_index | 是 | 否(装配期局部) |
| LogConfig | 是 | 移走到 runtime 或 simulation/log/ |
详见 dev_doc/Refactoring/PCR_Master_Blueprint.md §7.16 迁移条目。
3. BodyEnv:单 body 局部 Reader
3.1 类型布局
// simulation/env/BodyEnv.h
namespace sim {
struct BodyEnv {
// ── 当前 Body 的局部上下文切片(probe 产物) ──
TrajCtx traj; // 经纬高、ECF 位置、速度方位角
AeroCtx aero; // static_pressure, density, mach, dyn_pressure, wind_b
MassPropsCtx mass_props; // 当前 mass / centroid / inertia tensor
// ── 该 Body 引用的资产(非拥有指针,指向 WorldEnv) ──
const plant::model::BodyAsset* asset;
const plant::model::AeroSpec* aero_spec;
// ── 全局只读引用(指向 WorldEnv 字段) ──
const env::Atmosphere* atmosphere;
const env::GravityField* gravity_field;
const env::WindField* wind_field;
// ── 上下文 ──
Time current_time;
};
} // namespace sim3.2 设计原则
| 原则 | 解释 |
|---|---|
| 算子只读 BodyEnv 扁平字段 | compute_thrust 读 env.aero.static_pressure,不读 env.atmosphere->pressure_at(h) |
| probe 是唯一的"模型耦合点" | 换大气模型只动 probe_aero;pipeline 不动 |
| 指针稳定 | 所有指针指向 WorldEnv 字段,WorldEnv 生命周期 ≥ BodyEnv |
| 栈对象 | BodyEnv 是 BodyTick 入口的栈对象,tick 结束自动析构 |
| 不持有可变状态 | BodyEnv 不含任何 mutable 字段;所有可变态在 RocketBody(BodyRWS 的 State) |
4. probe:双层 RWS 之间的降维算子
4.1 函数签名
// simulation/probe/Probe.h
namespace sim::probe {
TrajCtx probe_traj (const WorldEnv&, const RocketBody&, Time t);
AeroCtx probe_aero (const WorldEnv&, const RocketBody&, Time t);
MassPropsCtx probe_mass_props(const WorldEnv&, const RocketBody&, Time t);
// 一次性装配整个 BodyEnv
BodyEnv assemble_body_env(const WorldEnv&, const RocketBody&, Time t);
} // namespace sim::probe这四个函数定义了双层 RWS 的"降维接口":World 层只读全局 → Body 层只读局部。
4.2 probe_traj:弹道上下文
TrajCtx probe_traj(const WorldEnv& we, const RocketBody& body, Time t) {
Vec3_T<frame::LIC> pos_lic = body.spatial.pos_lic;
Vec3_T<frame::ECF> pos_ecf = we.frame.lic_to_ecf(pos_lic, t);
auto [lat, lon, h_msl] = we.frame.ecf_to_lla(pos_ecf);
Vec3_T<frame::ECF> vel_ecf = we.frame.lic_to_ecf_vel(body.spatial.vel_lic, pos_lic, t);
return TrajCtx {
.pos_ecf = pos_ecf,
.vel_ecf = vel_ecf,
.lat = lat,
.lon = lon,
.h_msl = h_msl,
.speed = vel_ecf.norm(),
.azimuth = compute_azimuth(vel_ecf, lat, lon),
};
}4.3 probe_aero:气动上下文
AeroCtx probe_aero(const WorldEnv& we, const RocketBody& body, Time t) {
double h = body.spatial.h_msl(); // 经纬高
double pressure = we.atmosphere.pressure_at(h);
double density = we.atmosphere.density_at(h);
Vec3_T<frame::ECF> wind_ecf = we.wind_field.wind_at(body.spatial.pos_ecf, t);
Vec3_T<frame::BODY> wind_b = we.frame.ecf_to_body(wind_ecf, body.spatial.q_lic_body);
Vec3_T<frame::BODY> vel_b = we.frame.lic_to_body(body.spatial.vel_lic, body.spatial.q_lic_body);
Vec3_T<frame::BODY> v_rel_b = vel_b - wind_b;
double v_squared = v_rel_b.dot(v_rel_b);
double speed_of_sound = std::sqrt(1.4 * pressure / density); // 理想气体近似
double mach = v_rel_b.norm() / speed_of_sound;
return AeroCtx {
.static_pressure = pressure,
.density = density,
.mach = mach,
.dyn_pressure = 0.5 * density * v_squared,
.v_rel_b = v_rel_b,
.wind_b = wind_b,
};
}> 核心:这里集中了所有跨域查表逻辑。plant::physics::compute_thrust(BodyEnv, RocketBody) 只看 env.aero.static_pressure,不知道 we.atmosphere 是 env::Atmosphere 的什么形态——指数模型、US Standard 1976、CFD 表格都一样。
4.4 probe_mass_props:质量上下文
MassPropsCtx probe_mass_props(const WorldEnv& we, const RocketBody& body, Time t) {
double total_mass = body.inertial.mass; // 当前积分质量
Vec3 centroid_b = body.inertial.centroid;
Matrix inertia_b = body.inertial.inertia;
return MassPropsCtx {
.total_mass = total_mass,
.centroid_b = centroid_b,
.inertia_b = inertia_b,
};
}> 这个 probe 看起来只是搬运,但它的存在是接口契约——未来若引入 plumes、燃料晃动、外挂物分离,MassPropsCtx 字段会扩展,pipeline 算子签名稳定。
4.5 assemble_body_env:一次性装配
BodyEnv probe::assemble_body_env(const WorldEnv& we, const RocketBody& body, Time t) {
return BodyEnv {
.traj = probe_traj(we, body, t),
.aero = probe_aero(we, body, t),
.mass_props = probe_mass_props(we, body, t),
.asset = &we.get_asset(body.body_id),
.aero_spec = &we.aero_specs[body_type_to_aero_spec_idx(body)],
.atmosphere = &we.atmosphere,
.gravity_field = &we.gravity_field,
.wind_field = &we.wind_field,
.current_time = t,
};
}在 BodyTick 入口调用一次,整个 BodyTick 后续阶段共享这个 BodyEnv 栈对象。
5. WorldEnv 装配流程
WorldEnv 由 runtime::Assembler::assemble() 在仿真启动时一次性装配。流程:
data/input/run_xxx.yaml
├── frame: <FrameConfig>
├── environment: atmosphere_model: "us_standard_1976"
│ gravity_model: "j2"
│ wind_model: "hwm14"
├── rocket: <rocket_yaml_path>
├── mission: <mission_yaml_path>
└── deployment: <PlantScope>
│
▼
runtime::Assembler::assemble()
│
├── load_environment_(run_yaml)
│ → we.frame, we.constants, we.atmosphere, we.gravity_field, we.wind_field
│
├── load_plant_assets_(rocket_yaml)
│ │ 对每个 device 类型:
│ │
│ ├── parse devices.yaml
│ ├── for each spec_path in devices.yaml.engines:
│ │ EngineSpec spec = load_engine_spec_yaml(spec_path);
│ │ spec.tables = load_csv_tables(spec.csv_paths);
│ │ we.engine_specs.push_back(std::move(spec));
│ │
│ └── 类似处理 servo_specs / fin_specs / imu_specs / gps_specs / icu_specs / aero_specs
│
└── (此处 we 已完成,进入 instantiate_world_state_,详见 RocketBody_Composite.md §7)5.1 装配期局部状态
class Assembler {
private:
std::unordered_map<std::string, uint32_t> path_index_;
// ^^^^ 路径字符串 → spec 数组下标,仅装配期使用
// RAII:Assembler 析构时此表消失
};> 旧设计把 path_index 塞进 WorldEnv 是错的——它是装配过程的临时索引,不是运行时数据。Blueprint §2.6.2 明确:装配结束 RAII 析构。
详见 07_Runtime/Assembler.md(待写)。
6. 指针稳定性约束
RocketBody.engines[i].spec、BodyEnv.atmosphere 等都是非拥有指针,必须保证:
| 约束 | 实现 |
|---|---|
WorldEnv 装配后 vector 不 realloc | 装配阶段 reserve(N);装配结束后禁止 push_back |
WorldEnv 生命周期 ≥ WorldState 生命周期 | runtime::SimulationInstance 同时持有,WorldEnv 先构造、后析构 |
WorldEnv 装配后 冻结(移动到 const&) | Assembler::assemble() 返回 SimulationInstance 后 WorldEnv 仅以 const& 暴露 |
| 跨线程读 safe | 所有字段 const 后多线程共享 |
检查点:
- 编译期:
WorldEnv不暴露 mutable getter。 - 装配期:
Assembler::finalize()把所有 vector 转 const(或调用shrink_to_fit)。 - 运行时:debug build 在 BodyTick 入口断言
body.engines[i].spec != nullptr。
> 反例:在仿真运行中 we.engine_specs.push_back(...) —— 旧指针全部失效,是 use-after-realloc UB。Wave 0 必须删除任何运行时修改 WorldEnv 的代码路径。
7. probe 设计准则
7.1 probe 是纯函数
AeroCtx probe_aero(const WorldEnv&, const RocketBody&, Time t);
// ^^^^^^^^^ const& const& value
// 所有输入只读,无副作用,无状态。强约束:
- 不可在 probe 中修改 RocketBody 或 WorldEnv。
- 不可在 probe 中分配堆内存(probe 在热循环 → cache 友好 + 无 GC pressure)。
- 不可在 probe 中调用 IO(log/println/读文件)。
- 可以读
const&字段、查表、做数学。
7.2 probe 字段必须扁平
// ✗ 错:把 std::function 塞进 BodyEnv
struct AeroCtx_BAD {
std::function<double(double)> pressure_func; // 接口耦合
};
// ✓ 对:probe 在降维时**调用一次** atmosphere->pressure_at(h),得到 double
struct AeroCtx {
double static_pressure;
};理由:算子拿到的应该是数值(pressure: 100kPa),不是接口对象(pressure_func)。这是"配方与流水线分离"的本质。
7.3 字段扩展指南
引入新物理量时:
- 决定字段归属:弹道(TrajCtx)/ 气动(AeroCtx)/ 质量(MassPropsCtx)/ 还是新建 ctx?
- 在 ctx 中加字段,类型必须是扁平 POD。
- 在
probe_*中填充计算。 - 算子按需读取。不需要的算子不感知新字段。
> 不要 创建跨 ctx 字段(如"既属于 TrajCtx 又属于 AeroCtx")。如果一个字段被多个算子需要但语义跨域,把它放到最贴近物理意义的 ctx,或新建一个 ctx(如 RelativeFlowCtx)。
详见 02_Physical_World/Environment_Fields.md(atmosphere/gravity/wind 字段定义)。
8. 不变量与契约
| 契约 | 强度 | 检查点 |
|---|---|---|
| WorldEnv 装配后只读 | 必须 | API 上 const&;debug 断言 size 不变 |
| BodyEnv 是栈对象,BodyTick 出口析构 | 必须 | 不允许 BodyEnv 跨 tick 持久化 |
| probe 是纯函数 | 必须 | 函数签名 + code review |
| spec 指针非空 | 必须 | BodyTick 入口断言 |
| AeroCtx 字段全部扁平 POD | 必须 | type check |
| 时间一致:probe(t) → BodyTick 内部所有计算都用同一个 t | 必须 | probe 把 t 写进 BodyEnv.current_time |
| 同 tick 内 probe 只调用一次 | 设计 | BodyTick 入口 once |
9. 与三种部署模式的适配
| 部署 | WorldEnv 装配 | BodyEnv probe |
|---|---|---|
| avionics_dry(FCC 模飞) | 完整装配 | 物理冻结,但 probe 正常跑(返回固定值) |
| sil_monolithic(默认 SIL) | 完整装配 | 每 tick 实时 probe |
| hil_dyn(物理本机 + FCC 远程) | 完整装配 | 同 SIL |
| hil_fcc(FCC RTOS + Devices 仿真) | Plant 端装配 simulation Env;FCC 端不装配 WorldEnv | FCC 端无 BodyEnv(FCC 只看 FccInFrame) |
> 关键:HIL 部署时,FCC 在另一台机器上根本不知道 WorldEnv / BodyEnv 的存在。FCC 只看 FccInFrame(IMU/GPS payload)。这正是双层 RWS + simulation/ 跨域归属设计的目的:FCC 严格隔离。
详见 04_FCC/Free_Monad_DSL.md §6(FCC 不依赖 simulation/)。
10. 反模式
| 反模式 | 后果 | 正确做法 |
|---|---|---|
把 std::function / 闭包塞进 WorldEnv.atmosphere | 不可序列化;调试困难;接口耦合 | 用强类型 env::Atmosphere + 字段 |
| 装配后修改 WorldEnv(push_back spec) | 指针失效 UB | 装配后冻结 |
| 把 path_index 持久化进 WorldEnv | 运行时累赘;语义混淆 | 装配期局部 RAII |
| 在 BodyEnv 持有 mutable 字段 | Reader 不可变假设破产 | BodyEnv 全 const POD |
算子直接调 we.atmosphere.pressure_at(h) | pipeline 跨域;换模型要改算子 | 算子读 env.aero.static_pressure,probe 负责调 |
| probe 在 BodyTick 中段调用(而非入口) | 不同算子看到的环境时间不一致 | probe 在 BodyTick 入口 once |
在 WorldEnv 持有 bus::IBus* | 越层;bus 是 runtime 设施 | bus 在 runtime::Runner |
在 WorldEnv 持有运行时 RocketBody 实例 | Env / State 不分 | RocketBody 在 WorldState |
11. C-Distillation 路径
| C++ 抽象 | C 蜕化 |
|---|---|
sim::WorldEnv struct | 一个静态全局 const struct,编译期初始化 |
std::vector<EngineSpec> | 静态数组 + 编译期上限 |
env::Atmosphere(虚函数) | C 函数指针表 + plain data |
assemble_body_env 栈对象 | C 函数填充栈 struct |
probe_aero | 等价 C 函数(已经基本是纯函数) |
Constants map | #define GM 3.986e14 等编译期常量 |
详见 09_Cross_Cutting/C_Distillation.md。
12. 测试策略
12.1 单元层
probe_aero(known we, known body, t)→ 验证字段数值(手算或参考实现对比)。probe_traj验证 lat/lon/h 与 ECF/LIC 双向转换。- 极端高度(h=0, h=100km, h<0)边界。
12.2 组件层
- Assembler 装配测试:YAML → WorldEnv,验证
engine_specs.size()、字段值。 - 指针稳定性:装配后查询
we.engine_specs[0]地址,再装配第二轮,地址应为新对象。 assemble_body_env字段填充完整性。
12.3 集成层
- 整循环 SIL 1s,验证 RocketBody 演化 → 验证 probe 在不同高度返回正确大气值。
详见 08_Verification/Test_Strategy.md。
13. 引用
- Blueprint §2.6.2(WorldEnv 环境装配)、§2.6.3(Probe)、§4.3(probe_aero 与算子解耦实例)
Dual_Layer_RWS.md§4(probe 作为降维算子的位置)RocketBody_Composite.md§4(spec 指针所有权契约)Body_World_Tick.md§2(BodyTick 入口调用 probe)02_Physical_World/Environment_Fields.md(Atmosphere/GravityField/WindField 字段)02_Physical_World/Plant_Model_Assets.md(*Spec 来源与 YAML 格式)07_Runtime/Assembler.md(装配流程实现,待写)