The three-layer pipeline¶
manta is organized as three layers with an explicit boundary at every
step. The model is declarative; the transforms are siblings over
it, each owning its math and emitting a typed Module IR; a Target*
lowers any Module to a backend.
Model Transform Target Result
─────────────────────────────────────────────────────────────────
World → Sim(world) → TargetNumpy(sim) → NumpyRuntime
(linearized tick) .step() / .outputs()
World → EKF(world) → TargetNumpy(ekf) → NumpyRuntime
(Kalman recursion, .update() / .predict()
baked kernels) (you own the loop)
World → LQR(world, …) → TargetNumpy(lqr) → NumpyRuntime
(Riccati → gain K) .control(state) → {input: u}
any of these → .module() → TargetCpp(x, …) → <basename>.cpp/.hpp
+ flat-C kernels + CMake
Layer 1 — Model¶
A World holds Crafts,
Planets, Couplings, and shared
Fields. A craft is a tree of Parts, each
declaring its channels at class scope:
Parameter— frozen at construction, baked into the graph.State— mutable per-tick state, with a manifold (R1,R3,SO3).Input— per-tick user-supplied value (e.g. throttle).Output— per-tick observable (a sensor reading).Noise— white noise or a random-walk bias state.
The model is pure description: nothing executes at this layer.
Layer 2 — Transform¶
Sim(world), EKF(world), and LQR(world, …) are pure compile-time.
Each writes its math symbolically over the shared LinearizedSystem
(manifold-aware F / B / H / L over the compiled world tick) and emits a
typed Module — a state layout plus named CasADi kernels plus typed
entry points. A Module is not directly callable: it is data
describing the computation.
The three transforms are siblings — none is privileged, none knows about
the others, and each owns exactly its own math (forward dynamics for
Sim; the Kalman recursion for EKF; the Riccati solve for LQR).
Layer 3 — Target¶
A Target* lowers a Module to a backend:
TargetNumpy→ the one native-PythonNumpyRuntime. Its surface is derived from the Module's shape — a Sim Module yields.step()/.outputs(), an EKF Module yields.update()/.predict(), an LQR Module yields.control().TargetCpp→ a typed Eigen C++ class over flat-C kernels, plus a CMake project, for embedded deployment.TargetJax→ a jitted JAX rollout.
Backends contain no per-transform code: each implements exactly one generic lowering of a Module. This is what keeps the numpy and C++ paths behaving identically — you own the same driving loop in both.
Why error-state, why CasADi¶
The rigid-body state lives on a manifold (orientation is SO(3)), so the
EKF is an error-state filter: covariance and updates live in the
tangent space, and the model carries manifold-correct boxplus/boxminus
so attitude never leaves the unit-quaternion sheet. CasADi gives manta
the symbolic graph + autodiff it needs to assemble F/B/H/L (and the
process/measurement noise) automatically from the declared model, and to
lower the same graph to C.