Related: 02_component_protocol.md | 09_memory_state_ownership.md | 08_vulcan_integration.md
In a 6DOF simulation, the Equations of Motion (EOM) require total force, total moment, total mass, CG position, and inertia tensor. Multiple components contribute to these quantities. This document defines the registration and aggregation pattern for force and mass.
flowchart LR
subgraph Force Sources
Aero["Aero<br/>(wind frame)"]
Prop["Propulsion<br/>(nozzle frame)"]
Grav["Gravity<br/>(inertial)"]
end
subgraph Mass Sources
Struct["Structure<br/>(static)"]
Fuel["FuelTank<br/>(dynamic)"]
end
Aero --> FA
Prop --> FA
Grav --> FA
Struct --> MA
Fuel --> MA
MA["MassAggregator"] --> FA["ForceAggregator"]
MA --> EOM
FA --> EOM["RigidBody6DOF<br/>EOM"]
style FA fill:#f9f,stroke:#333
style MA fill:#9ff,stroke:#333
style EOM fill:#ff9,stroke:#333
1. What Needs Global Aggregation
| Quantity | Why Global | Consumer |
| Forces | EOM needs net force on vehicle | EOM |
| Moments | EOM needs net moment about CG | EOM |
| Mass | EOM needs total mass for F=ma | EOM, ForceAggregator |
| CG Position | ForceAggregator needs CG for moment transfer | ForceAggregator |
| Inertia Tensor | EOM needs I for rotational dynamics | EOM |
What Doesn't Need Global Aggregation
| Quantity | Why Not | Better Approach |
| Power | Components have specific power relationships | Explicit wiring between sources/loads |
| Thermal | Heat transfer is between specific components | Component-level thermal models |
| Propellant | Engines draw from specific tanks | Explicit tank→engine wiring |
2. Core Principle: Registration Protocol
Components opt-in to aggregation by registering as sources during Provision:
- Explicit: Components choose to participate
- Discoverable: Aggregators find sources automatically
- Traceable: All data flows through Backplane signals
template <typename Scalar>
class Backplane {
std::vector<ForceSourceReg<Scalar>> force_sources_;
std::vector<MassSourceReg<Scalar>> mass_sources_;
public:
void register_force_source(ForceSourceReg<Scalar> reg);
void register_mass_source(MassSourceReg<Scalar> reg);
auto get_force_sources(const std::string& entity) const;
auto get_mass_sources(const std::string& entity) const;
};
3. Force/Moment Aggregation
3.1 The Problem
- Aero computes lift/drag in wind frame
- Propulsion computes thrust in nozzle-local frame
- Gravity is in inertial frame
- Control Surfaces produce forces at hinge points
All must become body-frame forces/moments about the CG.
3.2 Force Source Registration
template <typename Scalar>
struct ForceSourceReg {
std::string name;
Vec3<Scalar>* force;
Vec3<Scalar>* moment;
Frame frame;
Vec3<Scalar>* application_point;
Mat3<Scalar>* dcm_to_body;
Scalar* entity_active;
};
3.3 Registration Examples
template <typename Scalar>
void RocketEngine<Scalar>::Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) {
bp.register_output("thrust", &thrust_mag_, {.units = "N"});
bp.register_output("fuel_flow", &fuel_flow_, {.units = "kg/s"});
bp.register_force_source({
.name = full_name_ + ".thrust",
.force = &thrust_force_,
.moment = &thrust_moment_,
.frame = Frame::LOCAL,
.application_point = &nozzle_position_,
.dcm_to_body = &nozzle_dcm_,
.entity_active = entity_active_ptr_
});
}
template <typename Scalar>
void Aerodynamics<Scalar>::Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) {
bp.register_output("lift", &lift_, {.units = "N"});
bp.register_output("drag", &drag_, {.units = "N"});
bp.register_force_source({
.name = full_name_ + ".aero",
.force = &aero_force_,
.moment = &aero_moment_,
.frame = Frame::WIND,
.application_point = &aero_ref_point_,
.dcm_to_body = nullptr,
.entity_active = entity_active_ptr_
});
}
template <typename Scalar>
void GravityModel<Scalar>::Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) {
bp.register_force_source({
.name = full_name_ + ".weight",
.force = &weight_body_,
.moment = nullptr,
.frame = Frame::BODY,
.application_point = nullptr,
.dcm_to_body = nullptr,
.entity_active = entity_active_ptr_
});
}
3.4 Force Aggregator Implementation
template <typename Scalar>
class ForceAggregator : public Component<Scalar> {
std::vector<ForceSourceReg<Scalar>> sources_;
SignalHandle<Mat3<Scalar>> wind_to_body_dcm_;
SignalHandle<Mat3<Scalar>> inertial_to_body_dcm_;
SignalHandle<Vec3<Scalar>> cg_position_;
public:
void Stage(Backplane<Scalar>& bp, const RunConfig& rc) override {
sources_ = bp.get_force_sources(entity_prefix_);
wind_to_body_dcm_ = bp.resolve<Mat3<Scalar>>(entity_ + ".Nav.dcm_wind_body");
inertial_to_body_dcm_ = bp.resolve<Mat3<Scalar>>(entity_ + ".Nav.dcm_inertial_body");
cg_position_ = bp.resolve<Vec3<Scalar>>(entity_ + ".Mass.cg");
ICARUS_INFO("ForceAggregator: {} sources for '{}'", sources_.size(), entity_prefix_);
}
void Step(Scalar t, Scalar dt) override {
Vec3<Scalar> F_total = {0, 0, 0};
Vec3<Scalar> M_total = {0, 0, 0};
for (const auto& src : sources_) {
Scalar active = src.entity_active ? *src.entity_active : Scalar(1);
Vec3<Scalar> F_body = TransformToBody(*src.force, src);
Vec3<Scalar> M_body = src.moment ? TransformToBody(*src.moment, src)
: Vec3<Scalar>{0, 0, 0};
if (src.application_point) {
Vec3<Scalar> r = *src.application_point - *cg_position_;
M_body += vulcan::cross(r, F_body);
}
F_total += F_body * active;
M_total += M_body * active;
}
*output_total_force_ = F_total;
*output_total_moment_ = M_total;
}
private:
Vec3<Scalar> TransformToBody(const Vec3<Scalar>& v, const ForceSourceReg<Scalar>& src) {
switch (src.frame) {
case Frame::BODY: return v;
case Frame::WIND: return (*wind_to_body_dcm_) * v;
case Frame::INERTIAL: return (*inertial_to_body_dcm_) * v;
case Frame::LOCAL: return (*src.dcm_to_body) * v;
}
}
};
4. Mass/Inertia Aggregation
4.1 Mass Source Registration
template <typename Scalar>
struct MassSourceReg {
std::string name;
Scalar* mass;
Vec3<Scalar>* cg_body;
Mat3<Scalar>* inertia_cg;
Lifecycle lifecycle;
Scalar* entity_active;
};
4.2 Registration Examples
template <typename Scalar>
void Structure<Scalar>::Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) {
dry_mass_ = cfg.get<double>("dry_mass");
dry_cg_ = cfg.get<Vec3<double>>("dry_cg");
dry_inertia_ = cfg.get<Mat3<double>>("dry_inertia");
bp.register_mass_source({
.name = full_name_ + ".dry",
.mass = &dry_mass_,
.cg_body = &dry_cg_,
.inertia_cg = &dry_inertia_,
.lifecycle = Lifecycle::STATIC,
.entity_active = entity_active_ptr_
});
}
template <typename Scalar>
void FuelTank<Scalar>::Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) {
bp.register_output("fuel_mass", &fuel_mass_, {.units = "kg"});
bp.register_mass_source({
.name = full_name_ + ".fuel",
.mass = &fuel_mass_,
.cg_body = &fuel_cg_,
.inertia_cg = nullptr,
.lifecycle = Lifecycle::DYNAMIC,
.entity_active = entity_active_ptr_
});
}
4.3 Mass Aggregator Implementation
template <typename Scalar>
class MassAggregator : public Component<Scalar> {
std::vector<MassSourceReg<Scalar>> sources_;
public:
void Stage(Backplane<Scalar>& bp, const RunConfig& rc) override {
sources_ = bp.get_mass_sources(entity_prefix_);
ICARUS_INFO("MassAggregator: {} sources", sources_.size());
}
void Step(Scalar t, Scalar dt) override {
Scalar total_mass = 0;
Vec3<Scalar> mass_moment = {0, 0, 0};
for (const auto& src : sources_) {
Scalar active = src.entity_active ? *src.entity_active : Scalar(1);
Scalar m = (*src.mass) * active;
total_mass += m;
mass_moment += m * (*src.cg_body);
}
Vec3<Scalar> cg = mass_moment / total_mass;
Mat3<Scalar> total_inertia = Mat3<Scalar>::Zero();
for (const auto& src : sources_) {
Scalar active = src.entity_active ? *src.entity_active : Scalar(1);
Scalar m = (*src.mass) * active;
Mat3<Scalar> I_src = src.inertia_cg ? *src.inertia_cg : Mat3<Scalar>::Zero();
Vec3<Scalar> r = *src.cg_body - cg;
Scalar r_sq = vulcan::dot(r, r);
Mat3<Scalar> I_transfer = m * (r_sq * Mat3<Scalar>::Identity() - vulcan::outer(r, r));
total_inertia += (I_src + I_transfer) * active;
}
*output_total_mass_ = total_mass;
*output_cg_ = cg;
*output_inertia_ = total_inertia;
}
};
5. Execution Order
Aggregators must run after sources, before EOM:
1. [Environment] → Atmosphere, Gravity models
2. [Force Sources] → Aero, Propulsion (compute forces)
3. [Mass Sources] → FuelTanks (update mass after consumption)
4. [MassAggregator] → Compute total mass, CG, inertia
5. [ForceAggregator] → Transform and sum forces (needs CG)
6. [EOM] → Integrate dynamics
6. Components Without Aggregated Quantities
Components that don't produce forces or have mass simply don't register:
| Component | Force | Mass |
| RocketEngine | ✓ | ✓ |
| JetEngine | ✓ | ✓ |
| Aerodynamics | ✓ | ✗ |
| GravityModel | ✓ | ✗ |
| FuelTank | ✗ | ✓ |
| Structure | ✗ | ✓ |
| Payload | ✗ | ✓ |
| Autopilot | ✗ | ✗ |
| IMU | ✗ | ✗ |
| Telemetry | ✗ | ✗ |
7. Benefits of Registration Pattern
| Aspect | Old Hierarchical | Registration Pattern |
| Discovery | Implicit tree walk | Explicit registry query |
| Opt-in | Forced on all | Components choose |
| Traceability | Hidden in hierarchy | Signals on Backplane |
| Frame handling | Often implicit | Explicit in metadata |
| Symbolic mode | Breaks with vtable | Clean signal flow |
| Testing | Mock whole hierarchy | Mock Backplane only |
8. Frame Reference
| Frame | Description | Used By |
| BODY | Vehicle-fixed, origin at reference point | EOM, most outputs |
| WIND | Velocity-aligned (X=airspeed direction) | Aerodynamics |
| LOCAL | Component-fixed (e.g., nozzle axis) | Thrusters, gimbals |
| INERTIAL | ECI or ECEF | Gravity, navigation |