Skip to content

Forces Monoid

> Aligned with PCR Master Blueprint v1.0 — see Blueprint §2.5(dynamics_core pipeline)、§4.1(Stage 5 Forces Monoid 累加)。 > 职责:本文定义 Forces —— 力 / 力矩 / 质量流率的 Monoid 容器。所有 plant/physics 力源(推力 / 气动 / 重力 / 控制扰流 / ...)通过 Monoid 加法累加,最终交给 dynamics_core::ode::integrate_rk4 推进状态。 > 核心约束Forces纯数据,归属 dynamics_core/pipeline/;它本身不知道力来自哪里,也不调用任何力学计算。 > C-Distillation noteForces 是 PoD;operator+ / operator+= 是 6-字段 element-wise;蒸馏后没有变化。


1. Monoid 的工程意义

物理引擎里的"合力"在数学上是力的加法群——可以零、可以叠加、加法满足结合律。把它建模为 Monoid 让管线获得三个工程性质:

Monoid 性质工程后果
Identity(Forces::zero()起点不需要"特殊第一个力源"——pipeline 从 Forces::zero() 开始
Associativity((a+b)+c == a+(b+c)力源可以任意顺序并行累加;多线程汇总不需要 mutex
Closure(Forces + Forces == Forces累加结果仍是 Forces,可继续传给下游

这与 DynLog(Writer Monoid)、FccLog(Writer Monoid)的设计完全同构——Forces 是 State 通道的 Monoid,DynLog 是 Writer 通道的 Monoid。整套 RWS pipeline 由此统一。


2. 类型定义(实际代码)

来自 src/dynamics/state/Forces.h(v1 目标位置:src/dynamics_core/pipeline/Forces.h):

cpp
// dynamics_core/pipeline/Forces.h
namespace dynamics {

struct Forces {
    Vec3   total_force_b;   // 本体系总力 (N)
    Vec3   total_moment_b;  // 本体系总力矩 (N·m)
    Vec3   gravity_lic;     // LIC 系重力加速度通道 (N),独立通道

    double d_mass_dt      = 0;   // 总质量流率 (kg/s),负值=减少
    double d_fuel_dt      = 0;
    double d_oxidizer_dt  = 0;

    // Monoid Identity
    static Forces zero() {
        return Forces{Vec3(), Vec3(), Vec3(), 0, 0, 0};
    }

    // Monoid Binary Operation
    Forces operator+(const Forces& other) const {
        return {
            total_force_b   + other.total_force_b,
            total_moment_b  + other.total_moment_b,
            gravity_lic     + other.gravity_lic,
            d_mass_dt       + other.d_mass_dt,
            d_fuel_dt       + other.d_fuel_dt,
            d_oxidizer_dt   + other.d_oxidizer_dt
        };
    }

    Forces& operator+=(const Forces& other) { *this = *this + other; return *this; }
};

}

六个字段,六个独立的加法通道。


3. 字段拆解

3.1 total_force_btotal_moment_b:体系合力 / 合力矩

体系(BODY)下的合力与合力矩。所有非重力体力(推力、气动、控制扰流、分离冲击)累加到这里。

为什么用 BODY 系?

  • 推力天然在 BODY 系(推力线沿喷管轴)
  • 气动力天然在 BODY 系(升力 / 阻力定义基于本体迎角与姿态)
  • 转动惯量在 BODY 系常对角化(绕主惯性轴)

最终在 compute_velocity_derivative 内通过 att_lic_to_body.conjugate() 投到 LIC 系积分。

3.2 gravity_lic:独立的 LIC 重力通道

关键设计:重力total_force_b,而是有独立字段。

原因

  1. 重力在 LIC 系是天然标量场(g = GM/r² 沿径向)
  2. 把重力先投到 BODY 系再投回 LIC 会引入两次旋转的数值误差
  3. IMU 真值需要"比力"(specific force = (total_force_b/m) - gravity_b)——把 gravity 分开通道使 compute_aux_instant 能直接取 total_force_b/mass,不必反推

详细推导见 Universal_ODE_Kernel.md §4.2 compute_velocity_derivative

3.3 d_mass_dt / d_fuel_dt / d_oxidizer_dt:质量流率

发动机点火期间,燃料 + 氧化剂以质量流率 消耗。三个独立通道:

  • d_fuel_dtd_oxidizer_dt:分别记录两种推进剂消耗
  • d_mass_dt:总流率 = 燃料 + 氧化剂 + (未来扩展,如冷却剂排放)

> 当前实现中 d_mass_dtcompute_thrust_contribution 直接填,与燃料/氧化剂之和应保持一致(但 Monoid 加法不强制——这是约定,不是类型约束)。

为什么分开?因为推进剂质量不同 ↔ 重心位置 / 转动惯量不同,下游 MassProps 计算需要分通道。


4. Pipeline 累加:BodyRWS<Forces> 组合

4.1 RWS 别名

cpp
// dynamics_core/pipeline/RWSAlias.h
template &lt;typename A&gt;
using BodyRWS = monad::RWS&lt;BodyEnv, DynLog, RocketBody, A&gt;;

> ⚠️ BodyEnv / RocketBody 本身定义在 dynamics_core/——它们是跨域复合体,归 simulation/state/dynamics_core/pipeline/RWSAlias.h 只做模板别名,不 include 这些跨域类型的定义;具体的 BodyRWS 实例化发生在 simulation/pipeline/

4.2 力源 contribution 的统一签名

每个 plant 力源都返回 BodyRWS&lt;Forces&gt;

cpp
// plant/physics/Thrust.h
BodyRWS&lt;Forces&gt; compute_thrust_contribution(const std::vector&lt;EngineEffect&gt;& effects);

// plant/physics/Drag.h
BodyRWS&lt;Forces&gt; compute_aero_contribution(const DynInFrame& in);

// plant/physics/Gravity.h
BodyRWS&lt;Forces&gt; compute_gravity_contribution();   // 填 gravity_lic 通道

契约

  • 每个函数返回仅本力源贡献Forces(其他字段为 zero)
  • 函数内部从 BodyEnvAeroCtx / TrajCtx / MassPropsCtx
  • 函数内部通过 dyn_tell()DynLog
  • 函数不修改 RocketBody(不调用 body_put

4.3 累加:fluent 风格

来自 dynamics/AGENTS.md 的约定:

cpp
// simulation/pipeline/BodyTick.cpp
BodyRWS&lt;Forces&gt; compute_total_forces(const std::vector&lt;EngineEffect&gt;& thrust,
                                       const DynInFrame& dyn_in) {
    return forces_pure(Forces::zero())
        &gt;&gt; compute_thrust_contribution(thrust)
        &gt;&gt; compute_aero_contribution(dyn_in)
        &gt;&gt; compute_gravity_contribution();
}

operator&gt;&gt;monad/RWS.h 内为 BodyRWS&lt;Forces&gt; 重载:

cpp
template &lt;typename A&gt;
BodyRWS&lt;A&gt; operator&gt;&gt;(BodyRWS&lt;A&gt; lhs, BodyRWS&lt;A&gt; rhs) {
    // 当 A 是 Monoid 时(如 Forces / DynLog),lhs 与 rhs 的 A 通道按 Monoid 加法合并
    // State 通道(RocketBody)按 lhs.state → rhs(顺序传递)
    // Writer 通道(DynLog)按 Monoid 加法合并
    return /* ... */;
}

> 注意:compute_*_contribution 的 State 通道(RocketBody)只读——它们不写回 body。因此 &gt;&gt; 的 state 传递在这种场景下退化为 const &,不产生竞争。

4.4 并行累加的合法性

由于 Monoid associativity:

cpp
// 顺序累加
auto f = thrust + aero + gravity;

// 等价于并行累加 + reduce
auto f1 = thread_pool.async(compute_thrust);
auto f2 = thread_pool.async(compute_aero);
auto f3 = thread_pool.async(compute_gravity);
auto f  = f1.get() + f2.get() + f3.get();

前提:力源之间不共享可变状态(已通过"BodyEnv 只读 + RocketBody 不写"保证)。

但在当前实现中 不并行:单体 force 计算 ~10μs 量级,线程切换开销反而更高。多体并行发生在 WorldTick 跨 body 维度(多个 RocketBody 并行各自 BodyRWS pipeline)。


5. Writer 通道:DynLog 同形态

DynLog 是另一个 Monoid,与 Forces 完全同构:

cpp
// dynamics_core/log/DynLog.h(待 Wave 0 落地)
struct DynLog {
    std::vector&lt;TraceEntry&gt; entries;
    static DynLog empty() { return {}; }
    DynLog operator+(const DynLog& o) const {
        DynLog r = *this;
        r.entries.insert(r.entries.end(), o.entries.begin(), o.entries.end());
        return r;
    }
};

每个 compute_*_contribution 写入自己的 trace entry;&gt;&gt; 自动 Monoid 合并。整个 tick 结束后 WorldTick 把所有 body 的 DynLog 同样 Monoid 合并写入 World 级 Writer。

这是 Blueprint §1 提到的"Monoid 合流"哲学——状态与日志按同一个数学规律组合。


6. 字段扩展指南

加新力源 / 新通道时的判断流程:

新力源贡献是 BODY 系力?
    └ 是 → 累加到 total_force_b(推力 / 气动 / 反作用控制)
    └ 否 ↓
新力源贡献是 BODY 系力矩?
    └ 是 → 累加到 total_moment_b
    └ 否 ↓
新力源贡献是 LIC 系加速度(无方向耦合)?
    └ 是 → 加新独立字段(如 perturbation_lic);不要塞 gravity_lic
    └ 否 ↓
新力源是质量变化?
    └ 是 → 加 d_&lt;species&gt;_dt 通道
    └ 否 → 重新审视:这真的是 Forces 该承担的吗?(可能属于 InertialState 或 AuxState)

反例

  • ❌ 把电池温升加到 Forces——这是温度状态,不是力
  • ❌ 把"是否点火"标志位加到 Forces——状态机标志归 RocketBody.engines[].fsm_state
  • ❌ 把"目标位置"加到 Forces——这是 FCC 制导量,不归 dynamics

7. 反模式

反模式为什么不行
把重力塞进 total_force_b双重旋转误差;IMU 比力反推困难(§3.2)
力源内部修改 RocketBody(写 State 通道)破坏 fluent 累加;力源应只读 Reader / 只产 Forces
力源跳过 Monoid 直接累加到全局变量失去 associativity;多线程不安全;破坏 RWS 纯度
compute_total_forcesif (body.id == 0) ...dynamics 不该感知 body 业务身份;分支应通过 BodyEnv.asset 数据驱动
Forces 字段改成 std::map&lt;ForceKind, Vec3&gt; 求"可读性"失去 PoD 性质;蒸馏期断裂;fixed 6 字段刚刚好
Forces::operator+ 累加两个不同 body 的力Forces 是单体量;跨 body 累加无物理意义(除非建模耦合力,那也应在新机制下)
在力源内 assert(forces.total_force_b.norm() &lt; 1e6)力源应纯函数;防御性检查在 DynLog 中以警告记录

8. 与上下游的契约(端到端图)

┌─────────────────────────────────────────────────────────────────────┐
│ plant/physics/*                       (本构层:每个力源单独 BodyRWS)│
│   compute_thrust_contribution  → BodyRWS&lt;Forces&gt;                    │
│   compute_aero_contribution    → BodyRWS&lt;Forces&gt;                    │
│   compute_gravity_contribution → BodyRWS&lt;Forces&gt;                    │
└─────────────────────────────────────────────────────────────────────┘

                                  │  fluent  &gt;&gt;  累加(Monoid)

┌─────────────────────────────────────────────────────────────────────┐
│ simulation/pipeline/BodyTick.cpp                                    │
│   compute_total_forces(...) → BodyRWS&lt;Forces&gt;                       │
│   (这是跨域代码:组合 plant/physics 的多个 contribution)          │
└─────────────────────────────────────────────────────────────────────┘

                                  │  Forces 落地为值

┌─────────────────────────────────────────────────────────────────────┐
│ dynamics_core/ode/integrate_rk4(state, forces, inertia, dt)         │
│   纯函数:消费 Forces,产出 StateVector                              │
└─────────────────────────────────────────────────────────────────────┘


                          下一时刻 SpatialState

注意:Forces 的累加发生在 simulation/,因为它跨 plant 多个力源;dynamics_core/消费 Forces,不知道它是怎么来的。这是 Blueprint §2.6.4 跨域判据的应用。


9. 测试策略

测试目标级别描述
Monoid identityunitForces::zero() + f == f 对任意 f
Monoid associativityunit(a+b)+c == a+(b+c) 对随机三元组
gravity_lic 通道隔离unitcompute_thrust_contribution 返回的 forces.gravity_lic 必须 == 0
累加顺序无关性integrationthrust+aero+gravgrav+aero+thrust 在积分后状态相同(数值精度内)
质量守恒benchbody.mass + Σ d_*_dt * dt 之差 < 1e-12
不变性staticForces 字段为 6 个固定通道;新增需通过 §6 流程

10. C-Distillation 视角

类型C++ 阶段C 蒸馏阶段
Forcesstruct + operator overloadstypedef struct Forces { Vec3 fb, mb, glic; double dm,df,dox; } Forces;
operator+methodForces forces_add(Forces a, Forces b); 自由函数
Forces::zero()static method#define FORCES_ZERO ((Forces){0})static const Forces FORCES_ZERO
operator&gt;&gt; (fluent)templateinline 函数 + struct 复合

蒸馏后整个 Monoid pipeline 退化为一组结构体 + 6 个 add 调用。零开销。


11. Cross References

  • Universal ODE Kernel(消费 Forces 的积分器)→ Universal_ODE_Kernel.md
  • WorldStage 与拓扑代数 → Topology_Algebra.md
  • 推力计算五阶段 → Blueprint §4
  • 力源具体实现 → 02_Physical_World/Plant_Physics_Constitutive.md
  • 跨域 BodyRWS pipeline 装配 → 06_Simulation/Body_World_Tick.md
  • BodyEnv / RocketBody 跨域复合体 → 06_Simulation/RocketBody_Composite.md
  • 现有代码 → src/dynamics/state/Forces.h(待 Wave 0 迁移到 src/dynamics_core/pipeline/
  • Blueprint §2.5(dynamics_core)、§4.1(Forces Monoid 累加阶段)