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 note:Forces 是 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):
// 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_b 与 total_moment_b:体系合力 / 合力矩
体系(BODY)下的合力与合力矩。所有非重力体力(推力、气动、控制扰流、分离冲击)累加到这里。
为什么用 BODY 系?
- 推力天然在 BODY 系(推力线沿喷管轴)
- 气动力天然在 BODY 系(升力 / 阻力定义基于本体迎角与姿态)
- 转动惯量在 BODY 系常对角化(绕主惯性轴)
最终在 compute_velocity_derivative 内通过 att_lic_to_body.conjugate() 投到 LIC 系积分。
3.2 gravity_lic:独立的 LIC 重力通道
关键设计:重力不进 total_force_b,而是有独立字段。
原因:
- 重力在 LIC 系是天然标量场(
g = GM/r²沿径向) - 把重力先投到 BODY 系再投回 LIC 会引入两次旋转的数值误差
- 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_dt、d_oxidizer_dt:分别记录两种推进剂消耗d_mass_dt:总流率 = 燃料 + 氧化剂 + (未来扩展,如冷却剂排放)
> 当前实现中 d_mass_dt 由 compute_thrust_contribution 直接填,与燃料/氧化剂之和应保持一致(但 Monoid 加法不强制——这是约定,不是类型约束)。
为什么分开?因为推进剂质量不同 ↔ 重心位置 / 转动惯量不同,下游 MassProps 计算需要分通道。
4. Pipeline 累加:BodyRWS<Forces> 组合
4.1 RWS 别名
// dynamics_core/pipeline/RWSAlias.h
template <typename A>
using BodyRWS = monad::RWS<BodyEnv, DynLog, RocketBody, A>;> ⚠️ BodyEnv / RocketBody 本身不定义在 dynamics_core/——它们是跨域复合体,归 simulation/state/。dynamics_core/pipeline/RWSAlias.h 只做模板别名,不 include 这些跨域类型的定义;具体的 BodyRWS 实例化发生在 simulation/pipeline/。
4.2 力源 contribution 的统一签名
每个 plant 力源都返回 BodyRWS<Forces>:
// plant/physics/Thrust.h
BodyRWS<Forces> compute_thrust_contribution(const std::vector<EngineEffect>& effects);
// plant/physics/Drag.h
BodyRWS<Forces> compute_aero_contribution(const DynInFrame& in);
// plant/physics/Gravity.h
BodyRWS<Forces> compute_gravity_contribution(); // 填 gravity_lic 通道契约:
- 每个函数返回仅本力源贡献的
Forces(其他字段为 zero) - 函数内部从
BodyEnv读AeroCtx/TrajCtx/MassPropsCtx - 函数内部通过
dyn_tell()写DynLog - 函数不修改
RocketBody(不调用body_put)
4.3 累加:fluent 风格
来自 dynamics/AGENTS.md 的约定:
// simulation/pipeline/BodyTick.cpp
BodyRWS<Forces> compute_total_forces(const std::vector<EngineEffect>& thrust,
const DynInFrame& dyn_in) {
return forces_pure(Forces::zero())
>> compute_thrust_contribution(thrust)
>> compute_aero_contribution(dyn_in)
>> compute_gravity_contribution();
}operator>> 在 monad/RWS.h 内为 BodyRWS<Forces> 重载:
template <typename A>
BodyRWS<A> operator>>(BodyRWS<A> lhs, BodyRWS<A> rhs) {
// 当 A 是 Monoid 时(如 Forces / DynLog),lhs 与 rhs 的 A 通道按 Monoid 加法合并
// State 通道(RocketBody)按 lhs.state → rhs(顺序传递)
// Writer 通道(DynLog)按 Monoid 加法合并
return /* ... */;
}> 注意:compute_*_contribution 的 State 通道(RocketBody)只读——它们不写回 body。因此 >> 的 state 传递在这种场景下退化为 const &,不产生竞争。
4.4 并行累加的合法性
由于 Monoid associativity:
// 顺序累加
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 完全同构:
// dynamics_core/log/DynLog.h(待 Wave 0 落地)
struct DynLog {
std::vector<TraceEntry> 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;>> 自动 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_<species>_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_forces 内 if (body.id == 0) ... | dynamics 不该感知 body 业务身份;分支应通过 BodyEnv.asset 数据驱动 |
把 Forces 字段改成 std::map<ForceKind, Vec3> 求"可读性" | 失去 PoD 性质;蒸馏期断裂;fixed 6 字段刚刚好 |
用 Forces::operator+ 累加两个不同 body 的力 | Forces 是单体量;跨 body 累加无物理意义(除非建模耦合力,那也应在新机制下) |
在力源内 assert(forces.total_force_b.norm() < 1e6) | 力源应纯函数;防御性检查在 DynLog 中以警告记录 |
8. 与上下游的契约(端到端图)
┌─────────────────────────────────────────────────────────────────────┐
│ plant/physics/* (本构层:每个力源单独 BodyRWS)│
│ compute_thrust_contribution → BodyRWS<Forces> │
│ compute_aero_contribution → BodyRWS<Forces> │
│ compute_gravity_contribution → BodyRWS<Forces> │
└─────────────────────────────────────────────────────────────────────┘
│
│ fluent >> 累加(Monoid)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ simulation/pipeline/BodyTick.cpp │
│ compute_total_forces(...) → BodyRWS<Forces> │
│ (这是跨域代码:组合 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 identity | unit | Forces::zero() + f == f 对任意 f |
| Monoid associativity | unit | (a+b)+c == a+(b+c) 对随机三元组 |
gravity_lic 通道隔离 | unit | compute_thrust_contribution 返回的 forces.gravity_lic 必须 == 0 |
| 累加顺序无关性 | integration | thrust+aero+grav 与 grav+aero+thrust 在积分后状态相同(数值精度内) |
| 质量守恒 | bench | body.mass + Σ d_*_dt * dt 之差 < 1e-12 |
| 不变性 | static | Forces 字段为 6 个固定通道;新增需通过 §6 流程 |
10. C-Distillation 视角
| 类型 | C++ 阶段 | C 蒸馏阶段 |
|---|---|---|
Forces | struct + operator overloads | typedef struct Forces { Vec3 fb, mb, glic; double dm,df,dox; } Forces; |
operator+ | method | Forces forces_add(Forces a, Forces b); 自由函数 |
Forces::zero() | static method | #define FORCES_ZERO ((Forces){0}) 或 static const Forces FORCES_ZERO |
operator>> (fluent) | template | inline 函数 + 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 累加阶段)