Skip to content

IR primitives

The low-level manta.ir layer — frames, typed values, manifolds, and the flat-vector state spec. Most users never touch this directly; it is exported for advanced use (building bare CasADi graphs).

Frames

manta.ir.Frame

Marker base class. Subclass to declare a new frame tag.

Frames are never instantiated; they are used purely as types passed to IR-value generics like Vec3[SomeFrame]. A frame's identity is its class object.

manta.ir.FrameError

FrameError(op, *, expected, got, source=None)

Bases: TypeError

Raised when an IR op's operands have incompatible frame tags.

Carries the operation name, the offending frames, and a source-location string captured at op-build time so the message points at the user's .py:line (not at the manta IR internals).

Source code in manta/ir/frames.py
def __init__(self, op: str, *, expected: str, got: str,
             source: str | None = None):
    msg = f"{op}: frame mismatch — expected {expected}, got {got}"
    if source:
        msg += f"\n  at {source}"
    super().__init__(msg)
    self.op = op
    self.expected = expected
    self.got = got
    self.source = source

Typed values

manta.ir.Vec3

Vec3(mx, frame)

Bases: _IRValue

A 3-vector tagged with a single frame.

Source code in manta/ir/types.py
def __init__(self, mx, frame):
    _validate_frame("Vec3", frame)
    mx = _as_mx(mx)
    if mx.shape != (3, 1):
        raise ValueError(
            f"Vec3: expected shape (3,1), got {tuple(mx.shape)}")
    self._mx = mx
    self._frame = frame

manta.ir.Mat3

Mat3(mx, from_frame, to_frame)

Bases: _IRValue

A 3×3 matrix mapping Vec3[B] → Vec3[A] (i.e., from_frame=A, to_frame=B).

Composition: Mat3[A,B] @ Mat3[B,C] → Mat3[A,C]. Application: Mat3[A,B] @ Vec3[B] → Vec3[A].

Source code in manta/ir/types.py
def __init__(self, mx, from_frame, to_frame):
    _validate_frame("Mat3 from_frame", from_frame)
    _validate_frame("Mat3 to_frame", to_frame)
    mx = _as_mx(mx)
    if mx.shape != (3, 3):
        raise ValueError(
            f"Mat3: expected shape (3,3), got {tuple(mx.shape)}")
    self._mx = mx
    self._from_frame = from_frame
    self._to_frame = to_frame

transpose

transpose()

Mat3[A,B] → Mat3[B,A].

Source code in manta/ir/types.py
def transpose(self):
    """Mat3[A,B] → Mat3[B,A]."""
    return Mat3(self._mx.T,
                from_frame=self._to_frame, to_frame=self._from_frame)

inv

inv()

Mat3[A,A] → Mat3[A,A]. Frames must match (only same-frame matrices are square endomorphisms with sensible inverse semantics).

Source code in manta/ir/types.py
def inv(self):
    """Mat3[A,A] → Mat3[A,A]. Frames must match (only same-frame
    matrices are square endomorphisms with sensible inverse semantics)."""
    if self._from_frame is not self._to_frame:
        raise FrameError(
            "Mat3.inv",
            expected="from_frame == to_frame (endomorphism)",
            got=f"{self._from_frame.__name__} vs {self._to_frame.__name__}",
            source=_capture_user_source(),
        )
    return Mat3(ca.inv(self._mx),
                from_frame=self._from_frame, to_frame=self._to_frame)

manta.ir.Quat

Quat(mx, from_frame, to_frame)

Bases: _IRValue

Unit quaternion representing a rotation from one frame to another.

Convention: a Quat[From, To], when applied to a vector in To, returns the vector expressed in From components::

q : Quat[From, To]
v_in_to : Vec3[To]
v_in_from = q.apply(v_in_to)    # → Vec3[From]

Composition Quat[A, B] * Quat[B, C] → Quat[A, C].

Storage: (w, x, y, z) as a 4×1 MX. Normalization is the caller's responsibility (use .normalize() after numerical updates).

Source code in manta/ir/types.py
def __init__(self, mx, from_frame, to_frame):
    _validate_frame("Quat from_frame", from_frame)
    _validate_frame("Quat to_frame", to_frame)
    mx = _as_mx(mx)
    if mx.shape != (4, 1):
        raise ValueError(
            f"Quat: expected shape (4,1), got {tuple(mx.shape)}")
    self._mx = mx
    self._from_frame = from_frame
    self._to_frame = to_frame

conjugate

conjugate()

Quat[A,B] → Quat[B,A]. For unit quaternions this is the inverse.

Source code in manta/ir/types.py
def conjugate(self) -> "Quat":
    """Quat[A,B] → Quat[B,A]. For unit quaternions this is the inverse."""
    return Quat(
        quat_conj(self._mx),
        from_frame=self._to_frame, to_frame=self._from_frame,
    )

apply

apply(vec)

q.apply(v_in_To) → v_in_From. Standard quaternion-vector product.

Source code in manta/ir/types.py
def apply(self, vec: Vec3) -> Vec3:
    """q.apply(v_in_To) → v_in_From. Standard quaternion-vector product."""
    if not isinstance(vec, Vec3):
        raise TypeError(
            f"Quat.apply: arg must be a Vec3, got {type(vec).__name__}")
    if vec._frame is not self._to_frame:
        raise FrameError(
            "Quat.apply",
            expected=f"vec frame matches Quat to_frame "
                     f"({self._to_frame.__name__})",
            got=f"vec_frame={vec._frame.__name__}, "
                f"to_frame={self._to_frame.__name__}",
            source=_capture_user_source(),
        )
    # Build the rotation matrix and apply. CasADi's CSE collapses the
    # repeated subterms; clearer than the direct sandwich formula.
    R = self._rotmat_mx()
    return Vec3(R @ vec._mx, frame=self._from_frame)

to_rotmat

to_rotmat()

Return the rotation matrix Mat3[From, To].

Source code in manta/ir/types.py
def to_rotmat(self) -> Mat3:
    """Return the rotation matrix Mat3[From, To]."""
    return Mat3(self._rotmat_mx(),
                from_frame=self._from_frame, to_frame=self._to_frame)

manta.ir.Scalar

Scalar(mx)

Bases: _IRValue

A scalar IR value. Has no frame tag — scalars are frame-agnostic.

Source code in manta/ir/types.py
def __init__(self, mx):
    mx = _as_mx(mx)
    if mx.shape != (1, 1):
        raise ValueError(
            f"Scalar: expected shape (1,1), got {tuple(mx.shape)}")
    self._mx = mx

coerce classmethod

coerce(value)

value as-is when already a Scalar, else a constant — the promotable-Parameter idiom (see _ParameterizedConstructor.coerce).

Source code in manta/ir/types.py
@classmethod
def coerce(cls, value) -> "Scalar":
    """`value` as-is when already a Scalar, else a constant — the
    promotable-Parameter idiom (see _ParameterizedConstructor.coerce)."""
    return cls._ctor().coerce(value)

Manifolds

manta.ir.Manifold dataclass

Manifold()

Bases: ABC

Structural descriptor + operations for a state-vector component.

Subclasses set kind (the backend-registry key) and the dims as class-level constants. Instance attributes carry per-occurrence context (e.g. a frame tag).

boxplus abstractmethod

boxplus(x, delta)

Frame-checked boxplus on tagged values: ambient ⊞ tangent → ambient. x is a Scalar/Vec3/Quat, delta the matching tangent value. Returns a value of the same type as x.

Source code in manta/ir/manifold.py
@abstractmethod
def boxplus(self, x, delta):
    """Frame-checked boxplus on tagged values: ambient ⊞ tangent →
    ambient. `x` is a `Scalar`/`Vec3`/`Quat`, `delta` the matching
    tangent value. Returns a value of the same type as `x`."""

boxminus abstractmethod

boxminus(a, b)

Frame-checked boxminus on tagged values: ambient ⊟ ambient → tangent value.

Source code in manta/ir/manifold.py
@abstractmethod
def boxminus(self, a, b):
    """Frame-checked boxminus on tagged values: ambient ⊟ ambient →
    tangent value."""

boxplus_sym abstractmethod

boxplus_sym(x_mx, delta_mx)

Symbolic boxplus: ambient + tangent → ambient. CasADi MX of shape (ambient_dim, 1) and (tangent_dim, 1).

Source code in manta/ir/manifold.py
@abstractmethod
def boxplus_sym(self, x_mx, delta_mx):
    """Symbolic boxplus: ambient + tangent → ambient. CasADi MX of
    shape (ambient_dim, 1) and (tangent_dim, 1)."""

boxminus_sym abstractmethod

boxminus_sym(a_mx, b_mx)

Symbolic boxminus: ambient − ambient → tangent.

Source code in manta/ir/manifold.py
@abstractmethod
def boxminus_sym(self, a_mx, b_mx):
    """Symbolic boxminus: ambient − ambient → tangent."""

boxplus_num abstractmethod

boxplus_num(x, delta)

Numeric boxplus on flat arrays.

Source code in manta/ir/manifold.py
@abstractmethod
def boxplus_num(self, x: np.ndarray, delta: np.ndarray) -> np.ndarray:
    """Numeric boxplus on flat arrays."""

default_value abstractmethod

default_value()

Python-side default value for fresh state (identity element for groups, zero for vectors). Returned as a numpy array of shape storage_shape or a scalar.

Source code in manta/ir/manifold.py
@abstractmethod
def default_value(self):
    """Python-side default value for fresh state (identity element
    for groups, zero for vectors). Returned as a numpy array of
    shape `storage_shape` or a scalar."""

ir_input abstractmethod

ir_input(name, *, default_frame=None)

Typed IR input symbol — Scalar/Vec3[frame]/Quat[from,to].

Source code in manta/ir/manifold.py
@abstractmethod
def ir_input(self, name: str, *, default_frame=None):
    """Typed IR *input* symbol — `Scalar`/`Vec3[frame]`/`Quat[from,to]`."""

ir_zero abstractmethod

ir_zero(*, default_frame=None)

Typed IR zero / identity value for this manifold.

Source code in manta/ir/manifold.py
@abstractmethod
def ir_zero(self, *, default_frame=None):
    """Typed IR zero / identity value for this manifold."""

ir_add abstractmethod

ir_add(value, delta_mx, *, default_frame=None)

Type-preserving value ⊕ delta_mx (raw MX), used by random-walk state updates: bias_next = bias + √dt · driver.

Source code in manta/ir/manifold.py
@abstractmethod
def ir_add(self, value, delta_mx, *, default_frame=None):
    """Type-preserving `value ⊕ delta_mx` (raw MX), used by random-walk
    state updates: `bias_next = bias + √dt · driver`."""

manta.ir.ScalarManifold dataclass

ScalarManifold()

Bases: Manifold

R^1 — scalar real. Backend kind: "scalar".

manta.ir.R3Manifold dataclass

R3Manifold(frame=None)

Bases: Manifold

R^3 — 3-vector. Backend kind: "vec" (size carried in storage_shape, not in the kind string, so a future R6 reuses the same backend dispatch).

manta.ir.SO3Manifold dataclass

SO3Manifold(from_frame=None, to_frame=None)

Bases: Manifold

SO(3) — rotations stored as a unit quaternion (w, x, y, z).

Ambient 4 (quat) / tangent 3 (axis-angle). Backend kind: "quat". Left-trivialization convention: the tangent vector lives in the rotation's from_frame, and

q ⊞ δ = exp(δ) ⊗ q

from_frame / to_frame parametrize the underlying Quat — a Quat[from, to] rotates a vector in to coords into from coords. Both must be provided when used as a user-declared State manifold (no Frame default — pick the application's convention; the framework's rigid-body orientation uses Quat[WorldFrame, CraftFrame], so an attitude estimator typically matches that). Codegen consumes only kind / storage_shape; the value-typed ops derive their frames from the Quat argument, so a frameless SO3Manifold() is a valid operator for already-frame-tagged values.

boxplus

boxplus(q, delta)

q ⊞ δ with δ in q's from_frame (left trivialization). Returns a Quat carrying q's frames.

Source code in manta/ir/manifold.py
def boxplus(self, q: Quat, delta: Vec3) -> Quat:
    """q ⊞ δ with δ in q's from_frame (left trivialization). Returns
    a Quat carrying q's frames."""
    if not isinstance(q, Quat):
        raise TypeError(
            f"SO3Manifold.boxplus: q must be a Quat, got {type(q).__name__}")
    if not isinstance(delta, Vec3):
        raise TypeError(
            "SO3Manifold.boxplus: delta must be a Vec3, got "
            f"{type(delta).__name__}")
    if delta._frame is not q._from_frame:
        raise FrameError(
            "SO3Manifold.boxplus",
            expected=f"delta frame matches q.from_frame "
                     f"({q._from_frame.__name__})",
            got=f"delta_frame={delta._frame.__name__}",
            source=_capture_user_source(),
        )
    new_mx = self.boxplus_sym(q._mx, delta._mx)
    return Quat(new_mx, from_frame=q._from_frame, to_frame=q._to_frame)

boxminus

boxminus(a, b)

δ = log(a ⊗ b⁻¹), returned in a's from_frame tangent space.

Source code in manta/ir/manifold.py
def boxminus(self, a: Quat, b: Quat) -> Vec3:
    """δ = log(a ⊗ b⁻¹), returned in a's from_frame tangent space."""
    if not isinstance(a, Quat) or not isinstance(b, Quat):
        raise TypeError(
            "SO3Manifold.boxminus: a and b must be Quat, got "
            f"{type(a).__name__} / {type(b).__name__}")
    if (a._from_frame is not b._from_frame
            or a._to_frame is not b._to_frame):
        raise FrameError(
            "SO3Manifold.boxminus",
            expected=f"matching Quat frames "
                     f"({a._from_frame.__name__}, {a._to_frame.__name__})",
            got=f"({b._from_frame.__name__}, {b._to_frame.__name__})",
            source=_capture_user_source(),
        )
    return Vec3(self.boxminus_sym(a._mx, b._mx), frame=a._from_frame)

State spec

manta.ir.StateSpec

StateSpec(slots)

Layout descriptor for a flat ambient state vector.

Source code in manta/ir/state_spec.py
def __init__(self, slots: list[StateSlot]) -> None:
    self._slots = list(slots)
    self._slot_by_name = {s.name: s for s in self._slots}
    self._ambient_dim = sum(s.ambient_dim for s in self._slots)
    self._tangent_dim = sum(s.tangent_dim for s in self._slots)

from_craft classmethod

from_craft(craft)

Build the canonical state spec for a single-craft world.

Layout: position (3, R3) orientation (4, SO3 → tangent 3) velocity (3, R3) angular_velocity (3, R3) . (1, R1) in declaration order

Future: multi-craft worlds will concatenate per-craft layouts here.

Source code in manta/ir/state_spec.py
@classmethod
def from_craft(cls, craft) -> "StateSpec":
    """Build the canonical state spec for a single-craft world.

    Layout:
      position           (3, R3)
      orientation        (4, SO3 → tangent 3)
      velocity           (3, R3)
      angular_velocity   (3, R3)
      <part>.<slot>      (1, R1)  in declaration order

    Future: multi-craft worlds will concatenate per-craft layouts here.
    """
    slots: list[StateSlot] = []
    offset = 0
    tan_offset = 0

    def add(name: str, manifold: Manifold):
        nonlocal offset, tan_offset
        slots.append(StateSlot(name=name,
                               ambient_offset=offset,
                               manifold=manifold,
                               tangent_offset=tan_offset))
        offset += manifold.ambient_dim
        tan_offset += manifold.tangent_dim

    cls._add_craft_slots(craft, "", add)
    return cls(slots)

from_world classmethod

from_world(world)

Build the joint state spec for a World — every craft's slots concatenated, each prefixed with <craft.name>..

Slot ordering: per-craft in world.crafts order; within each craft, the same per-craft layout as from_craft.

Source code in manta/ir/state_spec.py
@classmethod
def from_world(cls, world) -> "StateSpec":
    """Build the joint state spec for a World — every craft's slots
    concatenated, each prefixed with `<craft.name>.`.

    Slot ordering: per-craft in world.crafts order; within each
    craft, the same per-craft layout as `from_craft`.
    """
    slots: list[StateSlot] = []
    offset = 0
    tan_offset = 0

    def add(name: str, manifold: Manifold):
        nonlocal offset, tan_offset
        slots.append(StateSlot(name=name,
                               ambient_offset=offset,
                               manifold=manifold,
                               tangent_offset=tan_offset))
        offset += manifold.ambient_dim
        tan_offset += manifold.tangent_dim

    for craft in world.crafts:
        cls._add_craft_slots(craft, f"{craft.name}.", add)
    for field in world.fields:
        cls._add_field_disturbance_slots(field, add)
    return cls(slots)

from_layout classmethod

from_layout(layout)

Build a spec from an ordered list of (name, Manifold) pairs.

The world-agnostic constructor: a RecurrenceBlock (PID, Madgwick, the IMU integrator) declares its internal state this way, with no craft / world to walk. Offsets densify from 0 in the given order.

Source code in manta/ir/state_spec.py
@classmethod
def from_layout(cls, layout) -> "StateSpec":
    """Build a spec from an ordered list of `(name, Manifold)` pairs.

    The world-agnostic constructor: a `RecurrenceBlock` (PID, Madgwick,
    the IMU integrator) declares its internal state this way, with no
    craft / world to walk. Offsets densify from 0 in the given order.
    """
    slots: list[StateSlot] = []
    offset = 0
    tan_offset = 0
    for name, manifold in layout:
        slots.append(StateSlot(name=name,
                               ambient_offset=offset,
                               manifold=manifold,
                               tangent_offset=tan_offset))
        offset += manifold.ambient_dim
        tan_offset += manifold.tangent_dim
    return cls(slots)

subset classmethod

subset(full_spec, kept_names)

Build a standalone spec over a subset of full_spec's slots.

Slots are kept in their original full_spec order; ambient and tangent offsets re-densify from 0 so every existing method (pack/unpack/boxplus/boxminus) works on the result unchanged.

Source code in manta/ir/state_spec.py
@classmethod
def subset(cls, full_spec: "StateSpec",
           kept_names) -> "StateSpec":
    """Build a standalone spec over a subset of `full_spec`'s slots.

    Slots are kept in their original `full_spec` order; ambient and
    tangent offsets re-densify from 0 so every existing method
    (pack/unpack/boxplus/boxminus) works on the result unchanged.
    """
    kept = set(kept_names)
    slots: list[StateSlot] = []
    offset = 0
    tan_offset = 0
    for s in full_spec.slots:
        if s.name not in kept:
            continue
        slots.append(StateSlot(name=s.name,
                               ambient_offset=offset,
                               manifold=s.manifold,
                               tangent_offset=tan_offset))
        offset += s.ambient_dim
        tan_offset += s.tangent_dim
    return cls(slots)

slot

slot(name)

Lookup a slot by name.

Exact match wins. As a convenience for single-craft worlds, a suffix match also works — spec.slot("position") finds "drone.position" when there's exactly one such slot. The suffix match raises if ambiguous (the shared resolve_suffix rule).

Source code in manta/ir/state_spec.py
def slot(self, name: str) -> StateSlot:
    """Lookup a slot by name.

    Exact match wins. As a convenience for single-craft worlds, a
    suffix match also works — `spec.slot("position")` finds
    `"drone.position"` when there's exactly one such slot. The
    suffix match raises if ambiguous (the shared `resolve_suffix`
    rule)."""
    full = resolve_suffix(name, self._slot_by_name,
                          label="slot", who="StateSpec.slot")
    return self._slot_by_name[full]

pack

pack(state_dict)

Flatten a tick-style dict into the ambient vector.

Source code in manta/ir/state_spec.py
def pack(self, state_dict: dict[str, Any]) -> np.ndarray:
    """Flatten a tick-style dict into the ambient vector."""
    flat = np.zeros(self._ambient_dim, dtype=float)
    for slot in self._slots:
        value = state_dict[slot.name]
        arr = np.atleast_1d(np.asarray(value, dtype=float)).reshape(-1)
        if arr.size != slot.ambient_dim:
            raise ValueError(
                f"StateSpec.pack: slot {slot.name!r} expects dim {slot.ambient_dim}, "
                f"got len={arr.size}")
        flat[slot.ambient_offset : slot.ambient_offset + slot.ambient_dim] = arr
    return flat

unpack

unpack(flat)

Slice an ambient vector back into a tick-style dict.

Source code in manta/ir/state_spec.py
def unpack(self, flat: np.ndarray) -> dict[str, Any]:
    """Slice an ambient vector back into a tick-style dict."""
    flat = np.asarray(flat, dtype=float)
    if flat.shape != (self._ambient_dim,):
        raise ValueError(
            f"StateSpec.unpack: expected shape ({self._ambient_dim},), "
            f"got {flat.shape}")
    out: dict[str, Any] = {}
    for slot in self._slots:
        chunk = flat[slot.ambient_offset : slot.ambient_offset + slot.ambient_dim]
        if slot.ambient_dim == 1:
            out[slot.name] = float(chunk[0])
        else:
            out[slot.name] = chunk.copy()
    return out

pack_any

pack_any(state=None, *, base=None)

Pack a state given in ANY of the three shapes — nested dict ({owner: {slot: v}}), flat dict ({"owner.slot": v}), or ambient vector — merged over base (ambient vector or dict), into the ambient vector. The single owner of shape conversion: every runtime/analysis call site goes through here instead of hand-rolling the flatten + merge + filter loop.

Rules: * state is an ndarray → taken verbatim (length-checked). * state is a dict → flattened; unknown keys ignored; merged over base. * state is None → base packed as-is. * every slot must be covered by state or base.

Source code in manta/ir/state_spec.py
def pack_any(self, state=None, *, base=None) -> np.ndarray:
    """Pack a state given in ANY of the three shapes — nested dict
    (`{owner: {slot: v}}`), flat dict (`{"owner.slot": v}`), or ambient
    vector — merged over `base` (ambient vector or dict), into the
    ambient vector. The single owner of shape conversion: every
    runtime/analysis call site goes through here instead of hand-rolling
    the flatten + merge + filter loop.

    Rules:
      * `state` is an ndarray → taken verbatim (length-checked).
      * `state` is a dict     → flattened; unknown keys ignored; merged
        over `base`.
      * `state` is None       → `base` packed as-is.
      * every slot must be covered by `state` or `base`.
    """
    if isinstance(state, np.ndarray):
        arr = np.asarray(state, dtype=float).reshape(-1)
        if arr.shape != (self._ambient_dim,):
            raise ValueError(
                f"StateSpec.pack_any: expected ambient vector of length "
                f"{self._ambient_dim}, got {arr.shape}")
        return arr.copy()
    merged: dict[str, Any] = {}
    if base is not None:
        merged.update(self.unpack(np.asarray(base, dtype=float))
                      if isinstance(base, np.ndarray)
                      else flatten_nested(base))
    if state is not None:
        merged.update(flatten_nested(state))
    return self.pack({k: v for k, v in merged.items() if k in self})

to_nested

to_nested(flat_vector)

Ambient vector → nested {owner: {slot: v}} dict (the runtime's user-facing shape). A dotless slot name stays a top-level entry.

Source code in manta/ir/state_spec.py
def to_nested(self, flat_vector: np.ndarray) -> dict[str, Any]:
    """Ambient vector → nested `{owner: {slot: v}}` dict (the runtime's
    user-facing shape). A dotless slot name stays a top-level entry."""
    nested: dict[str, Any] = {}
    for full, val in self.unpack(np.asarray(flat_vector,
                                            dtype=float)).items():
        if "." in full:
            owner, slot = full.split(".", 1)
            nested.setdefault(owner, {})[slot] = val
        else:
            nested[full] = val
    return nested

boxplus_sym

boxplus_sym(x_ambient, delta_tangent)

Apply a tangent-space perturbation to an ambient state.

Per-slot manifold operation: R3 / R1 → x_new = x + δ SO3 → q_new = boxplus(q, δ_θ) (quaternion + axis-angle)

Returns an ambient-dimensioned MX. Used by the ESKF to build a symbolic perturbed state for Jacobian extraction:

δ_out = boxminus(f(boxplus(x, δ_in)), f(x))
F     = ∂δ_out / ∂δ_in  evaluated at δ_in = 0
Source code in manta/ir/state_spec.py
def boxplus_sym(self, x_ambient: ca.MX, delta_tangent: ca.MX) -> ca.MX:
    """Apply a tangent-space perturbation to an ambient state.

    Per-slot manifold operation:
      R3 / R1    →  x_new = x + δ
      SO3        →  q_new = boxplus(q, δ_θ)         (quaternion + axis-angle)

    Returns an ambient-dimensioned MX. Used by the ESKF to build a
    symbolic perturbed state for Jacobian extraction:

        δ_out = boxminus(f(boxplus(x, δ_in)), f(x))
        F     = ∂δ_out / ∂δ_in  evaluated at δ_in = 0
    """
    chunks: list[ca.MX] = []
    for slot in self._slots:
        x_chunk = x_ambient[slot.ambient_offset : slot.ambient_offset + slot.ambient_dim]
        d_chunk = delta_tangent[
            slot.tangent_offset : slot.tangent_offset + slot.tangent_dim]
        chunks.append(slot.manifold.boxplus_sym(x_chunk, d_chunk))
    return ca.vertcat(*chunks)

boxminus_sym

boxminus_sym(x_a, x_b)

Compute the tangent-space delta between two ambient states.

Per-slot: R3 / R1 → δ = x_a − x_b SO3 → δ_θ = boxminus(q_a, q_b) = log(q_a ⊗ q_b⁻¹)

Source code in manta/ir/state_spec.py
def boxminus_sym(self, x_a: ca.MX, x_b: ca.MX) -> ca.MX:
    """Compute the tangent-space delta between two ambient states.

    Per-slot:
      R3 / R1    →  δ = x_a − x_b
      SO3        →  δ_θ = boxminus(q_a, q_b) = log(q_a ⊗ q_b⁻¹)
    """
    chunks: list[ca.MX] = []
    for slot in self._slots:
        a_chunk = x_a[slot.ambient_offset : slot.ambient_offset + slot.ambient_dim]
        b_chunk = x_b[slot.ambient_offset : slot.ambient_offset + slot.ambient_dim]
        chunks.append(slot.manifold.boxminus_sym(a_chunk, b_chunk))
    return ca.vertcat(*chunks)

boxplus_num

boxplus_num(x_ambient, delta_tangent)

Numeric (numpy) boxplus. Used by the ESKF update step to apply the Kalman correction onto the manifold without going through a compiled symbolic function.

Source code in manta/ir/state_spec.py
def boxplus_num(self, x_ambient: np.ndarray,
                delta_tangent: np.ndarray) -> np.ndarray:
    """Numeric (numpy) boxplus. Used by the ESKF update step to apply
    the Kalman correction onto the manifold without going through a
    compiled symbolic function."""
    x = np.asarray(x_ambient, dtype=float).copy()
    d = np.asarray(delta_tangent, dtype=float)
    for slot in self._slots:
        x_chunk = x[slot.ambient_offset : slot.ambient_offset + slot.ambient_dim]
        d_chunk = d[slot.tangent_offset :
                    slot.tangent_offset + slot.tangent_dim]
        new_chunk = slot.manifold.boxplus_num(x_chunk, d_chunk)
        x[slot.ambient_offset : slot.ambient_offset + slot.ambient_dim] = new_chunk
    return x

manta.ir.StateSlot dataclass

StateSlot(name, ambient_offset, manifold, tangent_offset)

One named slot in the flat layout.

Attrs: name — string key in the tick dict (e.g., "position", "wheel.angle"). Same key the tick function uses. ambient_offset — start index in the ambient vector. manifold — Manifold instance describing the slot's storage and tangent dims. ambient_dim and tangent_dim are derived. Backends key on manifold.kind. tangent_offset — start index in the tangent vector.

The ambient pair (ambient_offset/ambient_dim) and the tangent pair (tangent_offset/tangent_dim) are named symmetrically on purpose: covariance / Jacobian work indexes by the tangent pair, packing / unpacking by the ambient pair, and mixing them is a classic bug.

manta.ir.SlotSet

Bases: IntFlag

Aliases for the rigid-body slots an EKF should track.

Combine with |::

EKF(world, track={"drone": POSE | TWIST})

The composites map to the rigid-body slot-name suffixes (position / orientation / velocity / angular_velocity). They name ONLY the rigid-body block — part R1/R3 states, RW-bias slots, and disturbance slots are never selected by an alias; they enter the tracked set via dynamics closure or because a chosen sensor observes them.

Wrench

manta.ir.Wrench

Wrench(force, torque)

Force + torque pair sharing a frame.

Construction::

w = Wrench(force=Vec3[F](...), torque=Vec3[F](...))
w = Wrench.zero(F)
Source code in manta/ir/wrench.py
def __init__(self, force: Vec3, torque: Vec3) -> None:
    if not isinstance(force, Vec3) or not isinstance(torque, Vec3):
        raise TypeError(
            f"Wrench: force and torque must be Vec3; "
            f"got {type(force).__name__}, {type(torque).__name__}")
    if force._frame is not torque._frame:
        raise FrameError(
            "Wrench",
            expected=f"both components in the same frame "
                     f"({force._frame.__name__})",
            got=f"force={force._frame.__name__}, "
                f"torque={torque._frame.__name__}",
            source=_capture_user_source(),
        )
    self.force = force
    self.torque = torque
    self._frame = force._frame