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 ¶
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 transform parameter — a static (x, y, z) position
offset from its parent's output frame. Static orientation between
part and parent is currently fixed at identity; non-identity static
rotations would be a future extension. The framework uses this
transform to roll the part's wrench up into its parent's frame
(force-at-offset → torque contribution at parent origin).
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,
transform=(0.0, 0.0, -0.5)) # 0.5 m below parent origin
Source code in manta/parts/base.py
noise_R ¶
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).
Used by the EKF to size measurement updates without the user
having to specify R separately:
ekf.update(h, z, R=imu.noise_R("gyro_noise")).
Source code in manta/parts/base.py
update ¶
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
manta.parts.CompositePart ¶
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 (translation only, via transform). 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, transform=(0.1, 0, 0)))
Source code in manta/parts/base.py
manta.parts.Parameter ¶
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.
Source code in manta/parts/_declarations.py
manta.parts.State ¶
Bases: _Declaration
Per-tick state slot.
Declared at class scope. The framework:
* Creates a graph input named "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
manta.parts.Input ¶
Bases: _Declaration
Per-tick external value.
Declared at class scope on a Part. The framework:
* Creates a graph input named "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
manta.parts.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 "
Source code in manta/parts/_declarations.py
manta.parts.Noise ¶
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".
- A state slot
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
resolved_signal_manifold ¶
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
state_manifold ¶
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
is_active ¶
Is this channel currently producing nonzero output? Reads
the runtime <name>_sigma attribute on the owner.
driver_input_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
initial_state_entries ¶
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
synthesize ¶
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
manta.parts.PartUpdate ¶
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 rate in Hz (None ⇒ every tick). It is metadata only — the
compiled tick stays a pure function (no sample-and-hold state enters
the kernel, so it never complicates autodiff or the EKF/LQR
linearization). The runtimes gate the matching port: an Output is
published once per 1/rate window and held in between; an Input
command is latched (ZOH) once per window, so truth and the
estimator's predict see the same held command.
Stateless parts can return a bare Wrench instead — the framework
wraps it as PartUpdate(wrench=w) automatically.
Source code in manta/parts/_declarations.py
Structure¶
manta.parts.Mass ¶
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, ctx.field(GravityField) returns an empty default and
the contribution is identically zero — no special-case opt-out
needed.
The part's spatial location is set via its transform 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
manta.parts.PointBuoy ¶
Bases: Part
Single-point buoyancy displacing a fixed volume.
Parameters: volume — m³ displaced by the buoyancy element. Default 1e-3.
Force = -ρ(p_world) · V · g(p_world) 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
manta.parts.Collider ¶
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 velocity perpendicular to the outward normal, gated smoothly by penetration (a smooth, EKF-friendly stand-in for Coulomb friction — grips a resting contact against sliding). Default 0 (frictionless contact, the prior behaviour).
Source code in manta/parts/base.py
Actuation¶
manta.parts.Thruster ¶
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
Articulation¶
manta.parts.RevoluteJoint ¶
Bases: ArticulatedJoint
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
manta.parts.PrismaticJoint ¶
Bases: ArticulatedJoint
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
Aerodynamics¶
manta.parts.DragSurface ¶
Bases: Part
Polynomial drag/lift surface (legacy Surface1..4).
Parameters: force_tensors — list of 3×3 matrices [A_1, A_2, …, A_N], CraftFrame. Default is a single zero matrix (no drag). torque_tensors — same shape, for the surface's contribution to body torque about the mount point.
Convenience args (mutually exclusive with the *_tensors form): force=(x,y,z) — sets A_1 = diag(x, y, z) (per-axis linear drag). torque=(x,y,z) — sets B_1 = diag(x, y, z) (per-axis linear torque).
Source code in manta/parts/aero/drag_surface.py
isotropic_quadratic
classmethod
¶
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
directional_quadratic
classmethod
¶
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
manta.parts.Naca00xx ¶
Bases: Part
Symmetric airfoil (NACA 00xx family).
Parameters: area — m². Planform area of the airfoil. chord_axis — body-frame unit vector along the chord (leading → trailing edge). Default (1, 0, 0). normal_axis — body-frame unit vector normal to the chord, in the airfoil's plane. Positive α (wind from below) → positive lift along this axis. Default (0, 0, 1). CL_max — dimensionless. Peak lift coefficient (achieved at α=45° in this model). For a NACA 0012 a realistic stall-limited value is ~1.0–1.3. CD_0 — dimensionless. Zero-lift drag coefficient (skin friction + pressure drag at α=0). Typical: 0.005–0.015 for clean airfoils. induced_k — dimensionless. Induced/lift-dependent drag coefficient. CD = CD_0 + induced_k·sin²(α). Higher for lower aspect-ratio wings.
Source code in manta/parts/aero/naca_airfoil.py
Sensors¶
manta.parts.IMU ¶
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
manta.parts.DVL ¶
Bases: Part
Body-frame linear-velocity sensor.
Outputs: velocity : Vec3[CraftFrame] — body-frame velocity (R^T·v_anchor). What a DVL reads when locked to a reference (seafloor / ground).
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
manta.parts.Magnetometer ¶
Bases: Part
3-axis magnetometer.
Outputs: B : Vec3[CraftFrame] — 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 is 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
manta.parts.PositionSensor ¶
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
manta.parts.ProjectiveCamera ¶
ProjectiveCamera(name, *, width=640.0, height=480.0, hfov_deg=70.0, fx=None, fy=None, cx=None, cy=None, rate=None, noise_sigma=0.0, transform=(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
set_targets ¶
Point the camera: fix the compile-time set of ellipsoids it
measures. Called once by World.finalize() 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
manta.parts.BBoxCamera ¶
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
manta.parts.CentroidCamera ¶
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
Attachment and disturbance¶
manta.parts.TetherEndpoint ¶
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
manta.parts.TrajectoryEndpoint ¶
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
manta.parts.ProcessNoise ¶
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
Field sources¶
manta.parts.GravitySource ¶
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
manta.parts.MagneticSource ¶
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
manta.parts.OpticalSource ¶
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.