SpatialState & FrameContext 架构设计
Context: 动力学坐标系、空间状态与 FP Monad 的深度融合重构 Date: 2026-04-03
1. 设计哲学与动机
在早期的设计中,CoordinateFrames 类既作为配置加载器,又被塞入 WorldState 中缓存各种中间坐标转换矩阵。这不仅带来了沉重的历史包袱,更破坏了函数式编程(FP)中 “单一事实来源(Single Source of Truth, SSOT)” 的核心理念。同时,各个管线中直接使用原始的 Vec3 导致了严重的坐标系错用风险(例如把体轴系下的力直接加到了惯性系下的速度上)。
为了彻底解决这些问题并深度契合 RWS (Reader-Writer-State) Monad 架构,我们引入了 SpatialState 与 FrameContext 的设计范式:
- 强类型防呆:利用幽灵类型(Phantom Types)包装
Vec3,在编译期杜绝坐标系错乱。 - 单一事实来源:状态中仅保留基准惯性系的绝对运动学数据。
- 闭包/视图推导:废除状态中缓存的变换矩阵,所有视角的坐标(如气动风轴系、地固系)都在运行时通过
Reader(配置) 和State(时间、绝对位置) 共同推导(Transient Context)。
2. 核心数据结构
2.1 幽灵类型向量 Vec3_T<Frame>
在 src/types/Frames.h 和 src/types/Vec3_T.h 中,我们定义了零开销的泛型包装:
namespace frame {
struct LIC {}; // Launch Inertial Coordinate System (发射惯性系)
struct ECEF {}; // Earth-Centered, Earth-Fixed (地固系)
struct AERO {}; // Aerodynamic (气动系)
struct BODY {}; // Rocket Body (本体系)
}
template<typename Frame>
struct Vec3_T {
Vec3 val;
// 重载 + - * /,确保只有相同 Frame 的向量才能运算
};2.2 唯一事实来源 SpatialState
RocketBody 不再零散地保存各类向量,而是统一收拢在 SpatialState 中。它永远只保存基于 LIC (发射惯性系) 的绝对状态:
class SpatialState {
private:
Vec3_T<frame::LIC> pos_lic_;
Vec3_T<frame::LIC> vel_lic_;
Quat att_lic_to_body_;
Vec3_T<frame::BODY> omega_body_;
public:
// 纯函数方法:应用导数,返回全新的 SpatialState
SpatialState apply_derivative(const SpatialDerivative& deriv, double dt) const;
};2.3 静态环境 FrameContext 与瞬态视图 TransformContext
FrameContext 是 DynEnv (Reader Monad) 的一部分,保存纯粹的静态锚点配置(例如发射点的 ECF 坐标、地球自转角速度等)。
当特定的业务(如气动力计算)需要特定坐标系下的数据时,利用 Reader 和 State 生成瞬态的 TransformContext(如 EcefTransformContext),再通过它从 SpatialState 提取目标坐标系的视图 KinematicsView。
3. 在 Dynamics Pipeline 中的使用流程
3.1 力的计算 (forces/):以 Drag.cpp 为例
在计算外力时,代码运行在 DynRWS Monad 内。我们要获取气动系/地固系的数据,流程如下:
- 获取 Context: 利用
dyn_ask()获取 Reader (DynEnv),利用dyn_get()获取 State (WorldState)。 - 提取 View: 通过
env.frame.get_ecef_context(time)结合当前时间生成EcefTransformContext,调用extract_view(body.spatial)将惯性系绝对状态投影为地固系下的运动学视图。 - 强类型运算: 利用提取出的
Vec3_T<frame::ECEF>计算相对风速,再转换到BODY系进行气动系数插值。
static DynRWS<Vec3> calculate_flow_in_body() {
return calculate_wind() >>= [](const Vec3& wind_in_launch) {
return dyn_ask() >>= [&wind_in_launch](const DynEnv& env) {
return dyn_get() >>= [&wind_in_launch, &env](const WorldState& w) {
const RocketBody& body = w.bodies[w.active_body_index];
Time time = w.clock.current_time;
// 1. 从静态配置(Reader)生成该时刻的瞬态转换上下文
auto ecef_ctx = env.frame.get_ecef_context(time);
// 2. 将单一事实来源的绝对状态提取为 ECEF 视图
auto ecef_view = ecef_ctx.extract_view(body.spatial);
// 3. 相对速度计算 (安全)
Vec3 velocity_in_launch = ecef_view.velocity.val;
Vec3 flow_in_launch = wind_in_launch - velocity_in_launch;
// ... 后续转换到 BODY 系返回
return dyn_pure(flow_in_body);
};
};
};
}3.2 微分方程解算 (ode/EquationsOfMotion.cpp)
所有计算出的气动力、推力都会汇聚为本体系 BODY 下的合力(Forces 结构体)。微分方程解算器需要将体轴系的力学结果投影回基准系 LIC 产生导数:
// 1. 体轴系比力
Vec3 spec_force_b = forces.total_force_b / body.mass;
// 2. 利用 SpatialState 中的四元数进行基准系反投射 (BODY -> LIC)
// Quat 保存的是 LIC -> BODY,因此使用 conjugate() 反向旋转
Vec3 spec_force_lic = body.spatial.attitude().conjugate().rotate(spec_force_b);
// 3. 构造强类型空间导数
eq.deriv.spatial_deriv.d_velocity = Vec3_T<frame::LIC>(spec_force_lic + gravity_lic);
eq.deriv.spatial_deriv.d_position = body.spatial.velocity();
eq.deriv.spatial_deriv.d_omega = Vec3_T<frame::BODY>(d_omega);
// 四元数导数更新
Quat q = body.spatial.attitude();
Quat w_quat(0, eq.aux.omega_b.x(), eq.aux.omega_b.y(), eq.aux.omega_b.z());
Quat dq = q * w_quat;
dq.w *= 0.5; dq.x *= 0.5; dq.y *= 0.5; dq.z *= 0.5;
eq.deriv.spatial_deriv.d_attitude = dq;3.3 状态积分 (ode/Integrator.cpp)
积分器是 Stateful 的算子。得益于 SpatialState 的纯函数设计,欧拉积分操作被简化为了一行完美的无副作用更新:
DynRWS<std::monostate> integrate(const DiffEq& eq, double dt) {
return dyn_get() >>= [eq, dt](const WorldState& w) {
WorldState next_state = w;
RocketBody& body = next_state.bodies[next_state.active_body_index];
// 核心:极简的函数式运动学积分更新
body.spatial = body.spatial.apply_derivative(eq.deriv.spatial_deriv, dt);
body.mass = body.mass + eq.deriv.d_mass * dt;
// ... (燃料消耗等更新)
return dyn_put(next_state);
};
}4. 总结与架构意义
- 彻底解耦:
WorldState再也不用关心“什么是 WGS84,什么是风轴系”。它只负责记录牛顿力学下最纯粹的初始惯性系运动属性。 - 符合 RWS 哲学:所有的坐标转换都成了 $View = ContextProvider(Reader, State) \rightarrow Extract(State)$ 的过程。没有任何环境相关的数据污染
State。 - 可扩展性极强:如果将来 FCC(飞行控制计算机)层需要针对目标落点系进行导航解算,我们只需要在外部新增一个
TargetFrame和TargetTransformContext,而无需改动SpatialState底层数据分毫。