Related: 03_signal_backplane.md | 04_lifecycle.md | 07_janus_integration.md
To satisfy solve_ivp and optimization solvers, the Continuous State must be contiguous.
flowchart TB
subgraph Simulator["Simulator (Owner)"]
X["X_global_<br/>[state vector]"]
Xdot["X_dot_global_<br/>[derivatives]"]
end
subgraph Components["Components (Views)"]
A["ComponentA.state_"]
B["ComponentB.state_"]
C["ComponentC.state_"]
end
X -.->|"ptr"| A
X -.->|"ptr"| B
X -.->|"ptr"| C
Xdot -.->|"ptr"| A
Xdot -.->|"ptr"| B
Xdot -.->|"ptr"| C
INT["Integrator<br/>(RK4/CVODES)"] -->|"reads X_dot"| Xdot
INT -->|"updates X"| X
style X fill:#9f9,stroke:#333
style Xdot fill:#f99,stroke:#333
style INT fill:#ff9,stroke:#333
1. The Global State Vector
template <typename Scalar>
class Simulator {
JanusVector<Scalar> X_global_;
JanusVector<Scalar> X_dot_global_;
struct StateSlice {
Component<Scalar>* owner;
size_t offset;
size_t size;
};
std::vector<StateSlice> state_layout_;
};
2. State Ownership Model
- Important
- The Simulator owns all state. Components hold views (pointers) into the global vector, not copies.
| Concept | Owner | Lifetime |
| Global State Vector X_global_ | Simulator | Provision → Destroy |
| Component State View state_ | Component (pointer only) | Stage → Reset |
| State Derivatives X_dot_global_ | Simulator | Provision → Destroy |
3. Scatter/Gather Protocol
void Simulator::Stage(const RunConfig& rc) {
size_t offset = 0;
for (auto* comp : components_) {
size_t state_size = comp->GetStateSize();
comp->BindState(
X_global_.data() + offset,
X_dot_global_.data() + offset
);
state_layout_.push_back({comp, offset, state_size});
offset += state_size;
}
}
template <typename Scalar>
class JetEngine : public Component<Scalar> {
Scalar* state_spool_speed_;
Scalar* state_dot_spool_speed_;
public:
void BindState(Scalar* state, Scalar* state_dot) override {
state_spool_speed_ = state;
state_dot_spool_speed_ = state_dot;
}
void Step(Scalar t, Scalar dt)
override {
Scalar omega = *state_spool_speed_;
Scalar omega_dot = (target_omega - omega) / tau;
*state_dot_spool_speed_ = omega_dot;
}
};
@ Step
Definition Error.hpp:231
4. Integration Flow
┌─────────────────────────────────────────────────────────────────┐
│ Integrator (RK4/CVODES) │
│ Owns: X_global_, X_dot_global_ │
└─────────────────────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Component A │ │ Component B │ │ Component C │
│ state_ ───────────► X_global_[0:3] │
│ state_dot_ ─────────► X_dot_[0:3] │
└─────────────┘ └─────────────┘ └─────────────┘
Step 1: Integrator calls Simulator.ComputeDerivatives(t, X)
Step 2: Simulator scatters X into component views (automatic via pointers)
Step 3: Each Component.Step() reads state_, writes state_dot_
Step 4: Simulator gathers X_dot (automatic via pointers)
Step 5: Integrator advances: X_new = X + dt * f(X_dot)
5. Why Components Don't Integrate
| Approach | Pros | Cons |
| Components integrate themselves | Simple, self-contained | Can't use adaptive solvers, no global error control |
| Simulator integrates globally ✓ | Full solver compatibility, global error control | Slightly more complex setup |
- Note
- Exception: Algebraic State. Some "state" is not integrated (e.g., lookup table outputs, mode flags). These can be updated directly by components since they're not part of the ODE system.
6. Solver Compatibility
This layout exposes the simulation as a standard ODE function:
JanusVector<Scalar> Simulator::ComputeDerivatives(Scalar t, const JanusVector<Scalar>& X) {
for (auto* comp : scheduled_components_) {
comp->Step(t, dt_nominal_);
}
return X_dot_global_;
}
7. Memory Layout Example
For a simulation with 3 components:
X_global_ layout:
┌─────────────────────────────────────────────────────────────────┐
│ Component A (3 states) │ Component B (6 states) │ Component C (4 states) │
│ [0] [1] [2] │ [3] [4] [5] [6] [7] [8] │ [9] [10] [11] [12] │
└─────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
A.state_ = &X[0] B.state_ = &X[3] C.state_ = &X[9]
This contiguous layout is optimal for:
- Cache efficiency during integration
- SIMD vectorization
- Direct handoff to external solvers (scipy, CVODES)
- Checkpoint/restore (single memcpy)