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, **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. See Field.value_at_sym for the per-stage rule. One of: "additive" (default) — straight linear sum. "averaged" — arithmetic-mean stage; the running additive total + every averaged contribution are averaged together. "projected" — only the component of this contribution that's orthogonal to (or extends) the running sum is kept. Order-dependent across multiple projected entries; the canonical order is insertion.

Source code in manta/fields/base.py
def __init__(self, name: str | None = None, *,
             combining: str | None = 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", "projected"):
            raise ValueError(
                f"Disturbance: combining must be 'additive', "
                f"'averaged', or 'projected'; got {combining!r}")
        self.combining = combining
    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

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))

Bases: SuperposedField

Fluid density + bulk velocity over the world frame.

The field value at a point is a FluidState. Concrete sources (uniform background, localized currents, planet-registered ocean + atmosphere) are added as Disturbance subclasses to one FluidField instance.

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

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)
             ) -> None:
    super().__init__()
    if density is not None:
        self.add_uniform(density, velocity)

add_uniform

add_uniform(density, velocity=(0.0, 0.0, 0.0))

Attach a uniform density (+ optional flow). 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)
                ) -> "FluidField":
    """Attach a uniform density (+ optional flow). Returns self."""
    return self.add(UniformFluid(density, velocity))

manta.fields.FluidState dataclass

FluidState(density, velocity)

Local fluid properties at an world-frame point.

density — kg/m³. CasADi-MX scalar (so it composes with symbolic state in tracing). velocity — bulk fluid velocity at the point, Vec3[WorldFrame].

Disturbances and FluidField.value_at_sym return / consume this type. Per-component addition is defined so the Field base class can sum disturbances without special-casing this field.

manta.fields.UniformFluid

UniformFluid(density, velocity=(0.0, 0.0, 0.0), *, name=None)

Bases: Disturbance

Position-independent fluid: constant density + (optional) flow.

Args: density — kg/m³. Common values: ~1.225 (air), ~1025 (seawater), ~1000 (fresh water). velocity — bulk flow vector in WorldFrame, m/s. Default zero.

Source code in manta/fields/fluid.py
def __init__(self,
             density: float,
             velocity: tuple[float, float, float] = (0.0, 0.0, 0.0), *, name: str | None = None) -> None:
    super().__init__(name=name)
    self.density  = float(density)
    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}")

manta.fields.CurrentFlow

CurrentFlow(velocity, *, name=None)

Bases: Disturbance

Localized current — adds a velocity contribution without changing density.

v1 ships the simplest non-spatial model: a constant velocity contribution everywhere. Future versions will accept a Gaussian envelope around a centroid, or a tabulated current map. For now CurrentFlow((0, 1, 0)) adds 1 m/s in +y to whatever density the background UniformFluid provides.

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) -> None:
    super().__init__(name=name)
    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.CraftWindBubble

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

Bases: Disturbance

A localized wind contribution anchored to a craft.

Density contribution is zero (wind doesn't change density). The velocity contribution equals the estimated wind vector inside the bubble (where |point - craft.position| < radius) and zero outside, gated by a hard ca.if_else.

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 inside contribute_at_sym from the active trace's bindings). radius — bubble radius in meters. Outside this, the contribution is exactly zero. sigma — RW drift density σ/√Hz for the wind. Larger ⇒ EKF expects more drift in the wind estimate.

The default combining="averaged" matches the design intent: overlapping bubbles compromise on the mean of their wind estimates.

Source code in manta/fields/wind_bubble.py
def __init__(self,
             craft,
             radius: float = 20.0,
             sigma: float = 1e-3, *,
             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)

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)