Skip to content

Parts

A Part is an atomic unit of behavior on a craft. The declaration sentinels (Parameter, State, Input, Output, Noise) are documented on the base classes; the stock parts below are what you add to a craft.

Declaration model

manta.parts.Part

Part(name, **overrides)

Bases: DeclarationHost

Base class for all parts.

Subclasses declare their interface via class-attribute Parameter (and later Input/State) entries, then implement update(ctx) to contribute a Wrench per tick.

Every Part has a static mount pose relative to its parent's output frame: mount_offset, an (x, y, z) position, and mount_orientation, a wxyz quaternion rotating the part's OWN axes into the parent's output frame.

Both carry the mount_ prefix deliberately. It separates the STATIC installation from the DYNAMIC kinematics that share the same vocabulary — ctx.position and ctx.orientation are what the part is doing this tick, self.mount_offset and self.mount_orientation are where it was bolted — and it leaves orientation free for the thing parts actually output (an AHRS reports one). The framework uses the pose to express the part's kinematics and to roll its wrench up into the parent's frame (force-at-offset → torque contribution at parent origin).

The two compose in the order you would bolt the thing on: the offset is measured in the PARENT's frame ("put it here"), then the rotation turns the part in place ("pointing that way"). So a thruster canted 30° outboard is one mount_orientation, not a hand- rotated thrust vector, and a sensor mounted on its side reports in a frame that is genuinely rotated rather than one the measurement function has to correct after the fact.

Every Part also has a parent attribute — either another Part (typically a CompositePart like the craft's RootPart or a joint) or None for the unattached state. Parents are set by CompositePart.add(child) when a child is attached. The craft's part tree is rooted at Craft.root.

Construction signature::

class Mass(Part):
    mass: Scalar = Parameter(1.0)

Mass("body")                            # at origin of parent
Mass("battery", mass=2.0,
     mount_offset=(0.0, 0.0, -0.5))        # 0.5 m below parent origin
Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

mounted_upright property

mounted_upright

True when this part's static mount rotation is identity — the common case, and the fast path the kinematic pass takes.

noise_R

noise_R(name)

Measurement-noise covariance for a declared Noise slot.

Reads the per-instance <name>_sigma attribute (set at construction time, default from the declaration). Returns: * σ² (float) for scalar noise. * σ²·I_d (np.ndarray, d×d) for d-vector noise (sized off signal_manifold.ambient_dim).

User-facing convenience: the framework's own EKF wiring reads <name>_sigma directly, but hand-driven filter code can size a measurement update without restating σ²: ekf.update(h, z, R=imu.noise_R("gyro_noise")).

Source code in manta/parts/base.py
def noise_R(self, name: str) -> Any:
    """Measurement-noise covariance for a declared Noise slot.

    Reads the per-instance `<name>_sigma` attribute (set at
    construction time, default from the declaration). Returns:
        * `σ²` (float) for scalar noise.
        * `σ²·I_d` (np.ndarray, d×d) for d-vector noise (sized
          off `signal_manifold.ambient_dim`).

    User-facing convenience: the framework's own EKF wiring reads
    `<name>_sigma` directly, but hand-driven filter code can size a
    measurement update without restating σ²:
        `ekf.update(h, z, R=imu.noise_R("gyro_noise"))`.
    """
    decls = self.noise_declarations()
    if name not in decls:
        raise KeyError(
            f"{type(self).__name__}({self.name!r}): no Noise slot "
            f"named {name!r}. Declared: {sorted(decls)}")
    decl = decls[name]
    sigma = float(getattr(self, f"{name}_sigma"))
    var = sigma ** 2
    d = decl.signal_manifold.ambient_dim
    if d == 1:
        return var
    return var * np.eye(d)

update

update(ctx)

Compute this part's wrench contribution for the current tick. ctx is the manta.craft.TickContext. Subclasses must override and return a Wrench or PartUpdate.

Source code in manta/parts/base.py
def update(self, ctx):
    """Compute this part's wrench contribution for the current tick.
    `ctx` is the `manta.craft.TickContext`. Subclasses must override
    and return a `Wrench` or `PartUpdate`."""
    raise NotImplementedError(
        f"{type(self).__name__}: must override update(self, ctx)")

on_world_resolve

on_world_resolve(world, craft)

Compile-time resolution hook — called once per part by snapshot resolution, after planets and field sources have registered their disturbances, with the world and this part's craft. The generic slot for anything a part can only do against the finished model:

  • resolve cross-craft wiring (a camera collecting the optical ellipsoids it can see),
  • validate structural invariants that need the complete part tree (a thermal link's same-craft check, a root-mount requirement)

so a user-authored part gets the same compile-time treatment as the stock ones — no isinstance ladder in World resolution. Default: no-op. Raise to reject a bad configuration at resolution time (before any tracing) rather than mid-trace.

Source code in manta/parts/base.py
def on_world_resolve(self, world, craft) -> None:
    """Compile-time resolution hook — called once per part by
    snapshot resolution, after planets and field sources have
    registered their disturbances, with the world and this part's
    craft. The generic slot for anything a part can only do against
    the *finished* model:

      * resolve cross-craft wiring (a camera collecting the optical
        ellipsoids it can see),
      * validate structural invariants that need the complete part
        tree (a thermal link's same-craft check, a root-mount
        requirement)

    so a user-authored part gets the same compile-time treatment as
    the stock ones — no `isinstance` ladder in World resolution.
    Default: no-op. Raise to reject a bad configuration at
    resolution time (before any tracing) rather than mid-trace."""

manta.parts.CompositePart

CompositePart(name, **overrides)

Bases: Part

A Part that hosts other Parts as children.

Children mount on this part's output frame. For a non-joint CompositePart the output frame is identical to the part's own frame — which is its parent's output frame displaced by mount_offset and turned by mount_orientation. An ArticulatedJoint overrides this — a RevoluteJoint's output frame additionally rotates by the joint angle (a PrismaticJoint's translates by its displacement).

add(child) appends a child Part, sets its parent to self, and returns the child (so chained construction reads naturally):

gimbal = pan.add(RevoluteJoint("tilt", axis=(0, 1, 0)))
gimbal.add(Mass("camera", mass=0.05, mount_offset=(0.1, 0, 0)))
Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    self._children: list[Part] = []

remove

remove(child)

Detach one direct child by instance or name and return it.

Source code in manta/parts/base.py
def remove(self, child: Part | str) -> Part:
    """Detach one direct child by instance or name and return it."""
    match = next(
        (candidate for candidate in self._children
         if candidate is child
         or (isinstance(child, str) and candidate.name == child)),
        None,
    )
    if match is None:
        label = child if isinstance(child, str) else getattr(child, "name", child)
        raise KeyError(
            f"{type(self).__name__}('{self.name}').remove: no direct "
            f"child {label!r}")
    self._children.remove(match)
    match.parent = None
    return match

walk

walk()

DFS over this part's subtree, yielding self then each descendant.

Source code in manta/parts/base.py
def walk(self):
    """DFS over this part's subtree, yielding self then each descendant."""
    yield self
    for child in self._children:
        if isinstance(child, CompositePart):
            yield from child.walk()
        else:
            yield child

update

update(ctx)

CompositePart has no intrinsic wrench contribution by default — subclasses (RootPart, joints, etc.) override if they need to. PartFrame, like every part's update: the tick rolls each wrench up from the part's own frame, so a CraftFrame zero here would FrameError the compile for any non-root composite.

Source code in manta/parts/base.py
def update(self, ctx):
    """CompositePart has no intrinsic wrench contribution by default —
    subclasses (RootPart, joints, etc.) override if they need to.
    PartFrame, like every part's update: the tick rolls each wrench
    up from the part's own frame, so a CraftFrame zero here would
    FrameError the compile for any non-root composite."""
    from ..ir.frames import PartFrame
    from ..ir.wrench import Wrench
    return Wrench.zero(PartFrame)

manta.parts.Parameter

Parameter(default, *, manifold=None, frame=None, allow_infinite=False, numeric=True)

Bases: _Declaration

Frozen-at-config-time value. Set when the user constructs a Part, used as a constant during graph tracing.

Concrete attribute types are deduced from the default value at init time — a Parameter(1.0) becomes a Python float; a Parameter((1.0, 0.0, 0.0)) stays a tuple until the part's update() promotes it to an IR vector (Vec3[F].constant / Vec3[F].coerce).

Args: manifold — optional Manifold instance or shortcut string ("R1", "R3"; same vocabulary as Noise). Declaring it makes the parameter promotable: a transform constructed with parameters=[...] (system identification — see manta.fit) can promote it from a baked graph constant to a live graph input named <craft>.<part>.<param>. Inside update() a promoted parameter reads as an IR value (the trace binds it), so parts consume promotable parameters through the .coerce factory, which accepts both forms. None (default) — a plain Python config value. frame — Frame tag, consumed when manifold is a shortcut resolving to a vector manifold. The promoted input's frame; must match what update() composes it with. numeric — whether the declaration participates in the framework's finite-number validation. Set false only for typed object configuration whose protocol the owning Part validates.

Source code in manta/parts/_declarations.py
def __init__(self, default: Any, *, manifold=None, frame=None,
             allow_infinite: bool = False,
             numeric: bool = True) -> None:
    super().__init__(default)
    if not isinstance(numeric, bool):
        raise TypeError("Parameter.numeric must be a bool")
    if not numeric and (manifold is not None or allow_infinite):
        raise ValueError(
            "non-numeric Parameters cannot declare a manifold or "
            "allow_infinite")
    self.allow_infinite = allow_infinite
    self.numeric = numeric
    if manifold is None:
        self.manifold = None
    else:
        from ..ir.manifold import Manifold, manifold_from_shortcut
        self.manifold = (manifold if isinstance(manifold, Manifold)
                         else manifold_from_shortcut(manifold,
                                                     frame=frame))

manta.parts.State

State(init, manifold='R1', frame=None)

Bases: _Declaration

Per-tick state slot.

Declared at class scope. The framework: * Creates a graph input named "." each compile. * Rebinds the part attribute to that input node before calling update(), so self.<state_name> reads the symbolic current value. * Reads the new value from PartUpdate.new_state["<state_name>"] and emits it as a graph output of the same name. Omitted states pass through unchanged.

Args: init Python value (default initial value across compiles). For R1 a float; for R3 a length-3 tuple / ndarray; for SO(3) a length-4 quaternion (w, x, y, z). manifold String shortcut ('R1', 'R3') or a Manifold instance. SO(3) state is fully supported — pass an explicit SO3Manifold(from_frame=..., to_frame=...) instance (the string shortcut is intentionally disallowed because SO(3) needs the dual-frame parametrization). The slot then evolves on the manifold: the part integrates it with manifold.boxplus(q, ω·dt), the framework keeps it unit-normalized, and the EKF/LQR linearization gives it a 3-dim tangent automatically. See tests/test_so3_state. frame Frame tag for R3 state. Default CraftFrame. Ignored for R1 and SO(3) (the latter's frames live on the manifold). Folded into the Manifold instance.

state.manifold always reads back as a Manifold instance; the string form is normalized at construction.

Source code in manta/parts/_declarations.py
def __init__(self, init, manifold="R1", frame=None) -> None:
    from ..ir.manifold import (
        Manifold,
        R3Manifold,
        SO3Manifold,
        manifold_from_shortcut,
    )
    if isinstance(manifold, Manifold):
        mfd = manifold
    else:
        # The shared shortcut grammar (`manifold_from_shortcut`) accepts
        # any R<n>, but State slots are deliberately restricted to the
        # manifolds the integrator/EKF/LQR plumbing is exercised on:
        # R1, R3, and SO(3). SO(3) is excluded from the shortcut form
        # (not the restriction) because it needs explicit
        # from_frame/to_frame.
        if manifold not in ("R1", "R3"):
            raise NotImplementedError(
                f"State.manifold={manifold!r}: state manifolds are "
                f"deliberately restricted to 'R1' / 'R3' (and SO(3)) "
                f"for now, though the shared shortcut grammar is wider. "
                f"For SO(3), pass an explicit "
                f"SO3Manifold(from_frame=..., to_frame=...) instance "
                f"to capture the dual-frame parametrization.")
        mfd = manifold_from_shortcut(manifold, frame=frame)
    if isinstance(mfd, R3Manifold):
        try:
            t = tuple(float(x) for x in init)
        except (TypeError, ValueError):
            raise ValueError(
                f"State(manifold='R3'): init must be a 3-element "
                f"sequence, got {init!r}")
        if len(t) != 3:
            raise ValueError(
                f"State(manifold='R3'): init must be length-3, got "
                f"{init!r}")
        init = t
    elif isinstance(mfd, SO3Manifold):
        if mfd.from_frame is None or mfd.to_frame is None:
            raise ValueError(
                f"State(SO3Manifold): from_frame and to_frame must "
                f"both be specified — got from_frame={mfd.from_frame!r}, "
                f"to_frame={mfd.to_frame!r}.")
        try:
            t = tuple(float(x) for x in init)
        except (TypeError, ValueError):
            raise ValueError(
                f"State(SO3Manifold): init must be a length-4 "
                f"quaternion (w, x, y, z), got {init!r}")
        if len(t) != 4:
            raise ValueError(
                f"State(SO3Manifold): init must be length-4, got "
                f"{init!r}")
        init = t
    super().__init__(default=init)
    self.init     = init
    self.manifold = mfd
    # Keep the explicit `frame` attribute for read sites that
    # consult it directly; for R3 it mirrors the manifold's frame.
    # For SO3 the dual frames live on the manifold itself.
    self.frame = (mfd.frame if isinstance(mfd, R3Manifold) else frame)

manta.parts.Input

Input(default)

Bases: _Declaration

Per-tick external value.

Declared at class scope on a Part. The framework: * Creates a graph input named "." each compile. * Rebinds the part attribute to the symbolic node before calling update(), so self.<input_name> reads the current value. * Initial state from Craft.initial_state() includes the input slot seeded with the declaration's default (or the construction-time override if the user passed one). * Inputs pass through Sim.step's merge — they persist between steps until the user overrides. This makes per-tick commands ergonomic: set once, tick repeatedly, change when you want.

Args: default — Python value used to seed the initial state. May be overridden at construction (Motor("m", torque_cmd=0.5)) in which case the override becomes the seed.

The semantic distinction from Parameter: Parameter values are frozen into the compiled graph as constants; Input values are re-evaluated each tick from the state dict.

Source code in manta/parts/_declarations.py
def __init__(self, default: Any) -> None:
    self.default = default

manta.parts.Output

Output()

Bases: _Declaration

Per-tick value produced by a part (sensor reading, derived quantity, telemetry signal).

Declared at class scope. The part writes its computed value via PartUpdate.outputs["<name>"] = <Vec3 | Scalar | …>. The framework emits the value as a graph output named "."; tick callers read it from the result dict (read-only, doesn't round-trip back as next-tick state). The output's shape is whatever the part writes — nothing downstream needs it declared.

Source code in manta/parts/_declarations.py
def __init__(self) -> None:
    super().__init__(default=None)

manta.parts.Noise

Noise(signal_manifold='R3', *, frame=None, sigma=0.0)

Bases: _Declaration

Abstract base for noise-channel declarations.

Subclasses set class-level metadata (kind, contributes_state) and implement synthesize() (the per-tick IR plumbing). Backends key on signal_manifold.kind via their own registry — no isinstance(WhiteNoise) dispatch anywhere in the codebase.

Concrete subclasses:

  • WhiteNoise — per-tick i.i.d. Gaussian. The framework creates a graph input named <part>.<noise_name>, rebinds the part attribute to that input, and the part adds it directly into its sensor reading (or process expression). σ is the per-tick measurement stddev. kind = "white".

  • RandomWalkNoise — random-walk bias. The framework synthesizes:

    • A state slot <part>.<noise_name> holding the bias.
    • A driver noise input <part>.<noise_name>_driver.
    • A state update each tick: bias_next = bias + sqrt(dt) · driver, driver ~ N(0, σ²). Inside update(), self.<noise_name> reads the bias state (the slowly-drifting current value). σ has continuous σ/√Hz semantics; per-tick bias variance is dt·σ². kind = "random_walk".
  • GaussMarkovNoise — first-order Gauss–Markov (exponentially correlated) error with correlation time τ and stationary variance σ². Same state-slot / driver plumbing as the random walk, with the exact discrete transition φ = exp(-dt/τ), e_next = φ · e + sqrt(1 − φ²) · driver, driver ~ N(0, σ²), so the slot's variance stays at σ² in steady state and the auto-assembled process noise is (1 − φ²)·σ² per tick — no Euler approximation anywhere. kind = "gauss_markov".

Args: signal_manifold — Manifold instance OR shortcut string. The manifold of the symbol user code reads as self.<name>. Shortcuts: "R1" (scalar), "R3" (combine with frame=). Default "R3". Same vocabulary as State(manifold=). frame — Frame class, only consumed when signal_manifold is a shortcut and resolves to a vector-typed manifold. Ignored otherwise. sigma — 1-σ standard deviation, scalar (isotropic across axes). See subclass docstrings for unit conventions.

Source code in manta/parts/_declarations.py
def __init__(self, signal_manifold="R3", *, frame=None,
             sigma: float = 0.0) -> None:
    super().__init__(default=None)
    from ..ir.manifold import manifold_from_shortcut
    self.signal_manifold = manifold_from_shortcut(
        signal_manifold, frame=frame)
    self.sigma = float(sigma)
    if self.sigma < 0.0:
        raise ValueError(
            f"{type(self).__name__}: sigma must be >= 0, "
            f"got {sigma!r}")

resolved_signal_manifold

resolved_signal_manifold(*, default_frame=None)

Return self.signal_manifold with any unresolved frame substituted from default_frame. Used at IR synthesis time; the unresolved form keeps R3Manifold(frame=None) legal so a part can declare a noise without committing to a frame until the compiler knows which one it's in (CraftFrame for parts, WorldFrame for disturbances).

Source code in manta/parts/_declarations.py
def resolved_signal_manifold(self, *, default_frame=None):
    """Return `self.signal_manifold` with any unresolved frame
    substituted from `default_frame`. Used at IR synthesis time;
    the unresolved form keeps `R3Manifold(frame=None)` legal so
    a part can declare a noise without committing to a frame
    until the compiler knows which one it's in (CraftFrame for
    parts, WorldFrame for disturbances)."""
    from ..ir.manifold import R3Manifold
    if isinstance(self.signal_manifold, R3Manifold) \
            and self.signal_manifold.frame is None:
        return R3Manifold(frame=default_frame)
    return self.signal_manifold

state_manifold

state_manifold(*, default_frame=None)

Manifold of the synthesized state slot, or None. For RW the state lives in the same space as the per-tick signal.

Source code in manta/parts/_declarations.py
def state_manifold(self, *, default_frame=None):
    """Manifold of the synthesized state slot, or None. For RW
    the state lives in the same space as the per-tick signal."""
    if not self.contributes_state:
        return None
    return self.resolved_signal_manifold(default_frame=default_frame)

runtime_attributes

runtime_attributes(name)

The per-instance attributes this channel exposes on its owner, attr -> (default, allow_zero). DeclarationHost seeds them from the declaration and accepts constructor overrides of the same names; is_active / synthesize read them back at compile time. Every channel has <name>_sigma; subclasses add their own (GaussMarkovNoise adds <name>_tau).

Source code in manta/parts/_declarations.py
def runtime_attributes(self, name: str) -> dict[str, tuple[float, bool]]:
    """The per-instance attributes this channel exposes on its owner,
    `attr -> (default, allow_zero)`. `DeclarationHost` seeds them from
    the declaration and accepts constructor overrides of the same
    names; `is_active` / `synthesize` read them back at compile time.
    Every channel has `<name>_sigma`; subclasses add their own
    (`GaussMarkovNoise` adds `<name>_tau`)."""
    return {f"{name}_sigma": (self.sigma, True)}

is_active

is_active(owner, name)

Is this channel currently producing nonzero output? Reads the runtime <name>_sigma attribute on the owner.

Source code in manta/parts/_declarations.py
def is_active(self, owner, name: str) -> bool:
    """Is this channel currently producing nonzero output? Reads
    the runtime `<name>_sigma` attribute on the owner."""
    return float(getattr(owner, f"{name}_sigma")) > 0.0

driver_input_name

driver_input_name(name)

The name of this channel's per-tick stochastic input. For White noise the signal IS the driver (same name); for RW the driver is a separate <name>_driver input distinct from the bias state name.

Source code in manta/parts/_declarations.py
def driver_input_name(self, name: str) -> str:
    """The name of this channel's per-tick stochastic input. For
    White noise the signal IS the driver (same name); for RW the
    driver is a separate `<name>_driver` input distinct from the
    bias state name."""
    return name

initial_state_entries

initial_state_entries(name, owner)

Names → zero values this channel contributes to the seed state dict (state_spec.unpack-compatible). Inert RW channels return an empty dict; everyone else seeds at least the signal slot.

Source code in manta/parts/_declarations.py
def initial_state_entries(self, name: str, owner) -> dict[str, object]:
    """Names → zero values this channel contributes to the seed
    state dict (state_spec.unpack-compatible). Inert RW channels
    return an empty dict; everyone else seeds at least the signal
    slot."""
    return {name: self._zero_value()}

synthesize

synthesize(*, base_name, name, dt, default_frame, owner)

Build one tick's worth of IR plumbing for this channel. Subclasses implement; the world-tick compiler calls this once per noise declaration per owner.

Source code in manta/parts/_declarations.py
def synthesize(self, *, base_name: str, name: str, dt, default_frame,
               owner) -> SynthesizedNoise:
    """Build one tick's worth of IR plumbing for this channel.
    Subclasses implement; the world-tick compiler calls this once
    per noise declaration per owner."""
    raise NotImplementedError(
        f"{type(self).__name__}.synthesize must be implemented.")

manta.parts.WhiteNoise

WhiteNoise(signal_manifold='R3', *, frame=None, sigma=0.0)

Bases: Noise

Per-tick i.i.d. Gaussian noise channel. σ is the per-tick stddev.

Source code in manta/parts/_declarations.py
def __init__(self, signal_manifold="R3", *, frame=None,
             sigma: float = 0.0) -> None:
    super().__init__(default=None)
    from ..ir.manifold import manifold_from_shortcut
    self.signal_manifold = manifold_from_shortcut(
        signal_manifold, frame=frame)
    self.sigma = float(sigma)
    if self.sigma < 0.0:
        raise ValueError(
            f"{type(self).__name__}: sigma must be >= 0, "
            f"got {sigma!r}")

manta.parts.RandomWalkNoise

RandomWalkNoise(signal_manifold='R3', *, frame=None, sigma=0.0)

Bases: Noise

Random-walk bias channel. σ has σ/√Hz drift-density units.

Source code in manta/parts/_declarations.py
def __init__(self, signal_manifold="R3", *, frame=None,
             sigma: float = 0.0) -> None:
    super().__init__(default=None)
    from ..ir.manifold import manifold_from_shortcut
    self.signal_manifold = manifold_from_shortcut(
        signal_manifold, frame=frame)
    self.sigma = float(sigma)
    if self.sigma < 0.0:
        raise ValueError(
            f"{type(self).__name__}: sigma must be >= 0, "
            f"got {sigma!r}")

manta.parts.GaussMarkovNoise

GaussMarkovNoise(signal_manifold='R3', *, frame=None, sigma=0.0, tau)

Bases: Noise

First-order Gauss–Markov error channel: correlation time tau (seconds, > 0) and stationary 1-σ sigma (signal units).

The synthesized state slot holds the correlated error itself; the exact discrete transition φ = exp(-dt/τ) keeps the slot's variance at σ² for any tick length. With sigma == 0 the channel is inert (no slot, no driver), exactly like RandomWalkNoise.

Source code in manta/parts/_declarations.py
def __init__(self, signal_manifold="R3", *, frame=None,
             sigma: float = 0.0, tau: float) -> None:
    super().__init__(signal_manifold, frame=frame, sigma=sigma)
    self.tau = _finite_positive_tau(tau, who=type(self).__name__)

manta.parts.PartUpdate

PartUpdate(wrench=None, new_state=None, outputs=None, rates=None)

Bundle returned by Part.update(ctx) describing this tick's contributions: a wrench (force + torque on parent in CraftFrame), new values for any declared State slots, any declared Output values the part produces, and the rates its I/O runs at.

Construction::

return PartUpdate(wrench, {"angle": a})
return PartUpdate(wrench=w, new_state={"angle": a, "rate": r})
return PartUpdate(wrench=w, outputs={"gyro": gyro_vec},
                  rates={"gyro": self.rate})

rates maps this part's Output slots and/or Input attribute names to a positive rate in Hz (None ⇒ every tick). The symbolic world tick stays a pure function: Sim.module() uses Output rates to partition measurement-only expressions into independently scheduled kernels, and the simulation runtime holds each reading between acquisitions. An Input rate remains an intake-contract annotation; the command transport owns its ZOH policy because an input that contributes force or state must remain in every plant integration step. Deploy estimator kernels remain continuous measurement functions and are invoked only when an external observation arrives.

Stateless parts can return a bare Wrench instead — the framework wraps it as PartUpdate(wrench=w) automatically.

Source code in manta/parts/_declarations.py
def __init__(self,
             wrench=None,
             new_state: dict | None = None,
             outputs: dict | None = None,
             rates: dict | None = None) -> None:
    if wrench is None:
        raise TypeError("PartUpdate: wrench is required")
    self.wrench = wrench
    self.new_state = dict(new_state) if new_state else {}
    self.outputs   = dict(outputs)   if outputs   else {}
    self.rates     = dict(rates)     if rates     else {}

Structure

manta.parts.Mass

Mass(name, **overrides)

Bases: Part

A lump of mass with diagonal inertia tensor.

Parameters: mass — kilograms. Promotable (system-ID target). moi — 3-tuple, diagonal MOI tensor (Ixx, Iyy, Izz) about the part's own COM, in part frame. Defaults to zero (point mass). Promotable, like mass: a tunable transform (Sim(world, parameters=[...]) / Fit) promotes it to a live R3 input and the inertia rollup keeps it symbolic.

Gravity contribution is applied automatically whenever a GravityField is registered on the world: F = m · g(p_world), sampled at the part's anchor position. With no GravityField registered the contribution is explicitly zero (gravity_at branches on ctx.has_field) — a free-space world is legitimate, not a configuration error.

The part's spatial location is set via its mount_offset parameter (inherited from Part). Aggregation at the Craft level rolls these individual contributions into total mass, COM, and MOI about craft origin via parallel-axis lifts.

Source code in manta/parts/structure/mass.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    # Zero is allowed per-part (the craft-level total-mass guard
    # catches an all-massless craft); negative mass/MOI is nonsense.
    if float(self.mass) < 0.0:
        raise ValueError(
            f"{type(self).__name__} {name!r}: mass must be >= 0, "
            f"got {self.mass!r}")
    moi = tuple(float(x) for x in self.moi)
    if len(moi) != 3 or any(x < 0.0 for x in moi):
        raise ValueError(
            f"{type(self).__name__} {name!r}: moi must be three "
            f"non-negative diagonal entries, got {self.moi!r}")

manta.parts.PointBuoy

PointBuoy(name, **overrides)

Bases: Part

Single-point buoyancy displacing a fixed volume.

Parameters: volume — m³ displaced by the buoyancy element. Default 1e-3.

Force = ρ(p_world) · V · (a_fluid - g) at the part's mount point, rotated from anchor to craft frame, applied at the offset (so the framework lifts force-at-offset → body-frame torque for tilt response).

Source code in manta/parts/structure/point_buoy.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    if float(self.volume) < 0.0:
        raise ValueError(
            f"{type(self).__name__} {name!r}: volume must be >= 0, "
            f"got {self.volume!r}")

manta.parts.DisplacementHull

DisplacementHull(name, *, dimensions, displacement_volume=None, hydrostatic_offset=(0, 0, 0), drag_coefficients=(0.2, 0.8, 1.0), reference_areas=None, sample_resolution=(5, 2, 8), **mount_overrides)

Bases: CompositePart

Low-speed surface-piercing displacement hull.

Args: dimensions: (length, beam, height) of the ellipsoidal hydrostatic sample envelope, metres, in the hull part frame. displacement_volume: Full-submersion displaced volume in m³. Defaults to the exact volume pi/6 * length * beam * height of the envelope. Set it from a measured displacement calibration when available. hydrostatic_offset: Translation of the sample cloud in the hull frame, metres. This is the explicit centre-of-buoyancy calibration knob. drag_coefficients: Per-axis non-negative quadratic (Cx, Cy, Cz). Body x is normally longitudinal, y lateral, and z vertical. reference_areas: Optional per-axis drag reference areas in m². Defaults to the ellipsoid's projected frontal, lateral, and planform areas. sample_resolution: (axial, radial, circumferential) product-quadrature counts. The circumferential count must be even. (5, 2, 8) gives 80 buoy/drag pairs and is the practical default; convergence should be checked for the craft's draft and sea-state bandwidth.

mount_offset and mount_orientation are the normal Part mount pose and apply to the entire generated sample cloud.

Source code in manta/parts/structure/displacement_hull.py
def __init__(
    self,
    name: str,
    *,
    dimensions: tuple[float, float, float],
    displacement_volume: float | None = None,
    hydrostatic_offset: tuple[float, float, float] = (0, 0, 0),
    drag_coefficients: tuple[float, float, float] = (0.2, 0.8, 1.0),
    reference_areas: tuple[float, float, float] | None = None,
    sample_resolution: tuple[int, int, int] = (5, 2, 8),
    **mount_overrides,
) -> None:
    dimensions = _positive_triplet(dimensions, name="dimensions")
    drag_coefficients = _nonnegative_triplet(
        drag_coefficients, name="drag_coefficients"
    )
    resolution = _resolution(sample_resolution)
    try:
        hydrostatic_offset = tuple(float(x) for x in hydrostatic_offset)
    except (TypeError, ValueError) as exc:
        raise ValueError("hydrostatic_offset must be three finite values") from exc
    if len(hydrostatic_offset) != 3 or any(
        not math.isfinite(x) for x in hydrostatic_offset
    ):
        raise ValueError(
            "hydrostatic_offset must be three finite values, got "
            f"{hydrostatic_offset!r}"
        )

    length, beam, height = dimensions
    geometric_volume = math.pi * length * beam * height / 6.0
    if displacement_volume is None:
        displacement_volume = geometric_volume
    displacement_volume = float(displacement_volume)
    if not math.isfinite(displacement_volume) or displacement_volume <= 0.0:
        raise ValueError(
            "displacement_volume must be finite and > 0, got "
            f"{displacement_volume!r}"
        )

    if reference_areas is None:
        reference_areas = (
            math.pi * beam * height / 4.0,
            math.pi * length * height / 4.0,
            math.pi * length * beam / 4.0,
        )
    else:
        reference_areas = _nonnegative_triplet(
            reference_areas, name="reference_areas"
        )

    super().__init__(name, **mount_overrides)
    self.dimensions = dimensions
    self.displacement_volume = displacement_volume
    self.geometric_volume = geometric_volume
    self.hydrostatic_offset = hydrostatic_offset
    self.drag_coefficients = drag_coefficients
    self.reference_areas = reference_areas
    self.sample_resolution = resolution
    self.samples = _ellipsoid_samples(
        dimensions=dimensions,
        displacement_volume=displacement_volume,
        reference_areas=reference_areas,
        hydrostatic_offset=hydrostatic_offset,
        resolution=resolution,
    )

    for index, sample in enumerate(self.samples):
        # Names include the composite name because names are globally
        # unique across a craft, not scoped to their parent subtree.
        self.add(
            PointBuoy(
                f"{name}_buoy_{index}",
                volume=sample.volume,
                mount_offset=sample.offset,
            )
        )
        self.add(
            DragSurface(
                f"{name}_drag_{index}",
                # Directional coefficients are folded into each sample's
                # quadratic tensor so each axis retains its own Cd.
                force_tensors=[
                    np.zeros((3, 3)),
                    -0.5
                    * np.diag(
                        np.asarray(sample.areas) * np.asarray(drag_coefficients)
                    ),
                ],
                mount_offset=sample.offset,
            )
        )

displaced_volume_below

displaced_volume_below(waterline_z)

Discrete displaced volume below a flat hull-frame waterline.

This calibration helper uses a hard horizontal cut through sample centres. Runtime physics does not: each child queries the world's smooth, possibly moving fluid boundary. Use this helper only to compare resolutions or choose an initial calm-water draft.

Source code in manta/parts/structure/displacement_hull.py
def displaced_volume_below(self, waterline_z: float) -> float:
    """Discrete displaced volume below a flat hull-frame waterline.

    This calibration helper uses a hard horizontal cut through sample
    centres.  Runtime physics does *not*: each child queries the world's
    smooth, possibly moving fluid boundary.  Use this helper only to
    compare resolutions or choose an initial calm-water draft.
    """

    waterline_z = float(waterline_z)
    if not math.isfinite(waterline_z):
        raise ValueError("waterline_z must be finite")
    return sum(s.volume for s in self.samples if s.offset[2] <= waterline_z)

manta.parts.Collider

Collider(name, *, friction=0.0, **overrides)

Bases: Part

Point contact element backed by the registered CollisionField.

Parameters: stiffness — N/m. Spring constant of the contact normal-force. Bigger = stiffer contact. Default 5e3. damping — N·s/m. Damper coefficient for the relative velocity along the outward normal direction. Bigger = more energy dissipation per bounce. Default 50.0. friction — N·s/m. Viscous TANGENTIAL friction: opposes the contact point's slip in the contact plane, gated smoothly by penetration (a smooth, EKF-friendly stand-in for Coulomb friction — grips a resting contact against sliding). ANISOTROPIC: a per-BODY-axis 3-vector of coefficients, so a wheel can roll free along one axis and grip the others (e.g. (0, c, 0) — free fore–aft, grips sideways). A scalar is accepted as shorthand for isotropic (c, c, c). Default 0 → frictionless contact (the prior behaviour).

Source code in manta/parts/structure/collider.py
def __init__(self, name: str, *, friction=0.0, **overrides) -> None:
    # `friction` is a per-body-axis 3-vector; a scalar broadcasts to the
    # isotropic (c, c, c).
    if isinstance(friction, (int, float)):
        friction = (float(friction),) * 3
    else:
        friction = tuple(float(c) for c in friction)
    super().__init__(name, friction=friction, **overrides)

manta.parts.ThermalMass

ThermalMass(name, *, source=None, **overrides)

Bases: Part

A lumped thermal node: heat capacity + temperature state + conduction/boundary/generation heat flows.

Parameters: heat_capacity — C, J/K. Promotable (a classic sysid target). Default 100. ambient_conductance — g_amb, W/K leak to the ambient boundary. 0 (default) = insulated: no ambient term, and the ambient input is not plumbed. ambient — where the boundary temperature comes from: "input" (the ambient_temperature Input) or "fluid" (the world FluidField's temperature at the part's position — requires a registered FluidField whose regimes declare temperatures).

Inputs: heat_input — external heat into the node, W (signed). ambient_temperature — boundary temperature the node leaks to, K. Only bound when ambient_conductance > 0 AND ambient="input". Default 293.15; rewrite per tick from the driving loop to script an environment.

State: temperature — node temperature, K. Init 293.15; set a per-run value via add_craft(..., **{"node.temperature": T0}).

Noise: heat_noise — white heat-flow noise, W (σ default 0 — inert). Gives the EKF an auto-Q channel for the temperature slot.

Construction: source — optional Part implementing dissipated_heat() -> MX (W); its loss is added to this node's balance each tick.

connect(other, conductance=k) links two nodes with a k W/K conductance (symmetric; both must ride the same craft).

Source code in manta/parts/thermal/thermal_mass.py
def __init__(self, name: str, *, source: Part | None = None,
             **overrides: Any) -> None:
    super().__init__(name, **overrides)
    who = f"ThermalMass {name!r}"
    if float(self.declared_value("heat_capacity")) <= 0.0:
        raise ValueError(
            f"{who}: heat_capacity must be > 0, got "
            f"{self.declared_value('heat_capacity')!r}")
    if float(self.declared_value("ambient_conductance")) < 0.0:
        raise ValueError(
            f"{who}: ambient_conductance must be ≥ 0, got "
            f"{self.declared_value('ambient_conductance')!r}")
    if self.declared_value("ambient") not in ("input", "fluid"):
        raise ValueError(
            f"{who}: ambient must be 'input' or 'fluid', got "
            f"{self.declared_value('ambient')!r}")
    if source is not None and not callable(
            getattr(source, "dissipated_heat", None)):
        raise TypeError(
            f"{who}: source {type(source).__name__}"
            f"('{getattr(source, 'name', '?')}') has no "
            f"dissipated_heat() — a heat source must implement "
            f"`dissipated_heat() -> MX` (watts).")
    self._source = source
    # (other_node, conductance, initiated) triples; each link is
    # registered on BOTH endpoints so each side's balance sees the
    # exchange, and `initiated` records which side made the
    # `connect` call — the reciprocal-call guard keys on it.
    self._links: list[tuple[ThermalMass, float, bool]] = []

input_declarations

input_declarations()

The ambient_temperature input exists only when it is the live boundary: an insulated node (ambient_conductance == 0) has no ambient term, and a fluid-coupled node reads the FluidField instead — drop it so it never reaches the u port.

Source code in manta/parts/thermal/thermal_mass.py
def input_declarations(self):
    """The ambient_temperature input exists only when it is the
    live boundary: an insulated node (ambient_conductance == 0) has
    no ambient term, and a fluid-coupled node reads the FluidField
    instead — drop it so it never reaches the u port."""
    decls = dict(super().input_declarations())
    if (float(self.declared_value("ambient_conductance")) <= 0.0
            or self.declared_value("ambient") == "fluid"):
        decls.pop("ambient_temperature", None)
    return decls

connect

connect(other, *, conductance)

Register a symmetric conduction link to other (W/K). One call wires BOTH directions — do not also call other.connect(self, ...); the natural-looking reciprocal call would silently double the conductance, so it raises instead. Deliberate parallel heat paths are still expressed by calling connect again from the same endpoint. Returns self for chaining.

Source code in manta/parts/thermal/thermal_mass.py
def connect(self, other: ThermalMass, *,
            conductance: float) -> ThermalMass:
    """Register a symmetric conduction link to `other` (W/K). One
    call wires BOTH directions — do not also call
    `other.connect(self, ...)`; the natural-looking reciprocal call
    would silently double the conductance, so it raises instead.
    Deliberate parallel heat paths are still expressed by calling
    `connect` again *from the same endpoint*. Returns self for
    chaining."""
    who = f"ThermalMass {self.name!r}.connect"
    if not isinstance(other, ThermalMass):
        raise TypeError(
            f"{who}: expected a ThermalMass, got "
            f"{type(other).__name__}")
    if other is self:
        raise ValueError(f"{who}: cannot connect a node to itself")
    k = float(conductance)
    if k <= 0.0:
        raise ValueError(
            f"{who}: conductance must be > 0 W/K, got {conductance!r}")
    if any(o is other and not initiated
           for o, _, initiated in self._links):
        raise ValueError(
            f"{who}: {other.name!r} already connected this pair from "
            f"its side — one connect() call wires both directions, "
            f"and a reciprocal call would double the conductance. "
            f"For a deliberate parallel path, call connect() again "
            f"from the same endpoint that made the first call.")
    self._links.append((other, k, True))
    other._links.append((self, k, False))
    return self

on_world_resolve

on_world_resolve(world, craft)

Validate the thermal network on the resolved snapshot — before any tracing — instead of erroring mid-trace: every linked node and the heat source must ride this node's craft.

Source code in manta/parts/thermal/thermal_mass.py
def on_world_resolve(self, world, craft) -> None:
    """Validate the thermal network on the resolved snapshot —
    before any tracing — instead of erroring mid-trace: every linked
    node and the heat source must ride this node's craft."""
    for other, _, _ in self._links:
        self._require_same_craft(other, "linked node")
    if self._source is not None:
        self._require_same_craft(self._source, "heat source")

Electrical

manta.parts.ElectricalNode

ElectricalNode(name, **overrides)

Bases: ElectricalPort, Part

Base contract for one node in a radial DC network.

The diagnostic outputs have identical meanings on every node:

voltage The node terminal/rail voltage (V). input_current / input_power Flow entering from the upstream edge (or ideal reservoir for a source), in A/W. output_current / output_power Flow delivered to children, or consumed as useful endpoint power by a load, in A/W. loss_power Electrical conversion/series loss (W), suitable for thermal coupling. brownout / open / tripped Unit-valued status signals. Brownout is 1 below the lower voltage and 0 above the recovery voltage. open and tripped are explicit; ordinary buses and loads report zero. kcl_residual / energy_residual Equation diagnostics (A and W). They are symbolically zero for a correctly assembled tick, including capacitor storage and injected current noise.

Use :meth:connect on the upstream node. Connectivity need not resemble mechanical mounting, but every connected node must ride the same craft.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    who = f"{type(self).__name__}({name!r})"
    low = _positive(self.declared_value("brownout_voltage"),
                    name=f"{who}.brownout_voltage", allow_zero=True)
    high = _positive(self.declared_value("recovery_voltage"),
                     name=f"{who}.recovery_voltage", allow_zero=True)
    if high < low:
        raise ValueError(
            f"{who}.recovery_voltage must be >= brownout_voltage; "
            f"got {high} < {low}")

dissipated_heat

dissipated_heat()

Electrical loss available to ThermalMass(source=...).

Source code in manta/parts/electrical/core.py
def dissipated_heat(self) -> ca.MX:
    """Electrical loss available to ``ThermalMass(source=...)``."""
    raise NotImplementedError

manta.parts.DCSource

DCSource(name, **overrides)

Bases: _CapacitiveRail

Current-limited Thevenin DC source with terminal capacitance.

open_circuit_voltage and source_resistance describe the ideal reservoir. Current flows only out of the reservoir; A1 deliberately does not model charging. enabled is a normalized contact command.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    if "rail_voltage" not in overrides:
        overrides["rail_voltage"] = overrides.get(
            "open_circuit_voltage",
            type(self)._declarations()["open_circuit_voltage"].default)
    super().__init__(name, **overrides)
    who = f"DCSource({name!r})"
    _positive(self.declared_value("open_circuit_voltage"),
              name=f"{who}.open_circuit_voltage")
    _positive(self.declared_value("source_resistance"),
              name=f"{who}.source_resistance")
    _positive_or_inf(self.declared_value("current_limit"),
                     name=f"{who}.current_limit")
    _unit_interval(self.declared_value("enabled"), name=f"{who}.enabled")

manta.parts.ExternalDCSupply

ExternalDCSupply(name, **overrides)

Bases: ElectricalNode

Runtime boundary for a simulation-only or hardware DC source.

supplied_voltage enters the Manta tick as an ordinary input and the aggregate downstream demand leaves as output_current. A battery plant may therefore keep cell, thermal, and fault state in a non-differentiable simulator while powered mechanical parts retain their normal compiled model. Source-internal heat remains owned by that plant; endpoint conversion loss is still available through each load's dissipated_heat().

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    who = f"{type(self).__name__}({name!r})"
    low = _positive(self.declared_value("brownout_voltage"),
                    name=f"{who}.brownout_voltage", allow_zero=True)
    high = _positive(self.declared_value("recovery_voltage"),
                     name=f"{who}.recovery_voltage", allow_zero=True)
    if high < low:
        raise ValueError(
            f"{who}.recovery_voltage must be >= brownout_voltage; "
            f"got {high} < {low}")

manta.parts.ElectricalBus

ElectricalBus(name, **overrides)

Bases: _CapacitiveRail

Capacitive DC bus fed through a current-limited series edge.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    who = f"{type(self).__name__}({name!r})"
    _positive(self.declared_value("series_resistance"),
              name=f"{who}.series_resistance")
    _positive_or_inf(self.declared_value("input_current_limit"),
                     name=f"{who}.input_current_limit")

manta.parts.DCConverter

DCConverter(name, **overrides)

Bases: _CapacitiveRail

Regulated DC converter with dropout, efficiency and hard ratings.

A proportional internal regulator charges the output capacitor toward output_voltage through control_resistance. Delivery is bounded by output current, output power and available input power. Below minimum_input_voltage it fades out through a C1 gate; above that voltage the stated efficiency relates input and rail-injection power.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    if "rail_voltage" not in overrides:
        overrides["rail_voltage"] = overrides.get(
            "output_voltage",
            type(self)._declarations()["output_voltage"].default)
    super().__init__(name, **overrides)
    who = f"DCConverter({name!r})"
    for attr in ("output_voltage", "control_resistance",
                 "minimum_input_voltage", "input_recovery_voltage"):
        _positive(self.declared_value(attr), name=f"{who}.{attr}")
    _positive(self.declared_value("dropout_voltage"),
              name=f"{who}.dropout_voltage", allow_zero=True)
    eta = _finite_scalar(self.declared_value("efficiency"),
                         name=f"{who}.efficiency")
    if not 0.0 < eta <= 1.0:
        raise ValueError(f"{who}.efficiency must be in (0, 1], got {eta}")
    if (float(self.declared_value("input_recovery_voltage"))
            <= float(self.declared_value("minimum_input_voltage"))):
        raise ValueError(
            f"{who}.input_recovery_voltage must be greater than "
            f"minimum_input_voltage")
    for attr in ("output_current_limit", "output_power_limit",
                 "input_power_limit"):
        _positive_or_inf(self.declared_value(attr), name=f"{who}.{attr}")
    _positive(self.declared_value("quiescent_current"),
              name=f"{who}.quiescent_current", allow_zero=True)
    _unit_interval(self.declared_value("enabled"), name=f"{who}.enabled")

manta.parts.Contactor

Contactor(name, **overrides)

Bases: ElectricalBus

Commanded series contactor with downstream hold-up capacitance.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    _unit_interval(self.declared_value("closed"),
                   name=f"Contactor({name!r}).closed")

manta.parts.Fuse

Fuse(name, **overrides)

Bases: ElectricalBus

Latching I²t fuse with a continuous overload accumulator.

trip_fraction integrates normalized I² above the rated current and latches at one. The electrical edge remains closed until the threshold, then opens. This is a deterministic hybrid event and therefore only piecewise differentiable at the exact trip surface.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    who = f"Fuse({name!r})"
    _positive(self.declared_value("rated_current"),
              name=f"{who}.rated_current")
    _positive(self.declared_value("trip_time"), name=f"{who}.trip_time")
    fraction = _finite_scalar(self.declared_value("trip_fraction"),
                              name=f"{who}.trip_fraction")
    if not 0.0 <= fraction <= 1.0:
        raise ValueError(f"{who}.trip_fraction must be in [0, 1]")

manta.parts.ElectricalLoad

ElectricalLoad(name, **overrides)

Bases: ElectricalNode

Base endpoint load with brownout and enable semantics.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    fraction = _finite_scalar(
        self.declared_value("heat_fraction"),
        name=f"{type(self).__name__}({name!r}).heat_fraction")
    if not 0.0 <= fraction <= 1.0:
        raise ValueError("heat_fraction must be in [0, 1]")
    _unit_interval(
        self.declared_value("enabled"),
        name=f"{type(self).__name__}({name!r}).enabled")

manta.parts.ResistiveLoad

ResistiveLoad(name, **overrides)

Bases: ElectricalLoad

Constant-resistance endpoint load.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    _positive(self.declared_value("resistance"),
              name=f"ResistiveLoad({name!r}).resistance")

manta.parts.ConstantCurrentLoad

ConstantCurrentLoad(name, **overrides)

Bases: ElectricalLoad

Constant-current endpoint load above its brownout recovery voltage.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    _positive(self.declared_value("current"),
              name=f"ConstantCurrentLoad({name!r}).current",
              allow_zero=True)

manta.parts.ConstantPowerLoad

ConstantPowerLoad(name, **overrides)

Bases: ElectricalLoad

Bounded constant-power endpoint load with a low-voltage floor.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    _positive(self.declared_value("power"),
              name=f"ConstantPowerLoad({name!r}).power", allow_zero=True)
    _positive_or_inf(self.declared_value("current_limit"),
                     name=f"ConstantPowerLoad({name!r}).current_limit")
    floor = _positive(self.declared_value("voltage_floor"),
                      name=f"ConstantPowerLoad({name!r}).voltage_floor")
    low = float(self.declared_value("brownout_voltage"))
    if low > 0.0 and floor > low:
        raise ValueError(
            f"ConstantPowerLoad({name!r}).voltage_floor must be <= "
            f"brownout_voltage so the load fades before its denominator "
            f"floor")

manta.parts.ConstantPowerElectronicsLoad

ConstantPowerElectronicsLoad(name, **overrides)

Bases: ConstantPowerLoad

Compute/electronics load whose consumed power ultimately becomes heat.

Source code in manta/parts/electrical/core.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    _positive(self.declared_value("power"),
              name=f"ConstantPowerLoad({name!r}).power", allow_zero=True)
    _positive_or_inf(self.declared_value("current_limit"),
                     name=f"ConstantPowerLoad({name!r}).current_limit")
    floor = _positive(self.declared_value("voltage_floor"),
                      name=f"ConstantPowerLoad({name!r}).voltage_floor")
    low = float(self.declared_value("brownout_voltage"))
    if low > 0.0 and floor > low:
        raise ValueError(
            f"ConstantPowerLoad({name!r}).voltage_floor must be <= "
            f"brownout_voltage so the load fades before its denominator "
            f"floor")

manta.parts.PoweredMotor

PoweredMotor(name, **overrides)

Bases: PoweredLoadMixin, Motor

A direct-voltage :class:Motor supplied by an A1 electrical rail.

The existing signed voltage input remains the requested winding voltage. The drive clips it to available rail magnitude and fades it through brownout. Reverse power is not returned to A1's unidirectional network; regenerative/dynamic braking energy is reported as heat.

Source code in manta/parts/electrical/powered.py
def __init__(self, name: str, **overrides: Any) -> None:
    super().__init__(name, **overrides)
    who = f"PoweredMotor({name!r})"
    efficiency = _finite_scalar(
        self.declared_value("controller_efficiency"),
        name=f"{who}.controller_efficiency")
    if not 0.0 < efficiency <= 1.0:
        raise ValueError(f"{who}.controller_efficiency must be in (0, 1]")
    _positive(self.declared_value("idle_power"),
              name=f"{who}.idle_power", allow_zero=True)

manta.parts.PoweredThruster

PoweredThruster(name, **overrides)

Bases: _CalibratedPoweredActuator, Thruster

Voltage-derated polynomial thruster with a calibrated power map.

Source code in manta/parts/electrical/powered.py
def __init__(self, name: str, **overrides: Any) -> None:
    required = {
        "rated_voltage", "rated_mechanical_power",
        "conversion_efficiency",
    }
    missing = required - set(overrides)
    if missing:
        raise TypeError(
            f"{type(self).__name__}({name!r}) requires explicit "
            f"calibration for {sorted(missing)}; the underlying "
            f"primitive has no motor shaft from which to infer it")
    super().__init__(name, **overrides)
    who = f"{type(self).__name__}({name!r})"
    for parameter in ("rated_voltage", "rated_mechanical_power",
                      "power_exponent"):
        _positive(self.declared_value(parameter),
                  name=f"{who}.{parameter}")
    efficiency = _finite_scalar(
        self.declared_value("conversion_efficiency"),
        name=f"{who}.conversion_efficiency")
    if not 0.0 < efficiency <= 1.0:
        raise ValueError(f"{who}.conversion_efficiency must be in (0, 1]")
    _positive(self.declared_value("idle_power"),
              name=f"{who}.idle_power", allow_zero=True)

manta.parts.PoweredDuctedPropeller

PoweredDuctedPropeller(name, **overrides)

Bases: _CalibratedPoweredActuator, DuctedPropeller

Voltage-derated ducted propeller with calibrated shaft power.

Source code in manta/parts/electrical/powered.py
def __init__(self, name: str, **overrides: Any) -> None:
    required = {
        "rated_voltage", "rated_mechanical_power",
        "conversion_efficiency",
    }
    missing = required - set(overrides)
    if missing:
        raise TypeError(
            f"{type(self).__name__}({name!r}) requires explicit "
            f"calibration for {sorted(missing)}; the underlying "
            f"primitive has no motor shaft from which to infer it")
    super().__init__(name, **overrides)
    who = f"{type(self).__name__}({name!r})"
    for parameter in ("rated_voltage", "rated_mechanical_power",
                      "power_exponent"):
        _positive(self.declared_value(parameter),
                  name=f"{who}.{parameter}")
    efficiency = _finite_scalar(
        self.declared_value("conversion_efficiency"),
        name=f"{who}.conversion_efficiency")
    if not 0.0 < efficiency <= 1.0:
        raise ValueError(f"{who}.conversion_efficiency must be in (0, 1]")
    _positive(self.declared_value("idle_power"),
              name=f"{who}.idle_power", allow_zero=True)

manta.parts.PoweredControlSurface

PoweredControlSurface(name, **overrides)

Bases: _CalibratedPoweredActuator, ControlSurface

Control surface whose servo torque and speed fade with rail voltage.

Source code in manta/parts/electrical/powered.py
def __init__(self, name: str, **overrides: Any) -> None:
    required = {
        "rated_voltage", "rated_mechanical_power",
        "conversion_efficiency",
    }
    missing = required - set(overrides)
    if missing:
        raise TypeError(
            f"{type(self).__name__}({name!r}) requires explicit "
            f"calibration for {sorted(missing)}; the underlying "
            f"primitive has no motor shaft from which to infer it")
    super().__init__(name, **overrides)
    who = f"{type(self).__name__}({name!r})"
    for parameter in ("rated_voltage", "rated_mechanical_power",
                      "power_exponent"):
        _positive(self.declared_value(parameter),
                  name=f"{who}.{parameter}")
    efficiency = _finite_scalar(
        self.declared_value("conversion_efficiency"),
        name=f"{who}.conversion_efficiency")
    if not 0.0 < efficiency <= 1.0:
        raise ValueError(f"{who}.conversion_efficiency must be in (0, 1]")
    _positive(self.declared_value("idle_power"),
              name=f"{who}.idle_power", allow_zero=True)

Actuation

manta.parts.Thruster

Thruster(name, **overrides)

Bases: Part

Polynomial-in-throttle thruster (linear + quadratic).

Coefficients are 3-vectors in the thruster's own frame. For a thruster mounted directly on the craft root that frame is CraftFrame, so Thruster("t", force=(0,0,1)) is a pure +z thrust in body coords. Mounted on a joint's rotor, the thruster's frame spins with the rotor and the framework rotates the emitted wrench into body coords — a gimballed thruster's thrust direction tracks the joint angle automatically, with no frame handling here. Any unset coefficient defaults to zero.

Input: throttle — scalar control input. Units depend on the scaling of the coefficients.

Process-noise channels (the actuator analogue of a sensor's noise — set σ to engage, default 0 = a perfectly clean actuator): force_noise : per-tick white force (N) added to the thrust, in the thruster frame. Because it enters the wrench (not an Output) it propagates through the dynamics into the next state, so the EKF auto-builds Q from it (just as σ on a sensor auto-builds R), and a NoiseDriver jitters the truth thrust by it. torque_noise : per-tick white torque (N·m) added to the reaction torque, same frame and same role for attitude.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.Motor

Motor(name, **overrides)

Bases: RevoluteDOF

Voltage-commanded DC motor on a revolute DOF.

Parameters: axis — input-frame unit vector along the rotation axis. Default (0, 0, 1). torque_constant — SI motor constant k: torque constant (N·m/A) and back-EMF constant (V·s/rad). Promotable. Default 0.05. resistance — winding resistance R (Ω). Promotable. Default 1.0. current_limit — armature-current clamp magnitude (A), the driver/thermal limit. Default inf (no limit). gear_ratio — motor-shaft turns per output-shaft turn (reduction ratio ≥ 1 gears torque up). Default 1.0 (direct drive). damping — viscous friction at the output shaft (N·m·s/rad). Default 0.

Inputs: voltage — terminal voltage V (signed; negative reverses).

State: angle — output-shaft angle, rad. rate — output-shaft rate relative to the mounting body, rad/s.

Source code in manta/parts/articulation/motor.py
def __init__(self, name: str, **overrides) -> None:
    if "mode" in overrides:
        raise TypeError(
            f"Motor {name!r}: no `mode` — the electrical model is "
            f"always live (voltage=0 gives dynamic braking through "
            f"the winding). Use a RevoluteJoint for passive/"
            f"saturating torque semantics.")
    super().__init__(name, **overrides)
    for attr in ("torque_constant", "resistance", "current_limit",
                 "gear_ratio"):
        if float(self.declared_value(attr)) <= 0.0:
            raise ValueError(
                f"Motor {name!r}: {attr} must be > 0, got "
                f"{self.declared_value(attr)!r}")

applied_dof_force

applied_dof_force()

Electrical shaft torque on top of the base viscous damping, as a raw MX scalar — this joint's generalized-force row entry (see the base class for the reaction-bookkeeping argument).

Source code in manta/parts/articulation/motor.py
def applied_dof_force(self):
    """Electrical shaft torque on top of the base viscous damping, as
    a raw MX scalar — this joint's generalized-force row entry (see
    the base class for the reaction-bookkeeping argument)."""
    k = _mx(self.torque_constant)
    G = float(self.declared_value("gear_ratio"))
    return super().applied_dof_force() + G * k * self._current_mx()

dissipated_heat

dissipated_heat()

Winding copper loss i²·R (W) — the ThermalMass heat-source protocol (ThermalMass("winding", source=motor)). Everything the electrical model wastes: at stall the full V²/R, at no-load speed ~0.

Source code in manta/parts/articulation/motor.py
def dissipated_heat(self) -> ca.MX:
    """Winding copper loss i²·R (W) — the `ThermalMass` heat-source
    protocol (`ThermalMass("winding", source=motor)`). Everything
    the electrical model wastes: at stall the full V²/R, at no-load
    speed ~0."""
    i = self._current_mx()
    return i * i * _mx(self.resistance)

Articulation

manta.parts.RevoluteJoint

RevoluteJoint(name, **overrides)

Bases: RevoluteDOF, CommandedDOF

1-DOF revolute joint with an axial rotor (set of Mass children).

Parameters: axis — input-frame unit vector along the rotation axis. Default (0, 0, 1). mode — "passive" or "saturating". Default "passive". stall_torque — saturating-mode torque clamp magnitude (N·m). Ignored in passive mode. Default 1.0. damping — viscous joint friction (N·m·s/rad). Default 0.

Inputs: torque_cmd — commanded torque about axis. Clamped to ±stall_torque in saturating mode; ignored entirely in passive mode.

State: angle — joint angle, rad. rate — joint angular rate (rotor spin relative to body), rad/s.

Source code in manta/parts/articulation/joint.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    # Post-super read so a subclass overriding the declared default
    # is validated too (a pre-super `overrides.get` would silently
    # validate the base default instead).
    mode = self.declared_value("mode")
    if mode not in _MODES:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mode must be one of "
            f"{_MODES}, got {mode!r}")

manta.parts.PrismaticJoint

PrismaticJoint(name, **overrides)

Bases: CommandedDOF

1-DOF prismatic (sliding) joint carrying a subtree of Mass children.

Parameters: axis — input-frame unit vector along the slide axis. Default (0, 0, 1). mode — "passive" or "saturating". Default "passive". stall_force — saturating-mode force clamp magnitude (N). Ignored in passive mode. Default 1.0. damping — viscous slide friction (N·s/m). Default 0.

Inputs: force_cmd — commanded force along axis. Clamped to ±stall_force in saturating mode; ignored entirely in passive mode.

State: displacement — slide displacement along axis, m. rate — slide rate (relative to the mount), m/s.

Source code in manta/parts/articulation/joint.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    # Post-super read so a subclass overriding the declared default
    # is validated too (a pre-super `overrides.get` would silently
    # validate the base default instead).
    mode = self.declared_value("mode")
    if mode not in _MODES:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mode must be one of "
            f"{_MODES}, got {mode!r}")

Aerodynamics

manta.parts.DragSurface

DragSurface(name, *, force=None, force_tensors=None, moment=None, moment_tensors=None, **overrides)

Bases: Part

Polynomial drag/lift surface.

Parameters: linear_coefficient_areas — positive diagonal coefficient-area vector for -rho * k1 * v. This is the compact, physically constrained system-identification form. quadratic_coefficient_areas — positive diagonal coefficient-area vector for -rho * k2 * v*abs(v). For the conventional drag law, k2 = 0.5 * Cd * A. force_tensors — list of 3×3 matrices [A_1, A_2, …, A_N], CraftFrame. Default is a single zero matrix (no drag). moment_tensors — same shape: the surface's FLOW-INDUCED moment about the mount point (τ = ρ·Σ B_k·v_rel^(k); not spin damping — see the module docstring).

Convenience args (mutually exclusive with the *_tensors form): force=(x,y,z) — sets A_1 = diag(x, y, z) (per-axis linear drag). moment=(x,y,z) — sets B_1 = diag(x, y, z) (per-axis, linear in flow).

Source code in manta/parts/aero/drag_surface.py
def __init__(self,
             name: str,
             *,
             force: tuple | None = None,
             force_tensors: list | tuple | None = None,
             moment: tuple | None = None,
             moment_tensors: list | tuple | None = None,
             **overrides) -> None:
    overrides["force_tensors"] = _as_tensor_polynomial(
        force=force, tensors=force_tensors, name=name, kind="force")
    for legacy in ("torque", "torque_tensors"):
        if legacy in overrides:
            raise TypeError(
                f"DragSurface {name!r}: `{legacy}=` was renamed "
                f"`{legacy.replace('torque', 'moment')}=` — these "
                f"tensors are moments induced by the incident FLOW "
                f"(τ = ρ·Σ B_k·v_rel^k), NOT damping in the body's "
                f"own spin. For spin damping use RotationalDrag.")
    overrides["moment_tensors"] = _as_tensor_polynomial(
        force=moment, tensors=moment_tensors, name=name, kind="moment")
    super().__init__(name, **overrides)
    for label in (
        "linear_coefficient_areas", "quadratic_coefficient_areas"
    ):
        values = tuple(float(value) for value in getattr(self, label))
        if len(values) != 3 or any(value < 0.0 for value in values):
            raise ValueError(
                f"DragSurface {name!r}: {label} must contain three "
                f"non-negative values, got {values!r}"
            )

isotropic_quadratic classmethod

isotropic_quadratic(name, *, area, drag_coefficient, **kwargs)

Single-Cd quadratic hull/sphere drag: F = -½·ρ·A·Cd · v_rel^(2) (element-wise square per body axis) Identical to the v1 isotropic model, just expressed in tensor form so the user can mix it with other polynomial orders.

Source code in manta/parts/aero/drag_surface.py
@classmethod
def isotropic_quadratic(cls,
                        name: str,
                        *,
                        area: float,
                        drag_coefficient: float,
                        **kwargs) -> DragSurface:
    """Single-Cd quadratic hull/sphere drag:
        F = -½·ρ·A·Cd · v_rel^(2)  (element-wise square per body axis)
    Identical to the v1 isotropic model, just expressed in tensor form
    so the user can mix it with other polynomial orders."""
    A_1 = np.zeros((3, 3))
    A_2 = -0.5 * area * drag_coefficient * np.eye(3)
    return cls(name, force_tensors=[A_1, A_2], **kwargs)

directional_quadratic classmethod

directional_quadratic(name, *, areas, drag_coefficient, **kwargs)

Anisotropic quadratic drag — a per-body-axis reference area: F_i = -½·ρ·areas_i·Cd · v_i·|v_i| (diagonal A_2) Use it for a slender body: a cylindrical fuselage along, say, body +z is areas=(side, side, frontal) with frontal ≪ side — low drag nose-on, high drag broadside (and an off-axis flow gets a restoring body torque through the standard force-at-offset lift).

Source code in manta/parts/aero/drag_surface.py
@classmethod
def directional_quadratic(cls,
                          name: str,
                          *,
                          areas: tuple,
                          drag_coefficient: float,
                          **kwargs) -> DragSurface:
    """Anisotropic quadratic drag — a per-body-axis reference area:
        F_i = -½·ρ·areas_i·Cd · v_i·|v_i|   (diagonal A_2)
    Use it for a slender body: a cylindrical fuselage along, say, body
    +z is ``areas=(side, side, frontal)`` with ``frontal ≪ side`` —
    low drag nose-on, high drag broadside (and an off-axis flow gets a
    restoring body torque through the standard force-at-offset lift)."""
    ax, ay, az = (float(a) for a in areas)
    A_1 = np.zeros((3, 3))
    A_2 = -0.5 * drag_coefficient * np.diag([ax, ay, az])
    return cls(name, force_tensors=[A_1, A_2], **kwargs)

manta.parts.RotationalDrag

RotationalDrag(name, *, torque=None, torque_tensors=None, **overrides)

Bases: Part

Damping torque polynomial in the part-frame angular velocity.

Parameters: torque — (kx, ky, kz): the one-order shortcut, τ_i = ρ·k_i·ω_i (linear damping; k_i < 0). torque_tensors — full per-order 3×3 tensor list instead.

Tensors are per unit fluid density, like DragSurface's.

Source code in manta/parts/aero/rotational_drag.py
def __init__(self, name: str, *, torque: tuple | None = None,
             torque_tensors: list | tuple | None = None,
             **overrides) -> None:
    overrides["torque_tensors"] = _as_polynomial(
        torque, torque_tensors, name)
    super().__init__(name, **overrides)

manta.parts.AddedMass

AddedMass(name, **overrides)

Bases: Part

Diagonal added mass (kg) and added rotational inertia (kg·m²), in the part's own frame, about the craft's COM.

Parameters: translational — (Ax, Ay, Az) kg: extra effective mass per part-frame axis. Slender body along +x: Ax ≪ Ay ≈ Az. rotational — (Bx, By, Bz) kg·m²: extra effective rotational inertia per part-frame axis. Bx ≈ 0 for a hull of revolution (fluid slips around the roll axis).

Source code in manta/parts/aero/added_mass.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    for label, value in (("translational", self.translational),
                         ("rotational", self.rotational)):
        vals = tuple(float(x) for x in value)
        if len(vals) != 3 or any(x < 0.0 for x in vals):
            raise ValueError(
                f"{type(self).__name__} {name!r}: {label} must be "
                f"three non-negative diagonal entries, got {value!r}")

manta.parts.FossenDamping

FossenDamping(name, *, damping=None, tensors=None, **overrides)

Bases: Part

Full 6×6 damping wrench, polynomial in ν = [v_rel; ω].

Parameters: damping — (kvx, kvy, kvz, kwx, kwy, kwz): the diagonal one-order shortcut (dissipative entries are < 0). tensors — full per-order 6×6 tensor list instead.

Tensors are per unit fluid density and applied additively — see the module docstring's sign-convention note before pasting a textbook D in here.

Source code in manta/parts/aero/fossen_damping.py
def __init__(self, name: str, *, damping: tuple | None = None,
             tensors: list | tuple | None = None,
             **overrides) -> None:
    flats = _as_orders(damping, tensors, name)
    for k, flat in enumerate(flats):
        overrides[f"D{k + 1}"] = flat
    super().__init__(name, **overrides)

manta.parts.Aerofoil

Aerofoil(name, **overrides)

Bases: Part

A cambered, Reynolds-aware lifting surface.

Parameters: area — m². Planform (reference) area. chord — m. Mean aerodynamic chord; the Reynolds reference length (and, for a ControlSurface, the wing chord a flap is measured against). chord_axis — unit vector along the chord (leading → trailing edge) in the part's OWN frame. Default (1, 0, 0); leave it there — see "Orientation" below. normal_axis — unit vector normal to the chord, in the foil plane; the +lift side. Default (0, 0, 1); leave it there. alpha_0 — zero-lift angle of attack, rad. 0 for a symmetric foil; negative for positive camber. From the camber line (Re-independent); naca(...) fills it. CL_alpha — lift-curve slope, per rad. Thin-airfoil ≈ 2π. Cm_ac — moment coefficient about the aerodynamic centre (constant; nose-down negative for positive camber). CL_max — reference (high-Re) stall lift coefficient. Scaled DOWN at low Reynolds number. CD_0 — reference (at Re ≈ 5·10⁵) zero-lift drag coefficient. Scaled by the local Reynolds number. induced_k — induced-drag factor: CD gains induced_k·CL², so induced_k ≈ 1/(π·AR·e) (≈0.05 for an AR-6 wing).

Orientation: use mount_orientation, not the axis pair.

chord_axis / normal_axis define this foil's CANONICAL frame — chord along +x, lift along +z — and should stay at their defaults. How the surface is INSTALLED is a mount rotation: rigging incidence, dihedral, a vertical stabiliser's 90° roll, an all-moving fin's station around a hull. All of those belong in Part.mount_orientation.

The axis pair predates static mount rotations, and encoding installation angles in it has three costs. One physical rotation gets spread across two hand-derived vectors that must stay mutually perpendicular (the constructor checks, because they can silently stop being so). A left/right mirrored pair becomes a sign buried in a vector component rather than the sign of a roll angle. And only the aerodynamics end up rotated — every other quantity the part sees is still in the unrotated frame, so the part's own idea of "its frame" and the framework's disagree.

The parameters remain for the models written before mount rotations existed (examples/vehicles/airplane.py); new ones should not touch them.

Source code in manta/parts/aero/aerofoil.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    who = f"{type(self).__name__} {name!r}"
    if float(self.area) <= 0.0:
        raise ValueError(f"{who}: area must be > 0, got {self.area!r}")
    if float(self.chord) <= 0.0:
        raise ValueError(f"{who}: chord must be > 0, got {self.chord!r}")
    if float(self.CL_max) <= 0.0:
        raise ValueError(f"{who}: CL_max must be > 0, got {self.CL_max!r}")
    if float(self.CD_0) < 0.0 or float(self.induced_k) < 0.0:
        raise ValueError(
            f"{who}: CD_0 and induced_k must be >= 0, got "
            f"CD_0={self.CD_0!r}, induced_k={self.induced_k!r}")
    self.chord_axis  = unit_axis(self.chord_axis,
                                 who=who, what="chord_axis")
    self.normal_axis = unit_axis(self.normal_axis,
                                 who=who, what="normal_axis")
    dot = sum(c * n for c, n in zip(self.chord_axis, self.normal_axis))
    if abs(dot) > 1e-6:
        raise ValueError(
            f"{who}: chord_axis and normal_axis must be perpendicular "
            f"(unit-vector dot product is {dot:.3e}).")

manta.parts.naca

naca(designation, name=None, **geom)

Build an Aerofoil for a NACA 4-digit section (e.g. "2412", "0012").

The first digit is max camber in % chord, the second its chordwise position in tenths, the last two the thickness in % chord. The Re-independent invariants — zero-lift angle alpha_0 and moment Cm_ac — are derived from the camber line by thin-airfoil theory; the reference CL_max and CD_0 are estimated from thickness (and are themselves rescaled by Reynolds number at run time). Any of these may be overridden, along with the geometry (area, chord, chord_axis, normal_axis, induced_k), via keyword.

Example::

a.add(naca("2412", "wing", area=0.72, chord=0.3))
Source code in manta/parts/aero/aerofoil.py
def naca(designation: str, name: str | None = None, **geom) -> Aerofoil:
    """Build an `Aerofoil` for a NACA 4-digit section (e.g. ``"2412"``,
    ``"0012"``).

    The first digit is max camber in % chord, the second its chordwise
    position in tenths, the last two the thickness in % chord. The
    Re-independent invariants — zero-lift angle `alpha_0` and moment
    `Cm_ac` — are derived from the camber line by thin-airfoil theory;
    the reference `CL_max` and `CD_0` are estimated from thickness (and
    are themselves rescaled by Reynolds number at run time). Any of these
    may be overridden, along with the geometry (`area`, `chord`,
    `chord_axis`, `normal_axis`, `induced_k`), via keyword.

    Example::

        a.add(naca("2412", "wing", area=0.72, chord=0.3))
    """
    d = designation.strip().upper().removeprefix("NACA").strip()
    if len(d) != 4 or not d.isdigit():
        raise ValueError(
            f"naca: expected a 4-digit designation like '2412', got "
            f"{designation!r}")
    m = int(d[0]) / 100.0          # max camber, fraction of chord
    p = int(d[1]) / 10.0           # position of max camber, fraction
    t = int(d[2:4]) / 100.0        # thickness, fraction of chord

    alpha_0, Cm_ac = _four_digit_invariants(m, p)
    # Thickness fits (engineering): form-factor profile drag and a stall
    # ceiling that grows mildly with thickness. Both are reference (high-Re)
    # values; the part rescales them by the local Reynolds number.
    CD_0 = 0.006 * (1.0 + 2.0 * t + 60.0 * t**4)
    CL_max = 1.2 + 1.5 * t

    params = {"alpha_0": alpha_0, "Cm_ac": Cm_ac, "CL_alpha": 2.0 * math.pi,
                  "CL_max": CL_max, "CD_0": CD_0}
    params.update(geom)            # caller overrides win
    return Aerofoil(name or f"naca{d}", **params)

manta.parts.ControlSurface

ControlSurface(name, **overrides)

Bases: Aerofoil

A wing section with a deflectable trailing-edge flap.

Inherits all Aerofoil geometry/aero parameters (these describe the WING section — area, chord, chord_axis, normal_axis, alpha_0, Cm_ac, CL_max, …); the flap is parameterised by its chord fraction, not a separate foil. The deflection is a single state driven by a commanded angle through a saturating first-order servo.

Flap + servo parameters: flap_chord_fraction — c_flap / c, in (0, 1). Sets the flap effectiveness τ and moment slope Cm_δ. servo_gain — K_servo, hinge restoring torque per rad of command error (N·m/rad). stall_torque — τ_stall, the servo's saturation torque (N·m). Aero hinge moment beyond this blows the surface back. hinge_damping — b, hinge viscous damping (N·m·s/rad); with servo_gain it sets the lag bandwidth K/b. max_deflection — travel limit, rad (the state saturates here). Ch_alpha, Ch_delta — hinge-moment coefficient slopes wrt angle of attack and deflection (both restoring, < 0). Engineering values; tune per surface.

Input: deflection_cmd — commanded deflection, rad. State: deflection — actual deflection δ, rad.

Source code in manta/parts/aero/control_surface.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    who = f"{type(self).__name__} {name!r}"
    E = float(self.flap_chord_fraction)
    if not (0.0 < E < 1.0):
        raise ValueError(
            f"{who}: flap_chord_fraction must be in (0, 1), got {E!r}")
    if float(self.hinge_damping) <= 0.0:
        raise ValueError(
            f"{who}: hinge_damping must be > 0, got "
            f"{self.hinge_damping!r}")
    if float(self.stall_torque) < 0.0 or float(self.servo_gain) < 0.0:
        raise ValueError(
            f"{who}: stall_torque and servo_gain must be >= 0")
    # Build-time flap derivatives (Re-independent geometry).
    self._tau, self._cm_delta = _flap_effectiveness(E)

Sensors

manta.parts.IMU

IMU(name, **overrides)

Bases: Part

Inertial-measurement unit with Kalibr-style 4-parameter noise.

Channels (override sigmas via construction): gyro_noise — vec3 white, per-tick rad/s. accel_noise — vec3 white, per-tick m/s². gyro_bias — vec3 RW, rad/s²/√Hz drift density. accel_bias — vec3 RW, m/s³/√Hz drift density.

The two RW channels add bias state slots that the EKF can estimate; skip them by leaving sigma at 0.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.ModelForce

ModelForce(name, *, imu, evidence=None, **overrides)

Bases: Part

Model-predicted sensor-frame specific force.

Args: imu — the colocated IMU whose raw accelerometer sample is the observation for specific_force (its cadence is the default rate). evidence — FitEvidence for that IMU's accel channel. Builds the complete error model (see module docstring) and refuses any hand-set error override alongside it. model_error_sigma — isotropic shorthand for the three white model_error_<axis>_sigma values (evidence-less use).

Per-axis channels (1-σ, m/s²): model_error_{x,y,z} (white) and model_error_correlated_{x,y,z} (Gauss–Markov, with model_error_correlated_<axis>_tau seconds). residual_bias is the deterministic held-out bias correction in the sensor frame.

Source code in manta/parts/sensor/model_force.py
def __init__(self, name: str, *, imu: IMU,
             evidence: FitEvidence | None = None, **overrides) -> None:
    if not isinstance(imu, IMU):
        raise TypeError("ModelForce: imu must be an IMU part")
    if "model_error_sigma" in overrides:
        iso = overrides.pop("model_error_sigma")
        for axis in _AXES:
            key = f"model_error_{axis}_sigma"
            if key in overrides:
                raise TypeError(
                    f"ModelForce({name!r}): model_error_sigma and {key} "
                    "both given")
            overrides[key] = iso
    if evidence is not None:
        overrides.update(self._overrides_from_evidence(
            name, imu, evidence, overrides))
    # The pseudo-reading is sourced from this accelerometer, so its
    # default cadence must agree.  A caller may still override ``rate``
    # explicitly for a downsampled observer channel.
    overrides.setdefault("rate", imu.rate)
    super().__init__(name, accelerometer=imu, evidence=evidence,
                     **overrides)

white_sigmas property

white_sigmas

Per-axis white model-error σ (the pseudo-measurement's R).

correlated_sigmas property

correlated_sigmas

Per-axis Gauss–Markov stationary σ (0 ⇒ inert channel).

correlated_taus property

correlated_taus

Per-axis Gauss–Markov correlation time, seconds.

manta.parts.VelocitySensor

VelocitySensor(name, **overrides)

Bases: Part

Body-frame linear-velocity sensor.

Outputs: velocity : Vec3[PartFrame] — the craft's inertial (ground- relative) velocity in the sensor's own case frame (R^T·v_anchor). For a root-mounted sensor that frame coincides with CraftFrame; on a rotor it spins with the joint.

Noise channel (set σ to engage): velocity_noise — vec3 white, per-tick m/s. Becomes the EKF's measurement R, exactly as PositionSensor's position_noise. Defaults to 0 (an ideal read).

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.Magnetometer

Magnetometer(name, **overrides)

Bases: Part

3-axis magnetometer.

Outputs: B : Vec3[PartFrame] — magnetic flux density at the sensor position, in the sensor's own frame. SI units (Tesla). For a sensor mounted directly on the craft root that frame coincides with CraftFrame; on a joint rotor it spins with the rotor.

Noise channel (set σ to engage): B_noise — vec3 white, per-tick Tesla. Becomes the EKF's measurement R for a heading/attitude fix, exactly as PositionSensor's position_noise does. Defaults to 0 (a clean reading).

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.PositionSensor

PositionSensor(name, **overrides)

Bases: Part

Outputs the sensor's world-frame position each tick.

Outputs: position : Vec3[WorldFrame] — sensor mount-point position in world frame; exactly what a GPS or mocap marker reads.

Noise channel (set σ to engage — leave at 0 for a noiseless oracle): position_noise : world-frame white noise on the reading. Engage it (PositionSensor("gps", position_noise_sigma=0.5)) to give the EKF an auto-built R for this sensor, e.g. when driving the filter through step().

Rate (Hz): rate : measurement rate. None (default) ⇒ a fresh fix every tick. Set it (PositionSensor("gps", rate=1.0)) to model a slow sensor: the Sim publishes a new reading once per 1/rate window and holds it in between, and the EKF folds each fix in exactly once. Pure metadata — the tick stays a smooth function (the estimator sees the continuous model).

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.Barometer

Barometer(name, **overrides)

Bases: Part

Outputs the local fluid pressure at the sensor each tick.

Outputs: pressure : scalar — absolute pressure (Pa) of the fluid at the sensor's world-frame position, read from the FluidField.

Noise channel (set σ to engage — leave at 0 for a noiseless oracle): pressure_noise : scalar white noise (Pa) on the reading. Engage it (Barometer("baro", pressure_noise_sigma=50.0)) to give the EKF an auto-built R for this sensor.

Rate (Hz): rate : measurement rate. None (default) ⇒ a fresh reading every tick. Set it (Barometer("baro", rate=10.0)) to model a slow sensor: the Sim publishes a new reading once per 1/rate window and holds it in between, and the EKF folds each in exactly once. Pure metadata — the tick stays a smooth function.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.ProjectiveCamera

ProjectiveCamera(name, *, width=None, height=None, hfov_deg=70.0, fx=None, fy=None, cx=None, cy=None, rate=None, noise_sigma=0.0, mount_offset=(0.0, 0.0, 0.0), mount_orientation=(1.0, 0.0, 0.0, 0.0))

Bases: Part

Base for a pinhole camera that measures OpticalField ellipsoids.

Construct with the image size and horizontal field of view (the focal length and a centred principal point are derived), or override any intrinsic explicitly. Subclasses set _COMPONENTS (the per-target scalar measurement names, excluding vis) and implement _project.

Parameters: width, height — image size in pixels. fx, fy, cx, cy — intrinsics (focal lengths + principal point, px). rate — optional capture rate (Hz); None ⇒ every tick. noise_sigma — per-component pixel measurement σ (subclasses expose it under a friendlier name). 0 ⇒ noiseless oracle, no noise channels (byte-identical to a camera with none), and not EKF-usable.

Source code in manta/parts/sensor/camera.py
def __init__(self, name: str, *, width=None, height=None,
             hfov_deg: float = 70.0, fx=None, fy=None, cx=None, cy=None,
             rate=None, noise_sigma: float = 0.0,
             mount_offset=(0.0, 0.0, 0.0),
             mount_orientation=(1.0, 0.0, 0.0, 0.0)) -> None:
    import math
    decls = self._declarations()
    w = float(width if width is not None else decls["width"].default)
    h = float(height if height is not None else decls["height"].default)
    f = (w / 2.0) / math.tan(math.radians(hfov_deg) / 2.0)
    super().__init__(
        name, width=w, height=h,
        fx=float(fx) if fx is not None else f,
        fy=float(fy) if fy is not None else (float(fx) if fx is not None else f),
        cx=float(cx) if cx is not None else w / 2.0,
        cy=float(cy) if cy is not None else h / 2.0,
        rate=rate, noise_sigma=float(noise_sigma),
        mount_offset=mount_offset,
        # Forwarded like mount_offset: the kinematic pass honors a
        # rotated camera (it looks down its own +z), so dropping this
        # kwarg would make rotated cameras unconstructable.
        mount_orientation=mount_orientation)
    # Set via set_targets() during World snapshot resolution (every
    # ellipsoid not on this camera's craft). Drives output/noise
    # declarations and update.
    self._targets: tuple = ()

on_world_resolve

on_world_resolve(world, craft)

Point the camera at every optical ellipsoid it can see — all but its own craft's. A camera with no OpticalField registered fails the requires_fields check at transform build; here we just skip target wiring.

Source code in manta/parts/sensor/camera.py
def on_world_resolve(self, world, craft) -> None:
    """Point the camera at every optical ellipsoid it can see — all
    but its own craft's. A camera with no OpticalField registered
    fails the `requires_fields` check at transform build; here we
    just skip target wiring."""
    from ...fields.optical import OpticalField
    optical = world.get_field(OpticalField)
    if optical is not None:
        self.set_targets(e for e in optical.ellipsoids
                         if e.source_craft is not craft)

set_targets

set_targets(targets)

Point the camera: fix the compile-time set of ellipsoids it measures. Called once during snapshot resolution before the tick is traced; the output/noise declarations follow it.

Also materializes the per-channel <name>_sigma attributes the framework reads off the instance (Noise.is_active, the tick- signature walk, Craft.sample_noise) — this is the one mutation point; noise_declarations() stays a pure read.

Source code in manta/parts/sensor/camera.py
def set_targets(self, targets) -> None:
    """Point the camera: fix the compile-time set of ellipsoids it
    measures. Called once during snapshot resolution before
    the tick is traced; the output/noise declarations follow it.

    Also materializes the per-channel `<name>_sigma` attributes the
    framework reads off the instance (`Noise.is_active`, the tick-
    signature walk, `Craft.sample_noise`) — this is the one mutation
    point; `noise_declarations()` stays a pure read."""
    self._targets = tuple(targets)
    sigma = float(self.noise_sigma)
    if sigma > 0.0:
        for e in self._targets:
            for suffix in self._COMPONENTS:
                setattr(self, f"{e.name}_{suffix}_noise_sigma", sigma)

manta.parts.BBoxCamera

BBoxCamera(name, *, bbox_sigma=0.0, **kw)

Bases: ProjectiveCamera

Pinhole camera emitting per-object image-frame bounding boxes.

Outputs per visible source S: <S>_xmin/_ymin/_xmax/_ymax (pixel box, clamped to the image) and <S>_vis (1 when in front and a real ellipse, else 0). The box size encodes range given the target's semi-axes.

BBoxCamera("cam", width=640, height=480, hfov_deg=70)
BBoxCamera("cam", width=1280, height=720, bbox_sigma=2.0)   # EKF-usable
Source code in manta/parts/sensor/camera.py
def __init__(self, name: str, *, bbox_sigma: float = 0.0, **kw) -> None:
    super().__init__(name, noise_sigma=bbox_sigma, **kw)

manta.parts.CentroidCamera

CentroidCamera(name, *, pixel_sigma=0.0, **kw)

Bases: ProjectiveCamera

Pinhole camera emitting per-object image-frame CENTROIDS (u, v).

A centroid is the projection of the target's centre — a pure bearing, independent of the target's size. Outputs per visible source S: <S>_u, <S>_v (pixels) and <S>_vis. One camera fixes a ray; space several apart and select their _u/_v as EKF sensors and the filter triangulates the target's 3-D position (the wider the baseline, the better the range — size never enters).

CentroidCamera("c0", width=1280, height=720, hfov_deg=40,
               pixel_sigma=1.0)
Source code in manta/parts/sensor/camera.py
def __init__(self, name: str, *, pixel_sigma: float = 0.0, **kw) -> None:
    super().__init__(name, noise_sigma=pixel_sigma, **kw)

manta.parts.Antenna

Antenna(name, **overrides)

Bases: Part

A kinematic marker for an RF antenna phase center and local frame.

Outputs: position : Vec3[WorldFrame] Antenna phase-center position in world coordinates. orientation : Quat[WorldFrame, PartFrame] World-from-antenna unit quaternion in (w, x, y, z) order. angular_velocity : Vec3[WorldFrame] Absolute angular velocity of the antenna frame, expressed in world coordinates, in rad/s.

These are ideal kinematic outputs. Radiation patterns, propagation, tracking, link quality, and packet behavior belong to downstream users of the model, not to this marker.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

Attachment and disturbance

manta.parts.TetherEndpoint

TetherEndpoint(name, **overrides)

Bases: Part

Marker Part for one end of a tether. No wrench contribution; the Tether coupling applies the actual force using this part's transform as the attachment offset.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.TrajectoryEndpoint

TrajectoryEndpoint(name, **overrides)

Bases: Part

Spring-damper that slews its craft along a reference pose path.

Parameters: trajectory — callable f(t) -> TrajectorySample taking the symbolic clock (a Scalar) and returning the reference at that time. Required. kp_pos, kd_pos — position spring / damping gains (N per m, N per m/s). With mass set, critical damping is kd_pos = 2·sqrt(kp_pos·mass). kp_att, kd_att — attitude spring / damping gains (N·m per rad, N·m per rad/s). mass — craft mass (kg). When > 0, enables gravity + linear- acceleration feedforward for tight tracking. 0 (the default) is a pure spring — fine for light craft and a zero-gravity world, but it will droop under gravity.

Mount it on the craft root at the origin (the default transform). An off-origin mount would inject a force×lever-arm torque that corrupts the attitude channel.

Source code in manta/parts/attachment/trajectory_endpoint.py
def __init__(self, name: str, **overrides) -> None:
    super().__init__(name, **overrides)
    if self.trajectory is None or not callable(self.trajectory):
        raise ValueError(
            f"{type(self).__name__}({name!r}): `trajectory` must be a "
            f"callable f(t) -> TrajectorySample.")
    if float(self.mass) < 0.0:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mass must be >= 0.")

manta.parts.ProcessNoise

ProcessNoise(name, **overrides)

Bases: Part

White force/torque wrench — model uncertainty as Langevin forcing.

Set σ to engage a channel (force_noise_sigma= / torque_noise_sigma= on construction); both default to 0 (a perfectly modeled craft). The EKF assembles Q from the engaged channels and NoiseDriver excites the truth identically.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

Field sources

manta.parts.GravitySource

GravitySource(name, **overrides)

Bases: FieldSource

Adds a point-mass gravity disturbance that rides the carrying craft.

Parameters: GM — gravitational parameter G·M (m³/s²). Earth ≈ 3.986e14, Moon ≈ 4.903e12, a 1000-ton asteroid ≈ 6.7e-5. eps — softening length (m) capping the singularity at the source.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.MagneticSource

MagneticSource(name, **overrides)

Bases: FieldSource

Adds a magnetic dipole disturbance that rides the carrying craft.

Parameters: moment — (mx, my, mz) dipole moment in the craft BODY frame, A·m². A small hobby motor magnet is ~1e-2–1e-1. eps — softening length (m) at the dipole position.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))

manta.parts.OpticalSource

OpticalSource(name, **overrides)

Bases: FieldSource

Adds a semantic ellipsoid disturbance that rides the carrying craft.

Parameters: semi_axes — (a, b, c) half-extents of the bounding ellipsoid along the craft's body axes, m. Roughly half the vehicle's length/width/height. label — integer class id the camera reports with each box.

Source code in manta/parts/base.py
def __init__(self, name: str, **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(name, who=type(self).__name__)
    self.parent: Part | None = None
    self._apply_declarations(overrides)
    q = np.asarray(self.declared_value("mount_orientation"), dtype=float)
    if q.shape != (4,):
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"wxyz quaternion (4 values); got {q.shape}")
    norm = float(np.linalg.norm(q))
    if not np.isfinite(norm) or norm < 1e-12:
        raise ValueError(
            f"{type(self).__name__}({name!r}): mount_orientation must be a "
            f"finite non-zero quaternion; got {tuple(q)}")
    if abs(norm - 1.0) > 1e-9:
        # Normalize once here rather than per tick: a rotation that
        # drifts off the unit sphere silently rescales every vector
        # it touches.
        self.mount_orientation = tuple(float(v) for v in q / norm)
    factor = np.asarray(
        self.declared_value("mount_uncertainty_sqrt"), dtype=float
    )
    if factor.size != 36:
        raise ValueError(
            f"{type(self).__name__}({name!r}): "
            "mount_uncertainty_sqrt must contain 36 row-major values"
        )
    factor = factor.reshape(6, 6)
    if float(self.mount_uncertainty_sigma) > 0.0 and not np.any(factor):
        raise ValueError(
            f"{type(self).__name__}({name!r}): active mount uncertainty "
            "requires a nonzero covariance square root"
        )
    self.mount_uncertainty_sqrt = tuple(float(v) for v in factor.reshape(-1))