Skip to content

Planets

A Planet is a world-level entity: a planet-fixed frame, standing disturbances on the shared fields, and initial-state factories.

manta.Planet

Planet(name='planet', *, position=(0.0, 0.0, 0.0), rotation_axis=(0.0, 0.0, 1.0), omega=0.0)

Body-fixed rotating planet frame + field-disturbance source.

Args: name — identifier (used in repr + lookups). position — planet center in WorldFrame (m). Default origin. rotation_axis — unit rotation axis in WorldFrame. Default (0,0,1). omega — angular rate, rad/s. Positive ⇒ right-hand-rule rotation about rotation_axis. Earth sidereal is ~7.272e-5 rad/s; default 0 (non-rotating).

Source code in manta/planets/base.py
def __init__(self,
             name: str = "planet",
             *,
             position: tuple[float, float, float] = (0.0, 0.0, 0.0),
             rotation_axis: tuple[float, float, float] = (0.0, 0.0, 1.0),
             omega: float = 0.0) -> None:
    from ..ir.module import check_name
    self.name = check_name(str(name), who=type(self).__name__)
    pos = np.asarray(position, dtype=float)
    if pos.shape != (3,):
        raise ValueError(f"Planet: position must be length-3, got {position!r}")
    self.center = pos
    axis = np.asarray(rotation_axis, dtype=float)
    n = float(np.linalg.norm(axis))
    if n == 0.0:
        raise ValueError("Planet: rotation_axis must be nonzero.")
    self.axis = axis / n
    self.omega = float(omega)

R_world_from_planet

R_world_from_planet(t)

3×3 rotation matrix from PlanetFrame to WorldFrame at time t.

Source code in manta/planets/base.py
def R_world_from_planet(self, t: float) -> np.ndarray:
    """3×3 rotation matrix from PlanetFrame to WorldFrame at time t."""
    theta = self.omega * float(t)
    c = np.cos(theta)
    s = np.sin(theta)
    ux, uy, uz = self.axis
    K = np.array([[0.0, -uz,  uy],
                  [ uz, 0.0, -ux],
                  [-uy,  ux, 0.0]], dtype=float)
    return np.eye(3) + s * K + (1.0 - c) * (K @ K)

omega_vec_world

omega_vec_world()

Constant angular-velocity 3-vector in WorldFrame coords.

Source code in manta/planets/base.py
def omega_vec_world(self) -> np.ndarray:
    """Constant angular-velocity 3-vector in WorldFrame coords."""
    return self.omega * self.axis

planet_to_world

planet_to_world(p_planet, v_planet, t)

Position + velocity of a point that, in PlanetFrame at time t, has coords (p_planet, v_planet). Returns (p_world, v_world).

Velocity transform: v_world = R · v_planet + ω × (p_world − planet.position)

Source code in manta/planets/base.py
def planet_to_world(self,
                    p_planet: tuple[float, float, float],
                    v_planet: tuple[float, float, float],
                    t: float
                    ) -> tuple[np.ndarray, np.ndarray]:
    """Position + velocity of a point that, in PlanetFrame at time
    `t`, has coords (p_planet, v_planet). Returns (p_world, v_world).

    Velocity transform:
        v_world = R · v_planet + ω × (p_world − planet.position)
    """
    R = self.R_world_from_planet(t)
    p_planet_arr = np.asarray(p_planet, dtype=float)
    p_world = R @ p_planet_arr + self.center
    omega_w = self.omega_vec_world()
    v_world = (R @ np.asarray(v_planet, dtype=float)
               + np.cross(omega_w, p_world - self.center))
    return p_world, v_world

position_world_sym

position_world_sym()

3×1 MX of the planet's center in WorldFrame (constant).

Source code in manta/planets/base.py
def position_world_sym(self) -> ca.MX:
    """3×1 MX of the planet's center in WorldFrame (constant)."""
    return ca.DM(self.center.reshape(3, 1))

omega_world_sym

omega_world_sym()

3×1 MX of the angular-velocity vector in WorldFrame (constant).

Source code in manta/planets/base.py
def omega_world_sym(self) -> ca.MX:
    """3×1 MX of the angular-velocity vector in WorldFrame (constant)."""
    return ca.DM((self.omega * self.axis).reshape(3, 1))

R_world_from_planet_sym

R_world_from_planet_sym(t_sym)

3×3 MX rotation from PlanetFrame to WorldFrame at symbolic t.

Rodrigues' formula with angle = omega·t. Branch-free.

Source code in manta/planets/base.py
def R_world_from_planet_sym(self, t_sym) -> ca.MX:
    """3×3 MX rotation from PlanetFrame to WorldFrame at symbolic t.

    Rodrigues' formula with angle = omega·t. Branch-free.
    """
    t_mx = t_sym._mx if hasattr(t_sym, "_mx") else t_sym
    theta = self.omega * t_mx
    c = ca.cos(theta)
    s = ca.sin(theta)
    ux, uy, uz = float(self.axis[0]), float(self.axis[1]), float(self.axis[2])
    K = ca.DM(np.array([[0.0, -uz,  uy],
                        [ uz, 0.0, -ux],
                        [-uy,  ux, 0.0]], dtype=float))
    I = ca.DM.eye(3)
    return I + s * K + (1.0 - c) * (K @ K)

world_to_planet_sym

world_to_planet_sym(p_world, v_world, t)

Symbolic Cartesian position/velocity in this planet's frame.

p_world and v_world must be Vec3[WorldFrame] values. The returned values are Vec3[PlanetFrame]. The method mirrors :meth:world_to_planet exactly and intentionally contains no geodetic conversion.

Source code in manta/planets/base.py
def world_to_planet_sym(self, p_world, v_world, t):
    """Symbolic Cartesian position/velocity in this planet's frame.

    ``p_world`` and ``v_world`` must be ``Vec3[WorldFrame]`` values.
    The returned values are ``Vec3[PlanetFrame]``.  The method mirrors
    :meth:`world_to_planet` exactly and intentionally contains no
    geodetic conversion.
    """
    from ..ir.frames import PlanetFrame, WorldFrame
    from ..ir.types import Vec3

    p_world = Vec3[WorldFrame].coerce(p_world)
    v_world = Vec3[WorldFrame].coerce(v_world)
    center = Vec3[WorldFrame].constant(tuple(float(x) for x in self.center))
    offset_world = p_world - center
    R_pw = ca.transpose(self.R_world_from_planet_sym(t))
    omega_world = Vec3[WorldFrame].constant(
        tuple(float(x) for x in self.omega_vec_world())
    )
    p_planet = Vec3[PlanetFrame].from_mx(R_pw @ offset_world._mx)
    v_planet = Vec3[PlanetFrame].from_mx(
        R_pw @ (v_world - omega_world.cross(offset_world))._mx
    )
    return p_planet, v_planet

planet_to_world_sym

planet_to_world_sym(p_planet, v_planet, t)

Symbolic inverse of :meth:world_to_planet_sym, Cartesian only.

Source code in manta/planets/base.py
def planet_to_world_sym(self, p_planet, v_planet, t):
    """Symbolic inverse of :meth:`world_to_planet_sym`, Cartesian only."""
    from ..ir.frames import PlanetFrame, WorldFrame
    from ..ir.types import Vec3

    # Coercion performs the frame check even though the expressions are
    # already symbolic IR values in the normal call path.
    p_planet = Vec3[PlanetFrame].coerce(p_planet)
    v_planet = Vec3[PlanetFrame].coerce(v_planet)
    R_wp = self.R_world_from_planet_sym(t)
    center = Vec3[WorldFrame].constant(tuple(float(x) for x in self.center))
    offset_world = Vec3[WorldFrame].from_mx(R_wp @ p_planet._mx)
    omega_world = Vec3[WorldFrame].constant(
        tuple(float(x) for x in self.omega_vec_world())
    )
    p_world = center + offset_world
    v_world = (
        Vec3[WorldFrame].from_mx(R_wp @ v_planet._mx)
        + omega_world.cross(offset_world)
    )
    return p_world, v_world

position

position(x, y, z)

Return a PlanetState wrapping a PlanetFrame position. Pass directly to World.add_craft(..., position=...) to seed the craft's initial WorldFrame position from PlanetFrame coords.

Source code in manta/planets/base.py
def position(self,
             x: float, y: float, z: float) -> PlanetState:
    """Return a `PlanetState` wrapping a PlanetFrame position. Pass
    directly to `World.add_craft(..., position=...)` to seed the
    craft's initial WorldFrame position from PlanetFrame coords."""
    from .state import PlanetState
    return PlanetState(self, "position", (float(x), float(y), float(z)))

velocity

velocity(vx, vy, vz)

Return a PlanetState wrapping a PlanetFrame velocity.

Source code in manta/planets/base.py
def velocity(self,
             vx: float, vy: float, vz: float) -> PlanetState:
    """Return a `PlanetState` wrapping a PlanetFrame velocity."""
    from .state import PlanetState
    return PlanetState(self, "velocity",
                       (float(vx), float(vy), float(vz)))

local_tangent_basis

local_tangent_basis(position)

Local East/North/Up unit vectors (WorldFrame) at a WorldFrame point — a purely Cartesian local-tangent frame, no lon needed.

Up comes from :meth:surface_normal, which is radial for the generic Cartesian planet. North is the spin axis projected into that tangent plane and East = North × Up.

Where North is undefined — the planet isn't rotating, or the point sits on the spin axis — it falls back to a stable tangential reference (world +x, else +y), so the basis is always well-formed (only its azimuth is then arbitrary). Returns (east, north, up).

Source code in manta/planets/base.py
def local_tangent_basis(self,
                        position: tuple[float, float, float]
                        ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Local East/North/Up unit vectors (WorldFrame) at a WorldFrame
    point — a purely Cartesian local-tangent frame, no lon needed.

    ``Up`` comes from :meth:`surface_normal`, which is radial for the
    generic Cartesian planet. ``North`` is the spin axis projected into
    that tangent plane and ``East = North × Up``.

    Where North is undefined — the planet isn't rotating, or the
    point sits on the spin axis — it falls back to a stable
    tangential reference (world +x, else +y), so the basis is always
    well-formed (only its azimuth is then arbitrary). Returns
    `(east, north, up)`.
    """
    up = self.surface_normal(position)
    north = self.axis - float(np.dot(self.axis, up)) * up
    nn = float(np.linalg.norm(north))
    if nn < 1e-9:
        for ref in (np.array([1.0, 0.0, 0.0]), np.array([0.0, 1.0, 0.0])):
            north = ref - float(np.dot(ref, up)) * up
            nn = float(np.linalg.norm(north))
            if nn > 1e-9:
                break
    north = north / nn
    east = np.cross(north, up)
    return east, north, up

surface_normal

surface_normal(position)

Cartesian outward normal used to orient a local Scene.

The base planet has no reference ellipsoid or geodesy contract, so its only meaningful convention is radial. Concrete planets can override this using the same Cartesian geometry as their fields.

Source code in manta/planets/base.py
def surface_normal(
    self, position: tuple[float, float, float]
) -> np.ndarray:
    """Cartesian outward normal used to orient a local ``Scene``.

    The base planet has no reference ellipsoid or geodesy contract, so
    its only meaningful convention is radial. Concrete planets can
    override this using the same Cartesian geometry as their fields.
    """
    r_world = np.asarray(position, dtype=float) - self.center
    norm = float(np.linalg.norm(r_world))
    if norm == 0.0:
        raise ValueError(
            f"{type(self).__name__}.surface_normal: undefined at "
            "the planet centre"
        )
    return r_world / norm

local_tangent_orientation

local_tangent_orientation(position, heading=0.0)

World-from-craft quaternion (w, x, y, z) placing the craft in the local-tangent frame at WorldFrame point position: body forward (+x) along North, up (+z) along the Cartesian surface normal, yawed by heading (radians, right-handed about Up — 0 faces North).

Cartesian and general: 'North' is the spin-axis tangential projection (see local_tangent_basis).

Source code in manta/planets/base.py
def local_tangent_orientation(self,
                              position: tuple[float, float, float],
                              heading: float = 0.0) -> tuple:
    """World-from-craft quaternion `(w, x, y, z)` placing the craft in
    the local-tangent frame at WorldFrame point `position`: body
    forward (+x) along North, up (+z) along the Cartesian surface normal, yawed
    by `heading` (radians, right-handed about Up — 0 faces North).

    Cartesian and general: 'North' is the spin-axis tangential
    projection (see `local_tangent_basis`)."""
    from ..ir._rotation import quat_from_rotmat_np
    R_wc = self._local_tangent_rotmat(position, float(heading))
    return tuple(float(v) for v in quat_from_rotmat_np(R_wc))

scene_at

scene_at(position, *, heading=0.0)

A local Scene anchored at PlanetFrame point position — a ground patch with a human-friendly East/North/Up frame, used to place craft and to translate poses/state for reporting + rendering.

position is in the planet-fixed frame (origin at the planet centre), so a point on the surface is a planet-radius vector — with the planet left at the world origin you place a craft anywhere on it. The scene's axes are the local tangent frame there (+z surface normal, +x north), optionally yawed by heading (radians) about up. See Scene for the full API (at_rest, relative, world_pose).

Source code in manta/planets/base.py
def scene_at(self,
             position: tuple[float, float, float],
             *,
             heading: float = 0.0) -> Scene:
    """A local **`Scene`** anchored at PlanetFrame point `position` — a
    ground patch with a human-friendly East/North/Up frame, used to
    place craft and to translate poses/state for reporting + rendering.

    `position` is in the planet-fixed frame (origin at the planet
    centre), so a point on the surface is a planet-radius vector — with
    the planet left at the world origin you place a craft anywhere on
    it. The scene's axes are the local tangent frame there (+z surface
    normal, +x north),
    optionally yawed by `heading` (radians) about up. See `Scene` for
    the full API (`at_rest`, `relative`, `world_pose`).
    """
    from .scene import Scene
    return Scene(self, position, heading=heading)

register_disturbances

register_disturbances(world)

Called by Sim(world) to attach this planet's standing contributions to the world's shared fields. A planet is the world's gravity declaration: an override must register the GravityField (world.get_or_create_field(GravityField)) even when it adds no gravity source, or the world refuses to resolve. Subclasses (Earth, Moon, ...) override to install gravity / ocean / atmosphere / magnetic-dipole disturbances. Base default: register the (empty) GravityField and nothing else — a bare Planet is a deliberate zero-gravity frame, not an undeclared one.

Subclasses should use world.get_or_create_field(FieldClass) to get the shared instance, then .add(disturbance).

Source code in manta/planets/base.py
def register_disturbances(self, world: World) -> None:
    """Called by `Sim(world)` to attach this planet's standing
    contributions to the world's shared fields. A planet is the
    world's gravity declaration: an override must register the
    GravityField (`world.get_or_create_field(GravityField)`) even when
    it adds no gravity source, or the world refuses to resolve.
    Subclasses (Earth,
    Moon, ...) override to install gravity / ocean / atmosphere /
    magnetic-dipole disturbances. Base default: register the (empty)
    GravityField and nothing else — a bare `Planet` is a deliberate
    zero-gravity frame, not an undeclared one.

    Subclasses should use `world.get_or_create_field(FieldClass)` to
    get the shared instance, then `.add(disturbance)`.
    """
    from ..fields import GravityField
    world.get_or_create_field(GravityField)

manta.planets.Earth

Earth(name='earth', *, position=(0.0, 0.0, 0.0), rotation_rate=None, rotation_axis=(0.0, 0.0, 1.0), flattening=FLATTENING, sea_level=0.0, water_density=1025.0, ocean_current=(0.0, 0.0, 0.0), air_density=1.225, sea_level_temperature=T0_ISA, lapse_rate=LAPSE_ISA, gravity_mu=MU, include_j2=None, dipole_moment=0.0, waves=None, surface_collision=True, surface_smoothing=0.0)

Bases: Planet

Standard Earth preset.

Args: name — identifier. Default "earth". position — planet center in WorldFrame (m). rotation_rate — angular rate, rad/s. Default: Earth's true sidereal rate (Earth.SIDEREAL). Pass 0.0 for a non-rotating Earth. Most users never set this — place craft with earth.scene_at(...) instead. flattening — of the reference ellipsoid. Default WGS-84 (Earth.FLATTENING); 0 gives a sphere of radius R_EQ. sea_level — normal offset of the ocean's top above the reference ellipsoid, m. Default 0 (the sea surface IS the ellipsoid, as for a WGS-84 altitude with no geoid model). water_density — ocean density, kg/m³. Default 1025 (seawater). air_density — atmosphere density at sea level, kg/m³. Default 1.225 (ISA). Sets the sea-level pressure via the ideal-gas law P0 = ρ0·R·T0; aloft the air follows the ISA troposphere (lapse + ideal gas), so density is no longer a pure exponential. sea_level_temperature — ISA sea-level temperature T0, K. Default 288.15. Drops with altitude at lapse_rate. lapse_rate — ISA troposphere temperature lapse, K/m. Default 6.5e-3. gravity_mu — gravitational parameter μ (m³/s²). 0 disables gravity. Default Earth.MU. include_j2 — register the J2 oblateness perturbation alongside the point-mass term. Default None → on whenever flattening > 0, off for a sphere. Point mass + J2 + the centrifugal term of the spinning frame make the ellipsoid an equipotential to O(f²), so gravity is normal to the sea surface (residual tangential acceleration < 1e-4 m/s²); with a point mass alone a craft at rest on the ellipsoid would feel a ~1.7e-2 m/s² pull toward the equator. An explicit False is honoured (physically inconsistent on an oblate Earth — for isolated gravity tests only). dipole_moment — magnetic dipole strength, A·m². 0 disables magnetic. Default 0. waves — optional SeaWaves: a sinusoidal moving sea surface (boundary elevation + underwater orbital velocity). Default None (flat sea). surface_collision — register the sea surface (the ellipsoid raised by sea_level) as a solid CollisionField obstacle (a rough model of the surface), so Collider-footed craft can stand anywhere on the planet without a per-site ground plane. Default True. surface_smoothing — m. Blend the water/air switch over this length (a C¹ Hermite step in altitude) instead of a hard if_else. Physically: a finite-size volume element crosses the surface over its own diameter; numerically it turns point- sampled buoyancy from bang-bang into a smooth ramp (a floating hull finds a stable draft, a surface-piercing foil gets a smooth lift-vs- height slope). Default 0 (hard boundary).

Source code in manta/planets/earth.py
def __init__(self,
             name: str = "earth",
             *,
             position: tuple[float, float, float] = (0.0, 0.0, 0.0),
             rotation_rate: float | None = None,
             rotation_axis: tuple[float, float, float] = (0.0, 0.0, 1.0),
             flattening: float = FLATTENING,
             sea_level: float = 0.0,
             water_density: float = 1025.0,
             ocean_current: tuple[float, float, float] = (0.0, 0.0, 0.0),
             air_density: float = 1.225,
             sea_level_temperature: float = T0_ISA,
             lapse_rate: float = LAPSE_ISA,
             gravity_mu: float = MU,
             include_j2: bool | None = None,
             dipole_moment: float = 0.0,
             waves: SeaWaves | None = None,
             surface_collision: bool = True,
             surface_smoothing: float = 0.0) -> None:
    # rotation_axis lets a LOCAL-tangent sim sit at a latitude: tilt the
    # spin axis off local-up so the inertial Earth rate the IMU senses has
    # a horizontal (north) component — the gyrocompass signal. Default +z
    # (sub at the pole / spin axis = local vertical).
    # Default to the true sidereal rate: a realistic Earth out of the
    # box. `rotation_rate=0.0` explicitly opts into a non-rotating one.
    omega = self.SIDEREAL if rotation_rate is None else float(rotation_rate)
    super().__init__(name=name,
                     position=position,
                     rotation_axis=rotation_axis,
                     omega=omega)
    self.flattening = float(flattening)
    if not 0.0 <= self.flattening < 1.0:
        raise ValueError(
            f"Earth.flattening must be within [0, 1), got {flattening!r}"
        )
    self.sea_level     = float(sea_level)
    self.water_density = float(water_density)
    self.ocean_current = tuple(float(v) for v in ocean_current)
    self.air_density   = float(air_density)
    self.sea_level_temperature = float(sea_level_temperature)
    self.lapse_rate    = float(lapse_rate)
    self.gravity_mu    = float(gravity_mu)
    # J2 is what makes gravity normal to an oblate sea surface (see
    # the class docstring); a sphere has no bulge to account for.
    self.include_j2    = (self.flattening > 0.0 if include_j2 is None
                          else bool(include_j2))
    self.dipole_moment = float(dipole_moment)
    self.waves         = waves
    self.surface_collision = bool(surface_collision)
    self.surface_smoothing = float(surface_smoothing)

planet_radius property

planet_radius

Equatorial radius of the sea surface, R_EQ + sea_level (m).

Also the radius that sets the surface gravity g0 = μ / R² the hydrostatic and barometric columns use — a single value for the whole planet, so those columns carry the equatorial g0 at every latitude (the pole is 0.5% stronger). The dynamics use the real gravity field; only the fluid pressure profiles take this shortcut.

sea_surface

sea_surface()

The mean sea surface as a solid Ellipsoid — the reference ellipsoid raised by sea_level, in WorldFrame about the planet centre. Its signed_height_sym is the signed height every Earth field is built on.

Source code in manta/planets/earth.py
def sea_surface(self) -> Ellipsoid:
    """The mean sea surface as a solid `Ellipsoid` — the reference
    ellipsoid raised by `sea_level`, in WorldFrame about the planet
    centre. Its `signed_height_sym` is the signed height every
    Earth field is built on."""
    return Ellipsoid(center=tuple(self.center.tolist()),
                     equatorial_radius=self.R_EQ,
                     flattening=self.flattening,
                     polar_axis=tuple(self.axis.tolist()),
                     height=self.sea_level,
                     name=f"{self.name}_surface")

surface_normal

surface_normal(position)

Outward normal of Earth's Cartesian reference ellipsoid.

Source code in manta/planets/earth.py
def surface_normal(
    self, position: tuple[float, float, float]
) -> np.ndarray:
    """Outward normal of Earth's Cartesian reference ellipsoid."""
    offset = np.asarray(position, dtype=float) - self.center
    _height, normal = self.sea_surface().signed_height(offset)
    return normal

manta.planets.Scene

Scene(planet, position, *, heading=0.0)

A local East/North/Up frame fixed in a planet's body frame.

Construct via planet.scene_at(position, heading=...) rather than directly. position is the anchor point in PlanetFrame (typically a point on the surface); the scene's axes are the local tangent frame there — +z along the planet's Cartesian surface normal (radial for the base planet), +x north (the planet spin axis projected into the tangent plane), +y = up×north — optionally yawed by heading (radians) about up.

Source code in manta/planets/scene.py
def __init__(self,
             planet,
             position: tuple[float, float, float],
             *,
             heading: float = 0.0) -> None:
    self.planet = planet
    self.anchor_planet = np.asarray(position, dtype=float)
    if self.anchor_planet.shape != (3,):
        raise ValueError(
            f"Scene: position must be length-3, got {position!r}")
    self.heading = float(heading)
    # The scene is fixed in PlanetFrame. At t=0 PlanetFrame and
    # WorldFrame share orientation (R_world_from_planet(0) = I), so the
    # tangent basis computed at the world point `anchor + centre` *is*
    # R_planet_from_scene — a constant we cache once.
    p_world0 = tuple(self.anchor_planet + planet.center)
    self.R_planet_from_scene = planet._local_tangent_rotmat(
        p_world0, self.heading)

R_world_from_scene

R_world_from_scene(t=0.0)

3×3 rotation from the scene frame to WorldFrame at time t.

Source code in manta/planets/scene.py
def R_world_from_scene(self, t: float = 0.0) -> np.ndarray:
    """3×3 rotation from the scene frame to WorldFrame at time `t`."""
    return self.planet.R_world_from_planet(t) @ self.R_planet_from_scene

origin_world

origin_world(t=0.0)

The scene origin's WorldFrame position at time t.

Source code in manta/planets/scene.py
def origin_world(self, t: float = 0.0) -> np.ndarray:
    """The scene origin's WorldFrame position at time `t`."""
    return (self.planet.R_world_from_planet(t) @ self.anchor_planet
            + self.planet.center)

world_pose

world_pose(t=0.0)

(origin_world, quat_world_from_scene) at time t — the scene's own pose, to publish as a parent/anchor entity for rendering.

Source code in manta/planets/scene.py
def world_pose(self, t: float = 0.0) -> tuple[tuple, tuple]:
    """`(origin_world, quat_world_from_scene)` at time `t` — the scene's
    own pose, to publish as a parent/anchor entity for rendering."""
    q = quat_from_rotmat_np(self.R_world_from_scene(t))
    return (tuple(float(v) for v in self.origin_world(t)),
            tuple(float(v) for v in q))

at_rest

at_rest(position=(0.0, 0.0, 0.0), *, heading=0.0)

Initial state for a craft at rest in the scene (co-rotating with the planet) at scene-frame coordinate position, yawed heading (radians) about local up from the scene's north.

Returns a kwargs dict (position, velocity, orientation, angular_velocity, all WorldFrame) to splat into World.add_craft::

w.add_craft(sub,  **scene.at_rest((0, 0, -0.2)))       # 0.2 m down
w.add_craft(buoy, **scene.at_rest(heading=np.radians(35)))

The orbital velocity ω × r and body spin rate R_craftᵀ·ω are filled in so the craft is genuinely fixed to the planet (a gyro reads the planet's spin; it does not drift through the co-rotating sea/air).

Source code in manta/planets/scene.py
def at_rest(self,
            position: tuple[float, float, float] = (0.0, 0.0, 0.0),
            *,
            heading: float = 0.0) -> dict:
    """Initial state for a craft **at rest in the scene** (co-rotating
    with the planet) at scene-frame coordinate `position`, yawed
    `heading` (radians) about local up from the scene's north.

    Returns a kwargs dict (`position`, `velocity`, `orientation`,
    `angular_velocity`, all WorldFrame) to splat into
    `World.add_craft`::

        w.add_craft(sub,  **scene.at_rest((0, 0, -0.2)))       # 0.2 m down
        w.add_craft(buoy, **scene.at_rest(heading=np.radians(35)))

    The orbital velocity `ω × r` and body spin rate `R_craftᵀ·ω` are
    filled in so the craft is genuinely fixed to the planet (a gyro
    reads the planet's spin; it does not drift through the co-rotating
    sea/air).
    """
    R_ps = self.R_planet_from_scene              # = R_world_from_scene(0)
    p_world = self.origin_world(0.0) + R_ps @ np.asarray(position, float)
    r_world = p_world - self.planet.center
    omega_w = self.planet.omega_vec_world()
    v_world = np.cross(omega_w, r_world)
    c, s = np.cos(heading), np.sin(heading)
    Rz = np.array([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]])
    R_wc = R_ps @ Rz
    orientation = quat_from_rotmat_np(R_wc)
    omega_body = R_wc.T @ omega_w
    return {
        "position":         tuple(float(v) for v in p_world),
        "velocity":         tuple(float(v) for v in v_world),
        "orientation":      tuple(float(v) for v in orientation),
        "angular_velocity": tuple(float(v) for v in omega_body),
    }

relative

relative(state, t=0.0)

Re-express a craft's WorldFrame state in the scene frame.

state is a per-craft state dict (as from sim.state[name] or ekf.state_dict()[name]): position/velocity (WorldFrame), orientation (world-from-craft quaternion), angular_velocity (body rates). Returns a dict of the same shape with:

  • position — in scene coordinates,
  • orientation — scene-from-craft quaternion,
  • velocity — velocity relative to the co-rotating scene (i.e. relative to the ground), in scene coords,
  • angular_velocity — body rate relative to the scene's spin (zero for a craft sitting still on the ground).

Any other keys (part states like joint angles) pass through unchanged. t is the sim time the state was sampled at (needed for a spinning planet; default 0).

Source code in manta/planets/scene.py
def relative(self, state: dict, t: float = 0.0) -> dict:
    """Re-express a craft's WorldFrame `state` in the scene frame.

    `state` is a per-craft state dict (as from `sim.state[name]` or
    `ekf.state_dict()[name]`): `position`/`velocity` (WorldFrame),
    `orientation` (world-from-craft quaternion), `angular_velocity`
    (body rates). Returns a dict of the same shape with:

      * `position`    — in scene coordinates,
      * `orientation` — scene-from-craft quaternion,
      * `velocity`    — velocity **relative to the co-rotating scene**
                        (i.e. relative to the ground), in scene coords,
      * `angular_velocity` — body rate **relative to the scene's spin**
                        (zero for a craft sitting still on the ground).

    Any other keys (part states like joint angles) pass through
    unchanged. `t` is the sim time the state was sampled at (needed for
    a spinning planet; default 0).
    """
    R_ws = self.R_world_from_scene(t)
    R_sw = R_ws.T
    o = self.origin_world(t)
    omega_w = self.planet.omega_vec_world()

    out = dict(state)
    p = np.asarray(state["position"], dtype=float).ravel()
    out["position"] = tuple(float(v) for v in (R_sw @ (p - o)))

    q_wc = None
    if "orientation" in state:
        q_wc = np.asarray(state["orientation"], dtype=float).ravel()
        q_ws = quat_from_rotmat_np(R_ws)
        q_sc = quat_mul_np(quat_conj_np(q_ws), q_wc)
        if q_sc[0] < 0.0:                 # canonicalise to w ≥ 0 (q ≡ −q)
            q_sc = -q_sc
        out["orientation"] = tuple(float(v) for v in q_sc)
    if "velocity" in state:
        v = np.asarray(state["velocity"], dtype=float).ravel()
        v_rel = v - np.cross(omega_w, p - self.planet.center)
        out["velocity"] = tuple(float(v_) for v_ in (R_sw @ v_rel))
    if "angular_velocity" in state and q_wc is not None:
        wb = np.asarray(state["angular_velocity"], dtype=float).ravel()
        R_wc = quat_to_rotmat_np(q_wc)
        out["angular_velocity"] = tuple(
            float(v_) for v_ in (wb - R_wc.T @ omega_w))
    return out

manta.planets.PlanetState

PlanetState(planet, kind, value)

Initial-state value carrying its PlanetFrame origin.

Resolved by World.add_craft at compile time via planet.planet_to_world(...).

Source code in manta/planets/state.py
def __init__(self,
             planet: Planet,
             kind: str,
             value: tuple[float, float, float]) -> None:
    if kind not in ("position", "velocity"):
        raise ValueError(
            f"PlanetState: kind must be 'position' or 'velocity', "
            f"got {kind!r}")
    from .base import Planet
    if not isinstance(planet, Planet):
        raise TypeError(
            f"PlanetState: planet must be a Planet, got "
            f"{type(planet).__name__}")
    if len(value) != 3:
        raise ValueError(
            f"PlanetState: value must be length-3, got {value!r}")
    self.planet = planet
    self.kind = kind
    self.value = tuple(
        finite_real(x, f"PlanetState.{kind}") for x in value)

manta.planets.SeaWaves dataclass

SeaWaves(amplitude, wavelength, direction=(1.0, 0.0, 0.0), speed=None)

Planar deep-water sinusoid riding a planet's sea surface.

The surface elevation (above the mean sea surface) is

η(p, t) = amplitude · cos(k·ξ − ω·t),   ξ = p_planet · direction

with k = 2π/wavelength and ω = k·c. The phase speed c defaults to the deep-water dispersion relation c = √(g·λ / 2π). Underwater, the fluid carries the matching first-order orbital velocity — particles circle with radius amplitude at the surface, decaying as e^{k·z} with depth — so drag surfaces and foils feel the moving water, not just the moving boundary. The pressure carries the matching depth-attenuated dynamic term ρ·g·η·e^{k·z} on top of the mean hydrostatic column, so a submerged pressure sensor sees the waves at the physically correct (depth-filtered) amplitude — the signal a wave-detecting barometer works with.

direction is a planet-frame vector (normalized; its vertical component at the point of interest should be ~0). The wave is a PLANAR field in planet coordinates — valid for a local patch of ocean, not a globe-wrapping solution.

Args: amplitude — m (crest height above mean sea level). wavelength — m (crest-to-crest). direction — planet-frame propagation direction. Default +x. speed — phase speed override, m/s. None → deep-water dispersion using the planet's surface gravity.