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.
    """
    theta = self.omega * t_sym
    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)

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

at_rest

at_rest()

Shorthand for planet.velocity(0, 0, 0) — sets the WorldFrame velocity such that the craft sits at rest in PlanetFrame (i.e., co-rotates with the planet).

Source code in manta/planets/base.py
def at_rest(self) -> "PlanetState":
    """Shorthand for `planet.velocity(0, 0, 0)` — sets the WorldFrame
    velocity such that the craft sits at rest in PlanetFrame
    (i.e., co-rotates with the planet)."""
    return self.velocity(0.0, 0.0, 0.0)

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 lat/lon.

Up is the local radial (from the planet centre out through the point). North is the planet's spin axis projected into the tangent plane and normalised — the same true-north direction a gyrocompass finds from the spin vector. 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 lat/lon.

    `Up` is the local radial (from the planet centre out through the
    point). `North` is the planet's spin axis projected into the
    tangent plane and normalised — the same true-north direction a
    gyrocompass finds from the spin vector. `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)`.
    """
    r_world = np.asarray(position, dtype=float) - self.center
    n = float(np.linalg.norm(r_world))
    if n == 0.0:
        raise ValueError(
            f"{type(self).__name__}.local_tangent_basis: undefined at "
            f"the planet centre")
    up = r_world / n
    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

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 local radial, 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 local radial, 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: north pole (0, 0, R_EQ), equator (R_EQ, 0, 0). The scene's axes are the local tangent frame there (+z up, +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: north pole `(0, 0, R_EQ)`, equator `(R_EQ, 0, 0)`. The scene's
    axes are the local tangent frame there (+z up, +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. Subclasses (Earth, Moon, ...) override to install gravity / ocean / atmosphere / magnetic-dipole disturbances. Base default: no-op.

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. Subclasses (Earth,
    Moon, ...) override to install gravity / ocean / atmosphere /
    magnetic-dipole disturbances. Base default: no-op.

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

manta.planets.Earth

Earth(name='earth', *, position=(0.0, 0.0, 0.0), rotation_rate=None, rotation_axis=(0.0, 0.0, 1.0), sea_level=0.0, water_density=1025.0, air_density=1.225, sea_level_temperature=T0_ISA, lapse_rate=LAPSE_ISA, gravity_mu=MU, include_j2=False, 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. sea_level — elevation of the ocean's top above the planet's equatorial radius, m. Default 0 (sea-level surface coincides with R_EQ). 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 a J2 oblateness perturbation alongside the point-mass term. Default False. 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-level sphere 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),
             sea_level: float = 0.0,
             water_density: float = 1025.0,
             air_density: float = 1.225,
             sea_level_temperature: float = T0_ISA,
             lapse_rate: float = LAPSE_ISA,
             gravity_mu: float = MU,
             include_j2: bool = False,
             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.sea_level     = float(sea_level)
    self.water_density = float(water_density)
    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)
    self.include_j2    = 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

Radius from planet center to sea-level surface. Equal to R_EQ + sea_level.

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 up (radial), +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}")
    self.planet = planet
    self.kind = kind
    self.value = tuple(float(x) for x in value)
    if len(self.value) != 3:
        raise ValueError(
            f"PlanetState: value must be length-3, got {value!r}")

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 sea-level sphere) 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.

direction is a planet-frame vector (normalized; its radial 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.