Skip to content

SpatialState & FrameContext 架构设计

Context: 动力学坐标系、空间状态与 FP Monad 的深度融合重构 Date: 2026-04-03


1. 设计哲学与动机

在早期的设计中,CoordinateFrames 类既作为配置加载器,又被塞入 WorldState 中缓存各种中间坐标转换矩阵。这不仅带来了沉重的历史包袱,更破坏了函数式编程(FP)中 “单一事实来源(Single Source of Truth, SSOT)” 的核心理念。同时,各个管线中直接使用原始的 Vec3 导致了严重的坐标系错用风险(例如把体轴系下的力直接加到了惯性系下的速度上)。

为了彻底解决这些问题并深度契合 RWS (Reader-Writer-State) Monad 架构,我们引入了 SpatialStateFrameContext 的设计范式:

  1. 强类型防呆:利用幽灵类型(Phantom Types)包装 Vec3,在编译期杜绝坐标系错乱。
  2. 单一事实来源:状态中仅保留基准惯性系的绝对运动学数据。
  3. 闭包/视图推导:废除状态中缓存的变换矩阵,所有视角的坐标(如气动风轴系、地固系)都在运行时通过 Reader (配置) 和 State (时间、绝对位置) 共同推导(Transient Context)。

2. 核心数据结构

2.1 幽灵类型向量 Vec3_T<Frame>

src/types/Frames.hsrc/types/Vec3_T.h 中,我们定义了零开销的泛型包装:

cpp
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 (发射惯性系) 的绝对状态:

cpp
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

FrameContextDynEnv (Reader Monad) 的一部分,保存纯粹的静态锚点配置(例如发射点的 ECF 坐标、地球自转角速度等)。

当特定的业务(如气动力计算)需要特定坐标系下的数据时,利用 Reader 和 State 生成瞬态的 TransformContext(如 EcefTransformContext),再通过它从 SpatialState 提取目标坐标系的视图 KinematicsView


3. 在 Dynamics Pipeline 中的使用流程

3.1 力的计算 (forces/):以 Drag.cpp 为例

在计算外力时,代码运行在 DynRWS Monad 内。我们要获取气动系/地固系的数据,流程如下:

  1. 获取 Context: 利用 dyn_ask() 获取 Reader (DynEnv),利用 dyn_get() 获取 State (WorldState)。
  2. 提取 View: 通过 env.frame.get_ecef_context(time) 结合当前时间生成 EcefTransformContext,调用 extract_view(body.spatial) 将惯性系绝对状态投影为地固系下的运动学视图。
  3. 强类型运算: 利用提取出的 Vec3_T<frame::ECEF> 计算相对风速,再转换到 BODY 系进行气动系数插值。
cpp
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 产生导数:

cpp
// 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 的纯函数设计,欧拉积分操作被简化为了一行完美的无副作用更新:

cpp
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. 总结与架构意义

  1. 彻底解耦WorldState 再也不用关心“什么是 WGS84,什么是风轴系”。它只负责记录牛顿力学下最纯粹的初始惯性系运动属性。
  2. 符合 RWS 哲学:所有的坐标转换都成了 $View = ContextProvider(Reader, State) \rightarrow Extract(State)$ 的过程。没有任何环境相关的数据污染 State
  3. 可扩展性极强:如果将来 FCC(飞行控制计算机)层需要针对目标落点系进行导航解算,我们只需要在外部新增一个 TargetFrameTargetTransformContext,而无需改动 SpatialState 底层数据分毫。