Skip to content

Fields

Each Field is a typed superposition of Disturbance objects, combined per each disturbance's combining flag. See Fields and disturbances for the concepts.

Base classes

manta.fields.Field

Field()

Base for a typed physical field — a host that CARRIES disturbances.

A Field fixes value_shape (the CasADi-MX type its disturbances produce) and registers compatible disturbances via add(). Two kinds of field extend it:

  • SuperposedField — the disturbances SUM into one value_at_sym(point, t) (gravity, B-field, fluid, collision).
  • OpticalField (an enumerated field) — the disturbances are kept SEPARATE and enumerated, never summed (a camera draws one box per ellipsoid). It has no value_at_sym.

Field itself is the shared host (the registry + the value-shape contract); it is not meant to be instantiated directly.

Source code in manta/fields/base.py
def __init__(self) -> None:
    self._disturbances: list[Disturbance] = []

add

add(disturbance)

Register a disturbance with this field. Returns self for chaining.

Source code in manta/fields/base.py
def add(self, disturbance: Disturbance) -> "Field":
    """Register a disturbance with this field. Returns self for chaining."""
    if not isinstance(disturbance, Disturbance):
        raise TypeError(
            f"{type(self).__name__}.add: expected Disturbance, got "
            f"{type(disturbance).__name__}")
    if disturbance.field_value_shape is not self.value_shape:
        raise TypeError(
            f"{type(self).__name__}.add: disturbance "
            f"{type(disturbance).__name__} produces "
            f"{disturbance.field_value_shape!r}, not "
            f"{self.value_shape!r}")
    self._disturbances.append(disturbance)
    return self

manta.fields.Disturbance

Disturbance(name=None, *, combining=None, membership=None, **overrides)

Bases: DeclarationHost, ABC

Base for one contribution to a Field.

Subclass and implement contribute_at_sym(point, t). The returned MX must have the Field's value shape (e.g. Vec3[WorldFrame] for GravityField). Multiple disturbances on the same field combine according to their combining flag (see Field.value_at_sym).

Disturbances may declare State / Noise channels at class scope. The framework picks them up at compile time exactly like it does for Parts: * Each State slot becomes a graph input + output named <disturbance.name>.<slot>; the disturbance's attribute is rebound to the symbolic input inside contribute_at_sym. * Each white Noise channel becomes a per-tick graph input. * Each RW Noise channel (sigma > 0) synthesizes a bias state + driver, evolving via bias_next = bias + sqrt(dt)·driver.

Args: name — identifier used as the IR-slot prefix. Must be unique across the world's disturbances. Defaults to <ClassName>_<counter>. combining — how this disturbance's contribution composes with others on the same field. One of: "additive" (default) — straight linear sum. "baseline" — a regime medium (e.g. ocean / air); baselines layer by membership rather than summing. "averaged" — a membership-weighted self-mean among the averaged disturbances (an estimation overlay, e.g. overlapping wind bubbles agreeing on a mean). Only FluidField interprets "baseline"/"averaged" (see FluidField.value_at_sym); for other superposed fields (gravity, B-field) every disturbance is summed additively regardless of this flag. membership — optional callable (point, t) -> MX in [0, 1] giving this disturbance's spatial support. Defaults to 1 everywhere. Used by FluidField to bound regimes / perturbations; ignored by purely-additive fields.

Source code in manta/fields/base.py
def __init__(self, name: str | None = None, *,
             combining: str | None = None,
             membership=None,
             **overrides: Any) -> None:
    from ..ir.module import check_name
    self.name = check_name(
        name if name is not None else (
            f"{type(self).__name__}_{next(_DISTURBANCE_NAME_COUNTER)}"),
        who=type(self).__name__)
    if combining is not None:
        if combining not in ("additive", "averaged", "baseline"):
            raise ValueError(
                f"Disturbance: combining must be 'additive', "
                f"'averaged', or 'baseline'; got {combining!r}")
        self.combining = combining
    if membership is not None:
        self._membership = membership
    self._apply_declarations(overrides)

contribute_at_sym abstractmethod

contribute_at_sym(point, t)

Return this disturbance's contribution at the given world-frame point at world-clock time t (Scalar MX). Output type matches the host Field's value type. Static disturbances accept t and ignore it.

Source code in manta/fields/base.py
@abstractmethod
def contribute_at_sym(self, point: "Vec3", t):
    """Return this disturbance's contribution at the given world-frame
    point at world-clock time `t` (Scalar MX). Output type matches
    the host Field's value type. Static disturbances accept `t` and
    ignore it."""
    raise NotImplementedError

membership

membership(point, t)

Spatial support of this disturbance at point/t, an MX in [0, 1]. Defaults to 1 everywhere (global); a membership= callable passed at construction, or a subclass override, narrows it (a sea half-space, a bubble). FluidField weights each contribution by this; additive vector fields ignore it.

Source code in manta/fields/base.py
def membership(self, point: "Vec3", t) -> "ca.MX":
    """Spatial support of this disturbance at `point`/`t`, an MX in
    [0, 1]. Defaults to 1 everywhere (global); a `membership=`
    callable passed at construction, or a subclass override, narrows
    it (a sea half-space, a bubble). `FluidField` weights each
    contribution by this; additive vector fields ignore it."""
    if getattr(self, "_membership", None) is not None:
        return self._membership(point, t)
    return ca.MX(1.0)

Gravity

manta.fields.GravityField

GravityField(g=None)

Bases: SuperposedField

Gravitational acceleration g(point) in the WorldFrame.

value_at_sym(point) returns Vec3[WorldFrame] giving the acceleration a free-falling test mass would experience at point.

The add_uniform builder returns self for chaining: GravityField().add_uniform((0,0,-9.81)). As a convenience, the constructor accepts g=(gx,gy,gz) for the common single-uniform case — equivalent to one add_uniform call.

Source code in manta/fields/gravity.py
def __init__(self, g: tuple[float, float, float] | None = None) -> None:
    super().__init__()
    if g is not None:
        self.add_uniform(g)

add_uniform

add_uniform(g_vec)

Attach a position-independent gravity vector. Returns self.

Other disturbances (point-mass, J2, …) attach via the generic field.add(PointMassGravity(...)).

Source code in manta/fields/gravity.py
def add_uniform(self, g_vec: tuple[float, float, float]) -> "GravityField":
    """Attach a position-independent gravity vector. Returns self.

    Other disturbances (point-mass, J2, …) attach via the generic
    `field.add(PointMassGravity(...))`."""
    return self.add(UniformGravity(g_vec))

manta.fields.UniformGravity

UniformGravity(g_vec, *, name=None)

Bases: Disturbance

Position-independent gravity vector. The standard default for sims that don't care about altitude variation.

Args: g_vec — (x, y, z) gravity acceleration in WorldFrame, m/s². Conventionally (0, 0, -9.81) for Earth-near-surface with z pointing up.

Source code in manta/fields/gravity.py
def __init__(self, g_vec: tuple[float, float, float], *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.g_vec = tuple(float(x) for x in g_vec)
    if len(self.g_vec) != 3:
        raise ValueError(
            f"UniformGravity: g_vec must be length-3, got {g_vec!r}")

manta.fields.PointMassGravity

PointMassGravity(position, GM, eps=1.0, *, name=None)

Bases: Disturbance

Newtonian gravity from a point mass at a fixed anchor position.

g(p) = -GM · (p - r_src) / |p - r_src|³

Args: position — (x, y, z) source position in WorldFrame, meters. GM — gravitational parameter (G·M), m³/s². For Earth GM ≈ 3.986e14; for the Moon ≈ 4.903e12. eps — softening length to avoid singularity at r→0 (m). Defaults to 1.0 — far below any realistic orbital scale, well above numerical noise.

Source code in manta/fields/gravity.py
def __init__(self,
             position: tuple[float, float, float],
             GM: float,
             eps: float = 1.0, *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.position = tuple(float(x) for x in position)
    self.GM = float(GM)
    self.eps = float(eps)
    if self.GM <= 0.0:
        raise ValueError(
            f"PointMassGravity: GM must be > 0, got {GM!r}")
    # eps == 0 is allowed: exact Newtonian, singular only at the source.
    if self.eps < 0.0:
        raise ValueError(
            f"PointMassGravity: eps must be >= 0, got {eps!r}")

manta.fields.J2Gravity

J2Gravity(position, GM, J2, eq_radius, polar_axis=(0.0, 0.0, 1.0), eps=1.0, *, name=None)

Bases: Disturbance

J2 oblateness perturbation around a point mass.

For an oblate spheroid with equatorial bulge, the gravitational potential acquires a J2 term beyond the point-mass; the corresponding acceleration is

g_J2(r) = -(3/2) · J2 · GM · R_eq² / r⁵ · [
    (1 - 5(ẑ·r̂)²) · r + 2(ẑ·r̂)·|r|·ẑ
]

where ẑ is the polar (spin) axis. This is the perturbation only — add it alongside a PointMassGravity to model the full field.

Args: position — (x, y, z) planet center in WorldFrame, m. GM — gravitational parameter (G·M), m³/s². J2 — dimensionless J2 coefficient. Earth: 1.0826e-3. eq_radius — equatorial radius, m. Earth: 6.378e6. polar_axis — unit polar/spin axis in WorldFrame. Default (0, 0, 1). eps — softening length, m.

Source code in manta/fields/gravity.py
def __init__(self,
             position: tuple[float, float, float],
             GM: float,
             J2: float,
             eq_radius: float,
             polar_axis: tuple[float, float, float] = (0.0, 0.0, 1.0),
             eps: float = 1.0, *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.position = tuple(float(x) for x in position)
    self.GM = float(GM)
    self.J2 = float(J2)
    self.eq_radius = float(eq_radius)
    axis = ca.DM([float(polar_axis[0]),
                  float(polar_axis[1]),
                  float(polar_axis[2])])
    n = float(ca.norm_2(axis))
    if n == 0.0:
        raise ValueError("J2Gravity: polar_axis must be nonzero.")
    self._axis = axis / n
    self.eps = float(eps)
    if self.GM <= 0.0:
        raise ValueError(f"J2Gravity: GM must be > 0, got {GM!r}")
    if self.eq_radius <= 0.0:
        raise ValueError(
            f"J2Gravity: eq_radius must be > 0, got {eq_radius!r}")
    if self.eps < 0.0:
        raise ValueError(f"J2Gravity: eps must be >= 0, got {eps!r}")

manta.fields.BodyPointMassGravity

BodyPointMassGravity(craft, offset_body, GM, eps=1.0, *, name=None)

Bases: Disturbance

Inverse-square gravity from a point mass that RIDES a craft — the field a GravitySource part emits to simulate a massive body (planet, asteroid, station) moving through the sim. Same law as PointMassGravity, but the source position tracks the carrying craft's pose (read from the active trace), not a fixed world point.

Args: craft — the craft carrying the source. offset_body — the source position in the craft's body frame, m. GM — gravitational parameter (G·M), m³/s². eps — softening length, m. Default 1.0.

Source code in manta/fields/gravity.py
def __init__(self, craft, offset_body, GM: float,
             eps: float = 1.0, *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.craft = craft
    self.offset_body = tuple(float(x) for x in offset_body)
    self.GM = float(GM)
    self.eps = float(eps)
    if self.GM <= 0.0:
        raise ValueError(f"BodyPointMassGravity: GM must be > 0, got {GM!r}")
    if self.eps < 0.0:
        raise ValueError(f"BodyPointMassGravity: eps must be >= 0, got {eps!r}")

Fluid

manta.fields.FluidField

FluidField(density=None, velocity=(0.0, 0.0, 0.0), *, pressure=0.0, temperature=0.0, viscosity=None)

Bases: SuperposedField

Fluid density + pressure + temperature + bulk velocity over the world frame.

The field value at a point is a FluidState. Concrete sources are added as Disturbance subclasses with a combining role (see the module docstring): baseline regime media, averaged estimation overlays, and additive perturbations.

The add_uniform builder returns self for chaining; other disturbances attach via field.add(CurrentFlow(...)). The constructor accepts density=/pressure=/temperature= for the common single-uniform case — equivalent to one add_uniform baseline.

Source code in manta/fields/fluid.py
def __init__(self,
             density: float | None = None,
             velocity: tuple[float, float, float] = (0.0, 0.0, 0.0),
             *,
             pressure: float = 0.0,
             temperature: float = 0.0,
             viscosity: float | None = None,
             ) -> None:
    super().__init__()
    if density is not None:
        self.add_uniform(density, velocity,
                         pressure=pressure, temperature=temperature,
                         viscosity=viscosity)

value_at_sym

value_at_sym(point, t)

Combine every registered disturbance at point/t into one FluidState, per the baseline / averaged / additive rule set (module docstring). Returns the zero value if none are registered.

Source code in manta/fields/fluid.py
def value_at_sym(self, point: "Vec3", t) -> FluidState:
    """Combine every registered disturbance at `point`/`t` into one
    `FluidState`, per the baseline / averaged / additive rule set
    (module docstring). Returns the zero value if none are
    registered.
    """
    if not self._disturbances:
        return self._zero_value()

    baselines = [d for d in self._disturbances if d.combining == "baseline"]
    averaged  = [d for d in self._disturbances if d.combining == "averaged"]
    additive  = [d for d in self._disturbances if d.combining == "additive"]

    # Baseline regimes: layered alpha-composite override in insertion
    # order. base ← (1 − w)·base + w·contribution.
    base = self._zero_value()
    for d in baselines:
        w = d.membership(point, t)
        c = d.contribute_at_sym(point, t)
        base = base.scaled(1.0 - w) + c.scaled(w)

    # Averaged overlays: membership-weighted self-mean.
    avg = self._zero_value()
    if averaged:
        num = self._zero_value()
        den = ca.MX(0.0)
        for d in averaged:
            w = d.membership(point, t)
            num = num + d.contribute_at_sym(point, t).scaled(w)
            den = den + w
        avg = num.scaled(1.0 / (den + 1e-9))

    # Additive perturbations: membership-weighted sum on top.
    pert = self._zero_value()
    for d in additive:
        w = d.membership(point, t)
        pert = pert + d.contribute_at_sym(point, t).scaled(w)

    return base + avg + pert

add_uniform

add_uniform(density, velocity=(0.0, 0.0, 0.0), *, pressure=0.0, temperature=0.0, viscosity=None)

Attach a uniform baseline medium (density + optional pressure, temperature, viscosity, flow). Viscosity defaults to Sutherland's law for air; pass it explicitly for a liquid. Returns self.

Source code in manta/fields/fluid.py
def add_uniform(self,
                density: float,
                velocity: tuple[float, float, float] = (0.0, 0.0, 0.0),
                *,
                pressure: float = 0.0,
                temperature: float = 0.0,
                viscosity: float | None = None,
                ) -> "FluidField":
    """Attach a uniform baseline medium (density + optional pressure,
    temperature, viscosity, flow). Viscosity defaults to Sutherland's
    law for air; pass it explicitly for a liquid. Returns self."""
    return self.add(UniformFluid(density, velocity,
                                 pressure=pressure,
                                 temperature=temperature,
                                 viscosity=viscosity))

manta.fields.FluidState dataclass

FluidState(density, pressure, temperature, viscosity, velocity)

Local fluid properties at a world-frame point.

Fields are ordered density, pressure, temperature, viscosity, velocity — the four scalars first, the Vec3 velocity last — and that order is used in every signature and call site.

density — kg/m³. CasADi-MX scalar (composes with symbolic state). pressure — Pa. MX scalar. temperature — K. MX scalar. viscosity — dynamic viscosity μ, Pa·s. MX scalar. An independent property (like density): a gas baseline fills it from temperature via Sutherland's law, while water sets it directly. Drives the Reynolds number a foil sees. Perturbation/overlay disturbances that carry no viscosity pass ca.MX(0.0). velocity — bulk fluid velocity at the point, Vec3[WorldFrame].

Disturbances and FluidField.value_at_sym return / consume this type. __add__ (per-component sum) backs the additive pool; scaled (per-component scalar multiply) backs the membership weighting in the baseline / averaged blends.

scaled

scaled(s)

This state with every component multiplied by the MX (or float) scalar s — the membership weight in the blends.

Source code in manta/fields/fluid.py
def scaled(self, s) -> "FluidState":
    """This state with every component multiplied by the MX (or
    float) scalar `s` — the membership weight in the blends."""
    return FluidState(
        density     = s * self.density,
        pressure    = s * self.pressure,
        temperature = s * self.temperature,
        viscosity   = s * self.viscosity,
        velocity    = _VEC3_W.from_mx(s * self.velocity._mx),
    )

manta.fields.UniformFluid

UniformFluid(density, velocity=(0.0, 0.0, 0.0), *, pressure=0.0, temperature=0.0, viscosity=None, name=None, combining=None, membership=None)

Bases: Disturbance

Position-independent baseline medium: constant density (+ optional pressure, temperature, flow).

A baseline regime — where it is active (its membership, global by default) it defines the ambient fluid rather than adding to it.

Args: density — kg/m³. Common: ~1.225 (air), ~1025 (seawater), ~1000 (fresh water). velocity — bulk flow vector in WorldFrame, m/s. Default zero. pressure — Pa. Default 0 (unset). temperature — K. Default 0 (unset). viscosity — dynamic viscosity μ, Pa·s. Default None → filled from Sutherland's law for air at temperature (or ISA sea level if temperature is unset), giving ~1.79e-5 for a bare air medium. Liquids and exotic gases pass μ explicitly (seawater ≈ 1.35e-3).

Source code in manta/fields/fluid.py
def __init__(self,
             density: float,
             velocity: tuple[float, float, float] = (0.0, 0.0, 0.0),
             *,
             pressure: float = 0.0,
             temperature: float = 0.0,
             viscosity: float | None = None,
             name: str | None = None,
             combining: str | None = None,
             membership=None) -> None:
    super().__init__(name=name, combining=combining, membership=membership)
    self.density     = float(density)
    self.pressure    = float(pressure)
    self.temperature = float(temperature)
    self.velocity    = tuple(float(x) for x in velocity)
    if self.density < 0.0:
        raise ValueError(
            f"UniformFluid: density must be >= 0, got {density!r}")
    if len(self.velocity) != 3:
        raise ValueError(
            f"UniformFluid: velocity must be length-3, got {velocity!r}")
    # Default-fill viscosity from Sutherland(T) for air — a lazy import
    # keeps the fields↔planets dependency one-directional at load time.
    if viscosity is None:
        from ..planets.atmosphere import T0_ISA, sutherland_viscosity
        T = self.temperature if self.temperature > 0.0 else T0_ISA
        viscosity = float(sutherland_viscosity(T))
    self.viscosity = float(viscosity)
    if self.viscosity < 0.0:
        raise ValueError(
            f"UniformFluid: viscosity must be >= 0, got {viscosity!r}")

manta.fields.CurrentFlow

CurrentFlow(velocity, *, name=None, combining=None, membership=None)

Bases: Disturbance

Localized current — an additive velocity perturbation that leaves density / pressure / temperature untouched.

v1 ships the simplest non-spatial model: a constant velocity contribution everywhere (bound it with a membership= for a pocket). Future versions will accept a Gaussian envelope or a tabulated map.

Args: velocity — world-frame velocity contribution, m/s.

Source code in manta/fields/fluid.py
def __init__(self,
             velocity: tuple[float, float, float],
             *,
             name: str | None = None,
             combining: str | None = None,
             membership=None) -> None:
    super().__init__(name=name, combining=combining, membership=membership)
    self.velocity = tuple(float(x) for x in velocity)
    if len(self.velocity) != 3:
        raise ValueError(
            f"CurrentFlow: velocity must be length-3, got {velocity!r}")

manta.fields.WeatherPatch

WeatherPatch(*, temperature=0.0, pressure=0.0, density=0.0, name=None, combining=None, membership=None)

Bases: Disturbance

Local thermodynamic perturbation — additive temperature / pressure (and optional density) deltas layered on top of the ambient regime.

The planet's Atmosphere / Ocean baseline already gives a sane average (T, P, ρ) everywhere; a WeatherPatch is how a user paints a custom LOCAL curve over it — a warm thermal, a low-pressure cell, a surface inversion — without touching the baseline. Bind it to a region with membership= (e.g. within_sphere(...)); the default is global.

Each of temperature (K), pressure (Pa) and density (kg/m³) is either a constant or a callable (point: Vec3[WorldFrame], t) -> ca.MX for a position/time-varying field — so a curve is just a Python function of the query point. They are deltas: they add to whatever the baseline (and any other patches) already report.

Note: this perturbs the (T, P, ρ) components independently — it does NOT re-impose the ideal-gas tie between them. That is deliberate (the user is authoring the curve they want); pass whichever components you care about and leave the rest at 0.

Args: temperature — K delta. Constant or (point, t) -> MX. Default 0. pressure — Pa delta. Constant or (point, t) -> MX. Default 0. density — kg/m³ delta. Constant or (point, t) -> MX. Default 0.

Source code in manta/fields/fluid.py
def __init__(self,
             *,
             temperature=0.0,
             pressure=0.0,
             density=0.0,
             name: str | None = None,
             combining: str | None = None,
             membership=None) -> None:
    super().__init__(name=name, combining=combining, membership=membership)
    self.temperature = temperature
    self.pressure    = pressure
    self.density     = density

manta.fields.CraftWindBubble

CraftWindBubble(craft, radius=20.0, sigma=0.001, *, boundary=0.0, name=None, combining=None)

Bases: Disturbance

A localized wind contribution anchored to a craft.

Density / pressure / temperature contributions are zero (wind only moves the air). The wind vector is the disturbance's value everywhere; its spatial confinement to the bubble (where |point - craft.position| < radius) is the membership — a smooth sphere indicator (1 inside, 0 outside, blended over boundary).

combining="averaged" keeps the estimation design intent: the field folds all averaged disturbances into a membership-weighted mean, so where two crafts' bubbles overlap their wind estimates agree on the weighted average (rather than summing).

The wind itself is a RandomWalkNoise channel — the framework synthesizes a state slot named <bubble.name>.wind and an RW driver, evolving the wind via wind_next = wind + sqrt(dt)·driver.

Args: craft — owning Craft. The bubble follows the craft's WorldFrame position symbolically (read from the active trace's bindings). radius — bubble radius in meters; membership is 1 well inside. sigma — RW drift density σ/√Hz for the wind. Larger ⇒ EKF expects more drift in the wind estimate. boundary — width (m) of the smooth membership shell at the radius. 0 (default) is a hard cutoff; >0 gives a C¹ ramp (healthier Jacobians for a craft crossing it).

Source code in manta/fields/wind_bubble.py
def __init__(self,
             craft,
             radius: float = 20.0,
             sigma: float = 1e-3, *,
             boundary: float = 0.0,
             name: str | None = None,
             combining: str | None = None) -> None:
    super().__init__(name=name or f"{craft.name}_wind",
                     combining=combining,
                     wind_sigma=sigma)
    self.craft    = craft
    self.radius   = float(radius)
    self.boundary = float(boundary)

Magnetic

manta.fields.MagField

MagField(B=None)

Bases: SuperposedField

Magnetic flux density field, Vec3[WorldFrame] in Tesla.

The add_uniform builder returns self for chaining; other disturbances attach via field.add(DipoleMag(...)). The constructor accepts B=(bx,by,bz) for the common single-uniform case — equivalent to one add_uniform.

Source code in manta/fields/mag.py
def __init__(self, B: tuple[float, float, float] | None = None) -> None:
    super().__init__()
    if B is not None:
        self.add_uniform(B)

add_uniform

add_uniform(B_vec)

Attach a position-independent magnetic field. Returns self.

Source code in manta/fields/mag.py
def add_uniform(self, B_vec: tuple[float, float, float]) -> "MagField":
    """Attach a position-independent magnetic field. Returns self."""
    return self.add(UniformMag(B_vec))

manta.fields.UniformMag

UniformMag(B_vec, *, name=None)

Bases: Disturbance

Position-independent magnetic field.

For a quick first-order Earth-field model, common values (in Tesla): * Equator ≈ 30 µT — (3e-5, 0, 0) horizontal * Mid-lat ≈ 50 µT — (2e-5, 0, -4.5e-5) inclined * Pole ≈ 60 µT — (0, 0, -6e-5)

Source code in manta/fields/mag.py
def __init__(self, B_vec: tuple[float, float, float], *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.B_vec = tuple(float(x) for x in B_vec)
    if len(self.B_vec) != 3:
        raise ValueError(
            f"UniformMag: B_vec must be length-3, got {B_vec!r}")

manta.fields.DipoleMag

DipoleMag(position, moment, eps=0.001, *, name=None)

Bases: Disturbance

Point magnetic dipole.

B(p) = μ₀/(4π) · [3(m·r̂)·r̂ − m] / r³

where r = p − r_src, r̂ = r/|r|.

Args: position — (x, y, z) dipole position in WorldFrame, m. moment — (mx, my, mz) magnetic dipole moment in WorldFrame, A·m². For Earth this is ~8e22 (purely fictitious point-dipole approximation). eps — softening length (m) to avoid singularity at the dipole position. Default 1e-3.

Source code in manta/fields/mag.py
def __init__(self,
             position: tuple[float, float, float],
             moment: tuple[float, float, float],
             eps: float = 1e-3, *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.position = tuple(float(x) for x in position)
    self.moment   = tuple(float(x) for x in moment)
    self.eps      = float(eps)

manta.fields.BodyDipoleMag

BodyDipoleMag(craft, offset_body, moment_body, eps=0.001, *, name=None)

Bases: Disturbance

A magnetic dipole that RIDES a craft — the field a MagneticSource part emits to model the magnetic signature of motors, magnets, or magnetized structure on a moving vehicle. Same law as DipoleMag, but BOTH the dipole position and its moment vector are body-fixed: the position tracks the craft and the moment rotates with it (read from the active trace), so a magnetometer on another craft sees the field swing as the source turns.

Args: craft — the craft carrying the source. offset_body — dipole position in the craft body frame, m. moment_body — dipole moment in the craft body frame, A·m². eps — softening length, m. Default 1e-3.

Source code in manta/fields/mag.py
def __init__(self, craft, offset_body,
             moment_body: tuple[float, float, float],
             eps: float = 1e-3, *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.craft = craft
    self.offset_body = tuple(float(x) for x in offset_body)
    self.moment_body = tuple(float(x) for x in moment_body)
    if len(self.moment_body) != 3:
        raise ValueError(
            f"BodyDipoleMag: moment_body must be length-3, "
            f"got {moment_body!r}")
    self.eps = float(eps)

Collision

manta.fields.CollisionField

CollisionField()

Bases: SuperposedField

Outward-penetration vector field for contact detection.

Per the Field-base pattern, every registered Disturbance is an obstacle shape that contributes its own penetration vector when the query point is inside it. Multi-obstacle overlap composes additively.

Source code in manta/fields/base.py
def __init__(self) -> None:
    self._disturbances: list[Disturbance] = []

add_half_space

add_half_space(origin=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0))

Attach a half-space obstacle (infinite ground plane / wall). Returns self.

Source code in manta/fields/collision.py
def add_half_space(self,
                   origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
                   normal: tuple[float, float, float] = (0.0, 0.0, 1.0)
                   ) -> "CollisionField":
    """Attach a half-space obstacle (infinite ground plane / wall).
    Returns self."""
    return self.add(HalfSpace(origin=origin, normal=normal))

add_sphere

add_sphere(center, radius)

Attach a solid-sphere obstacle (e.g. a planet surface). Returns self.

Source code in manta/fields/collision.py
def add_sphere(self,
               center: tuple[float, float, float],
               radius: float) -> "CollisionField":
    """Attach a solid-sphere obstacle (e.g. a planet surface).
    Returns self."""
    return self.add(Sphere(center=center, radius=radius))

manta.fields.HalfSpace

HalfSpace(origin=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0), *, name=None)

Bases: Disturbance

Infinite half-space below a plane.

The plane is defined by an origin point on it and an outward normal. Points where (p − origin) · normal < 0 are inside the obstacle (below the plane); the outward direction is +normal.

Args: origin — point on the plane (world frame), m. normal — outward unit normal (world frame). For a ground plane at z=0 with air above and solid below: origin=(0,0,0), normal=(0,0,1).

Source code in manta/fields/collision.py
def __init__(self,
             origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
             normal: tuple[float, float, float] = (0.0, 0.0, 1.0),
             *, name: str | None = None) -> None:
    from ..parts._declarations import unit_axis
    super().__init__(name=name)
    self.origin = tuple(float(x) for x in origin)
    if len(self.origin) != 3:
        raise ValueError(
            f"HalfSpace: origin must be length-3; got {origin!r}")
    # The penetration math assumes a unit normal (a non-unit one
    # would scale the response by |normal|²) — normalize at
    # construction, same convention as part axes.
    self.normal = unit_axis(normal, who="HalfSpace", what="normal")

manta.fields.Sphere

Sphere(center, radius, *, name=None)

Bases: Disturbance

Solid sphere obstacle — e.g. a whole planet's surface.

Points with |p − center| < radius are inside; the outward direction is the local radial, so a craft standing anywhere on the sphere gets an up-is-outward contact normal — no per-site ground plane needed.

Args: center — sphere centre (world frame), m. radius — sphere radius, m.

Source code in manta/fields/collision.py
def __init__(self,
             center: tuple[float, float, float],
             radius: float,
             *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.center = tuple(float(x) for x in center)
    self.radius = float(radius)
    if len(self.center) != 3:
        raise ValueError(
            f"Sphere: center must be length-3; got {center!r}")
    if self.radius <= 0.0:
        raise ValueError(f"Sphere: radius must be > 0; got {radius!r}")

Optical

manta.fields.OpticalField

OpticalField()

Bases: Field

A scene of semantic ellipsoids. Carries (does not sum) its disturbances; the camera part enumerates ellipsoids and projects each quadric to an image-frame bounding box.

Source code in manta/fields/base.py
def __init__(self) -> None:
    self._disturbances: list[Disturbance] = []

ellipsoids property

ellipsoids

Every registered ellipsoid (scenery + body-anchored).

add_ellipsoid

add_ellipsoid(center, semi_axes, *, orientation=(1.0, 0.0, 0.0, 0.0), label=0)

Attach a fixed scenery ellipsoid. Returns self for chaining.

Source code in manta/fields/optical.py
def add_ellipsoid(self, center, semi_axes, *,
                  orientation=(1.0, 0.0, 0.0, 0.0),
                  label: int = 0) -> "OpticalField":
    """Attach a fixed scenery ellipsoid. Returns self for chaining."""
    return self.add(SemanticEllipsoid(
        center, semi_axes, orientation=orientation, label=label))

manta.fields.SemanticEllipsoid

SemanticEllipsoid(center, semi_axes, *, orientation=(1.0, 0.0, 0.0, 0.0), label=0, name=None)

Bases: _EllipsoidBase

A fixed semantic ellipsoid in the world (a landmark / scenery).

Args: center — (x, y, z) world position of the ellipsoid center, m. semi_axes — (a, b, c) half-extents along the ellipsoid's own axes, m. orientation— wxyz quaternion (world ← ellipsoid). Default upright. label — integer class id the camera reports with the box.

Source code in manta/fields/optical.py
def __init__(self, center, semi_axes, *,
             orientation=(1.0, 0.0, 0.0, 0.0),
             label: int = 0, name: str | None = None) -> None:
    super().__init__(name=name)
    self.center = tuple(float(x) for x in center)
    self.semi_axes = tuple(float(x) for x in semi_axes)
    self._Lambda = _diag_shape_mx(self.semi_axes)
    q = ca.DM([float(x) for x in orientation])
    self._R = quat_to_rotmat(q / ca.norm_2(q))       # world ← body
    self.label = int(label)

manta.fields.BodySemanticEllipsoid

BodySemanticEllipsoid(craft, offset_body, semi_axes, *, label=0, name=None)

Bases: _EllipsoidBase

A semantic ellipsoid that rides a craft (emitted by an OpticalSource). Center and orientation track the carrying craft's pose, read from the active trace; the semi-axes are the body extent.

Source code in manta/fields/optical.py
def __init__(self, craft, offset_body, semi_axes, *,
             label: int = 0, name: str | None = None) -> None:
    super().__init__(name=name)
    self.source_craft = craft
    self.offset_body = tuple(float(x) for x in offset_body)
    self.semi_axes = tuple(float(x) for x in semi_axes)
    self._Lambda = _diag_shape_mx(self.semi_axes)
    self.label = int(label)