Related: 01_core_philosophy.md | 03_signal_backplane.md | 04_lifecycle.md
Components are the atomic units of execution. Each component has a well-defined interface consisting of:
| Category | Description | Types | Registered At | Optimizable |
| Outputs | Dynamic signals produced by the component | Scalar, Vec3<Scalar> | Provision | N/A |
| Inputs | Dynamic signal ports consumed by the component | Scalar, Vec3<Scalar> | Provision | N/A |
| Parameters | Continuous configurable values | Scalar | Provision | Yes |
| Config | Discrete configuration values | int, bool, enum | Provision | No |
All interface elements are:
- Discoverable via the auto-generated Data Dictionary
- Accessible at runtime via the Signal Access API (get/set any signal)
1. Component Interface Model
1.1 Outputs
Outputs are dynamic signals produced by the component. They represent the component's identity—what it is.
Vec3<Scalar> thrust_;
Scalar fuel_flow_;
void Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) override {
bp.register_output("thrust", &thrust_, "N", "Thrust force vector");
bp.register_output("fuel_flow", &fuel_flow_, "kg/s", "Fuel mass flow rate");
}
1.2 Inputs
Inputs are dynamic signal ports that the component needs. The component declares what it needs; the simulator wires where it comes from.
InputHandle<Scalar> throttle_;
InputHandle<Vec3<Scalar>> velocity_;
void Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) override {
bp.register_input("throttle", &throttle_, "", "Throttle command [0,1]");
bp.register_input("velocity", &velocity_, "m/s", "Vehicle velocity");
}
void Stage(Backplane<Scalar>& bp, const ComponentConfig& cfg) override {
bp.wire_inputs();
}
1.3 Parameters
Parameters are continuous configurable values that affect component behavior. They are Scalar-typed for full symbolic compatibility—the same code works with double or casadi::MX. Parameters can be optimization variables in trim/trajectory solvers.
Scalar max_thrust_;
Scalar isp_sea_level_;
Scalar isp_vacuum_;
Scalar time_constant_;
void Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) override {
bp.register_param("max_thrust", &max_thrust_,
cfg.get<Scalar>("max_thrust", Scalar{50000.0}),
"N", "Maximum thrust");
bp.register_param("isp_sea_level", &isp_sea_level_,
cfg.get<Scalar>("isp_sea_level", Scalar{280.0}),
"s", "Specific impulse at sea level");
bp.register_param("isp_vacuum", &isp_vacuum_,
cfg.get<Scalar>("isp_vacuum", Scalar{320.0}),
"s", "Specific impulse in vacuum");
bp.register_param("time_constant", &time_constant_,
cfg.get<Scalar>("time_constant", Scalar{0.5}),
"s", "Engine spool time constant");
}
1.4 Config
Config values are discrete configuration settings that control component behavior. They use concrete types (int, bool, enum) and are **not part of the symbolic graph**—they cannot be optimization variables.
int num_nozzles_;
bool enable_vectoring_;
GravityModel gravity_model_;
void Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) override {
bp.register_config("num_nozzles", &num_nozzles_,
cfg.get<int>("num_nozzles", 4),
"Number of engine nozzles");
bp.register_config("enable_vectoring", &enable_vectoring_,
cfg.get<bool>("enable_vectoring", true),
"Enable thrust vector control");
bp.register_config("gravity_model", &gravity_model_,
cfg.get<GravityModel>("gravity_model", GravityModel::WGS84),
"Gravity model selection");
}
Config vs Parameters
| Aspect | Parameters (Scalar) | Config (int, bool, enum) |
| Type | Continuous | Discrete |
| Symbolic | Yes (part of CasADi graph) | No (fixed during trace) |
| Optimizable | Yes (trim, trajectory) | No |
| Example | Mass, thrust limits, Isp | Nozzle count, flags, model selection |
| Runtime change | Safe (value substitution) | Requires re-Provision |
Config and Symbolic Mode
Config values are resolved at Provision time, before symbolic tracing. This is safe:
void Provision(...) {
if (cfg.get<bool>("use_j2")) {
enable_j2_ = true;
}
}
Using config in Step requires care:
void Step(Scalar t, Scalar dt) {
if (enable_j2_) {
accel += j2_perturbation();
}
accel += janus::where(j2_flag_ > Scalar{0.5},
j2_perturbation(),
Vec3<Scalar>::Zero());
}
2. Complete Component Example
template <typename Scalar>
class JetEngine : public Component<Scalar> {
Scalar thrust_;
Scalar fuel_flow_;
Scalar spool_speed_;
InputHandle<Scalar> altitude_;
InputHandle<Scalar> throttle_;
InputHandle<Scalar> mach_;
Scalar max_thrust_;
Scalar time_constant_;
Scalar min_throttle_;
int num_nozzles_;
bool enable_afterburner_;
Scalar afterburner_flag_;
public:
void Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) override {
bp.register_output("thrust", &thrust_, "N", "Net thrust force");
bp.register_output("fuel_flow", &fuel_flow_, "kg/s", "Fuel mass flow rate");
bp.register_output("spool_speed", &spool_speed_, {
.units = "rad/s",
.description = "Turbine spool angular velocity",
.integrable = true
});
bp.register_input("altitude", &altitude_, "m", "Geometric altitude");
bp.register_input("throttle", &throttle_, "", "Throttle command [0,1]");
bp.register_input("mach", &mach_, "", "Mach number");
bp.register_param("max_thrust", &max_thrust_,
cfg.get<Scalar>("max_thrust", Scalar{50000.0}),
"N", "Maximum thrust output");
bp.register_param("time_constant", &time_constant_,
cfg.get<Scalar>("time_constant", Scalar{0.5}),
"s", "Engine spool time constant");
bp.register_param("min_throttle", &min_throttle_,
cfg.get<Scalar>("min_throttle", Scalar{0.0}),
"", "Minimum throttle setting");
bp.register_config("num_nozzles", &num_nozzles_,
cfg.get<int>("num_nozzles", 1),
"Number of engine nozzles");
bp.register_config("enable_afterburner", &enable_afterburner_,
cfg.get<bool>("enable_afterburner", false),
"Enable afterburner capability");
afterburner_flag_ = enable_afterburner_ ? Scalar{1.0} : Scalar{0.0};
}
void Stage(Backplane<Scalar>& bp,
const ComponentConfig& cfg)
override {
bp.wire_inputs();
}
void Step(Scalar t, Scalar dt)
override {
Scalar alt = altitude_.get();
Scalar thr = throttle_.get();
Scalar m = mach_.get();
Scalar base_thrust = max_thrust_ * thr;
Scalar afterburner_bonus = janus::where(
afterburner_flag_ > Scalar{0.5},
base_thrust * Scalar{0.3},
Scalar{0.0}
);
thrust_ = base_thrust + afterburner_bonus;
fuel_flow_ = calculated_fuel_flow;
}
};
constexpr const char * Stage
Definition MissionLogger.hpp:42
@ Step
Definition Error.hpp:231
3. Wiring Configuration
Input wiring is specified externally to the component—either in YAML config or programmatically. Components never hardcode their input sources. Until configuration files are fleshed out, rely on programmatic wiring.
3.1 YAML Wiring
# scenario.yaml
components:
- type: JetEngine
name: MainEngine
entity: X15
config:
# Parameters (Scalar, optimizable)
max_thrust: 75000.0
time_constant: 0.5
# Config (discrete, not optimizable)
num_nozzles: 4
enable_afterburner: true
# wiring.yaml (or inline in scenario)
wiring:
X15.MainEngine:
altitude: "Environment.Atmosphere.altitude"
throttle: "X15.GNC.throttle_cmd"
mach: "Environment.Atmosphere.mach"
3.2 Programmatic Wiring
sim.Wire("X15.MainEngine.altitude", "Environment.Atmosphere.altitude");
sim.Wire("X15.MainEngine.throttle", "X15.GNC.throttle_cmd");
sim.Wire("X15.MainEngine.mach", "Environment.Atmosphere.mach");
sim.LoadWiring("wiring.yaml");
sim.ValidateWiring();
4. Why This Design?
4.1 Outputs vs Inputs
| Aspect | Outputs | Inputs |
| Defined by | Component code | Component code (ports) + Config (wiring) |
| Rationale | Identity—what the component is | Interface—what it needs |
| Flexibility | Fixed per component type | Wiring fully reconfigurable |
| Example | JetEngine always produces thrust | Altitude could come from sensor, model, or test harness |
- Note
- Outputs are identity. Inputs are interface. Wiring is topology. A JetEngine without thrust isn't a JetEngine. But where altitude comes from—barometer, GPS, truth model—that's just wiring.
4.2 Why Explicit Input Registration?
The previous pattern of only declaring inputs implicitly at resolve() time had problems:
| Issue | Old Pattern | New Pattern |
| Discoverability | Inputs not in Data Dictionary | All inputs discoverable |
| Validation | Runtime crash if signal missing | Pre-run validation possible |
| Coupling | Component knows provider names | Component only knows port names |
| Tooling | Can't visualize input requirements | Full interface visible |
4.3 Why Scalar Parameters?
Parameters are Scalar-typed (not double) for symbolic compatibility:
Scalar max_thrust_;
auto optimal_thrust = trim_solver.Optimize("X15.MainEngine.max_thrust");
5. Signal Access API
Any registered signal (output, input source, parameter, or config) can be accessed at runtime:
Scalar thrust = sim.Get<Scalar>("X15.MainEngine.thrust");
Vec3<Scalar> pos = sim.Get<Vec3<Scalar>>("X15.EOM.position");
sim.Set("X15.MainEngine.max_thrust", Scalar{60000.0});
sim.Set("X15.MainEngine.num_nozzles", 6);
sim.Set("X15.MainEngine.enable_afterburner", false);
SignalInfo info = sim.GetSignalInfo("X15.MainEngine.thrust");
- Warning
- Config changes at runtime require re-Provision. Changing discrete config values (int, bool, enum) after Provision may leave the component in an inconsistent state. Parameters (Scalar) can be changed safely at any time.
Use Cases
| Use Case | API |
| Debugging | sim.Get("signal") to inspect values |
| Parameter Tuning | sim.Set("param", Scalar{value}) during runtime |
| Config Override | sim.Set("config", value) before Provision |
| Test Harnesses | Override inputs/outputs for unit testing |
| Recording | Record any signal by name |
| Telemetry | Stream selected signals |
6. Auto-Generated Data Dictionary
After Provision, the Simulator generates a complete interface catalog:
void Simulator::Provision(const ScenarioConfig& cfg) {
for (auto* comp : components_) {
comp->Provision(backplane_, comp->config_);
}
backplane_.GenerateDataDictionary("output/data_dictionary.yaml");
}
Output: data_dictionary.yaml
# AUTO-GENERATED by Icarus Simulator
# Scenario: scenarios/x15_mission.yaml
# Generated: 2024-12-22T10:30:00Z
components:
X15.MainEngine:
type: JetEngine
outputs:
thrust:
type: Scalar
units: N
description: "Net thrust force"
fuel_flow:
type: Scalar
units: kg/s
description: "Fuel mass flow rate"
spool_speed:
type: Scalar
units: rad/s
description: "Turbine spool angular velocity"
integrable: true
inputs:
altitude:
type: Scalar
units: m
description: "Geometric altitude"
wired_to: "Environment.Atmosphere.altitude"
throttle:
type: Scalar
units: ""
description: "Throttle command [0,1]"
wired_to: "X15.GNC.throttle_cmd"
mach:
type: Scalar
units: ""
description: "Mach number"
wired_to: "Environment.Atmosphere.mach"
parameters: # Scalar-typed, optimizable
max_thrust:
type: Scalar
units: N
value: 75000.0
description: "Maximum thrust output"
time_constant:
type: Scalar
units: s
value: 0.5
description: "Engine spool time constant"
min_throttle:
type: Scalar
units: ""
value: 0.0
description: "Minimum throttle setting"
config: # Discrete, not optimizable
num_nozzles:
type: int
value: 4
description: "Number of engine nozzles"
enable_afterburner:
type: bool
value: true
description: "Enable afterburner capability"
Environment.Atmosphere:
type: StandardAtmosphere
outputs:
altitude:
type: Scalar
units: m
description: "Geometric altitude"
density:
type: Scalar
units: kg/m³
description: "Atmospheric density"
mach:
type: Scalar
units: ""
description: "Mach number"
# Summary
summary:
total_components: 12
total_outputs: 42
total_inputs: 38
total_parameters: 24
total_config: 18
integrable_states: 13
unwired_inputs: 0 # Validation: should be 0
7. Data Dictionary Uses
| Use Case | How |
| Telemetry Setup | Browse signals, select for streaming |
| Recording Config | Reference by name: ["X15.MainEngine.*"] |
| Wiring Validation | Check unwired_inputs: 0 |
| Documentation | Auto-generated, always current |
| Tooling | UI signal browser, wiring editor |
| Test Generation | Know required inputs for component |
8. Dependency Discovery & Scheduling
The Scheduler builds a dependency graph from registered inputs/outputs:
void Simulator::Stage(const RunConfig& rc) {
DependencyGraph graph;
for (auto* comp : components_) {
for (const auto& out : comp->GetOutputs()) {
graph.AddProvider(out.full_name, comp);
}
}
for (auto* comp : components_) {
for (const auto& in : comp->GetInputs()) {
graph.AddDependency(in.wired_to, comp);
}
}
execution_order_ = graph.TopologicalSort();
}
9. Validation
| Validation | When | Error Example |
| Signal exists | wire_inputs() at Stage | "Env.Atm.altidude" not found. Did you mean "Environment.Atmosphere.altitude"? |
| Type matches | Wiring validation | "gnc.mode" is int, but input expects Scalar |
| No duplicate outputs | register_output() at Provision | "thrust" already registered by LeftEngine |
| All inputs wired | ValidateWiring() | X15.MainEngine.throttle: input not wired |
| No dependency cycles | End of Stage | Cycle detected: A → B → C → A |
10. Aggregation Registration
Some quantities require global aggregation**—summing contributions from many components. Components **opt-in by registering as sources during Provision.
- Note
- See 12_quantity_aggregation.md for complete implementation details.
Force Sources
template <typename Scalar>
void RocketEngine<Scalar>::Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) {
bp.register_output("thrust", &thrust_mag_, "N", "Thrust magnitude");
bp.register_param("max_thrust", &max_thrust_, cfg.get(...), "N", "Max thrust");
bp.register_force_source({
.name = full_name_ + ".thrust",
.force = &thrust_force_,
.moment = &thrust_moment_,
.frame = Frame::LOCAL,
.application_point = &nozzle_pos_,
.dcm_to_body = &nozzle_dcm_,
.entity_active = entity_active_ptr_
});
}
Mass Sources
template <typename Scalar>
void FuelTank<Scalar>::Provision(Backplane<Scalar>& bp, const ComponentConfig& cfg) {
bp.register_output("fuel_mass", &fuel_mass_, "kg", "Current fuel mass");
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_
});
}
Why Registration (Not Base Class)?
| Approach | Problem |
| Bake into Component | Forces every component to define force/mass even when N/A |
| Virtual getForce() | Breaks symbolic mode (CasADi can't trace through vtables) |
| Registration | Opt-in, explicit, traceable, symbolic-compatible |
Components without forces or mass (e.g., Autopilot, IMU, Telemetry) simply don't register—no overhead, no boilerplate.