Skip to content

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-Python NumpyRuntime. 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.

See also