Skip to content

Coriolis Operators

Coriolis force operators for rotating-frame geophysical models on C-grids.

finitevolx.Coriolis2D

Bases: Module

Coriolis force operator for 2-D Arakawa C-grids.

Computes the Coriolis tendency for both velocity components:

du_cor[j, i+1/2] = +f_on_u[j, i+1/2] * v_on_u[j, i+1/2]
dv_cor[j+1/2, i] = -f_on_v[j+1/2, i] * u_on_v[j+1/2, i]

The Coriolis parameter f is interpolated from T-points to velocity points using simple x/y averaging. The cross-face velocity averages are computed with 4-point bilinear interpolation (same as :class:Interpolation2D).

Parameters:

Name Type Description Default
grid CartesianGrid2D

The underlying 2-D grid.

required
mask Mask2D or None

Optional land/ocean mask. When provided, the __call__ output is post-multiplied by mask.u / mask.v on the two velocity components. None (default) leaves the output untouched.

None

Examples:

>>> import jax.numpy as jnp
>>> from finitevolx import CartesianGrid2D, Coriolis2D
>>> grid = CartesianGrid2D.from_interior(8, 8, 1.0, 1.0)
>>> cor = Coriolis2D(grid=grid)
>>> u = jnp.zeros((grid.Ny, grid.Nx))
>>> v = jnp.ones((grid.Ny, grid.Nx))
>>> f = jnp.ones((grid.Ny, grid.Nx))
>>> du_cor, dv_cor = cor(u, v, f)
Source code in finitevolx/_src/operators/coriolis.py
class Coriolis2D(eqx.Module):
    """Coriolis force operator for 2-D Arakawa C-grids.

    Computes the Coriolis tendency for both velocity components:

        du_cor[j, i+1/2] = +f_on_u[j, i+1/2] * v_on_u[j, i+1/2]
        dv_cor[j+1/2, i] = -f_on_v[j+1/2, i] * u_on_v[j+1/2, i]

    The Coriolis parameter f is interpolated from T-points to velocity points
    using simple x/y averaging.  The cross-face velocity averages are computed
    with 4-point bilinear interpolation (same as :class:`Interpolation2D`).

    Parameters
    ----------
    grid : CartesianGrid2D
        The underlying 2-D grid.
    mask : Mask2D or None, optional
        Optional land/ocean mask.  When provided, the ``__call__``
        output is post-multiplied by ``mask.u`` / ``mask.v`` on the
        two velocity components.  ``None`` (default) leaves the
        output untouched.

    Examples
    --------
    >>> import jax.numpy as jnp
    >>> from finitevolx import CartesianGrid2D, Coriolis2D
    >>> grid = CartesianGrid2D.from_interior(8, 8, 1.0, 1.0)
    >>> cor = Coriolis2D(grid=grid)
    >>> u = jnp.zeros((grid.Ny, grid.Nx))
    >>> v = jnp.ones((grid.Ny, grid.Nx))
    >>> f = jnp.ones((grid.Ny, grid.Nx))
    >>> du_cor, dv_cor = cor(u, v, f)
    """

    grid: CartesianGrid2D
    mask: Mask2D | None
    interp: Interpolation2D

    def __init__(
        self,
        grid: CartesianGrid2D,
        mask: Mask2D | None = None,
    ) -> None:
        self.grid = grid
        self.mask = mask
        self.interp = Interpolation2D(grid=grid)

    def __call__(
        self,
        u: Float[Array, "Ny Nx"],
        v: Float[Array, "Ny Nx"],
        f: Float[Array, "Ny Nx"],
    ) -> tuple[Float[Array, "Ny Nx"], Float[Array, "Ny Nx"]]:
        """Coriolis tendencies (du_cor, dv_cor).

        du_cor[j, i+1/2] = +f_on_u * v_on_u
        dv_cor[j+1/2, i] = -f_on_v * u_on_v

        Parameters
        ----------
        u : Float[Array, "Ny Nx"]
            x-velocity at U-points (east faces).
        v : Float[Array, "Ny Nx"]
            y-velocity at V-points (north faces).
        f : Float[Array, "Ny Nx"]
            Coriolis parameter at T-points.

        Returns
        -------
        tuple[Float[Array, "Ny Nx"], Float[Array, "Ny Nx"]]
            ``(du_cor, dv_cor)`` — Coriolis tendencies at U-points and
            V-points respectively, both zero in the ghost ring.  When
            ``self.mask`` is set, ``du_cor`` is zeroed at dry U-faces
            via ``* mask.u`` and ``dv_cor`` at dry V-faces via
            ``* mask.v``.
        """
        # Interpolate f from T-points to velocity points
        # f_on_u[j, i+1/2] = 1/2 * (f[j, i] + f[j, i+1])
        f_on_u = self.interp.T_to_U(f)
        # f_on_v[j+1/2, i] = 1/2 * (f[j, i] + f[j+1, i])
        f_on_v = self.interp.T_to_V(f)

        # Cross-face velocity averages (4-point bilinear)
        # v_on_u[j, i+1/2] = 1/4*(v[j+1/2,i] + v[j-1/2,i] + v[j+1/2,i+1] + v[j-1/2,i+1])
        v_on_u = self.interp.V_to_U(v)
        # u_on_v[j+1/2, i] = 1/4*(u[j,i+1/2] + u[j+1,i+1/2] + u[j,i-1/2] + u[j+1,i-1/2])
        u_on_v = self.interp.U_to_V(u)

        # du_cor[j, i+1/2] = +f_on_u * v_on_u
        du_cor = interior(f_on_u[1:-1, 1:-1] * v_on_u[1:-1, 1:-1], u)
        # dv_cor[j+1/2, i] = -f_on_v * u_on_v
        dv_cor = interior(-f_on_v[1:-1, 1:-1] * u_on_v[1:-1, 1:-1], v)

        if self.mask is not None:
            du_cor = du_cor * self.mask.u
            dv_cor = dv_cor * self.mask.v

        return du_cor, dv_cor

__call__(u, v, f)

Coriolis tendencies (du_cor, dv_cor).

du_cor[j, i+1/2] = +f_on_u * v_on_u dv_cor[j+1/2, i] = -f_on_v * u_on_v

Parameters:

Name Type Description Default
u Float[Array, 'Ny Nx']

x-velocity at U-points (east faces).

required
v Float[Array, 'Ny Nx']

y-velocity at V-points (north faces).

required
f Float[Array, 'Ny Nx']

Coriolis parameter at T-points.

required

Returns:

Type Description
tuple[Float[Array, 'Ny Nx'], Float[Array, 'Ny Nx']]

(du_cor, dv_cor) — Coriolis tendencies at U-points and V-points respectively, both zero in the ghost ring. When self.mask is set, du_cor is zeroed at dry U-faces via * mask.u and dv_cor at dry V-faces via * mask.v.

Source code in finitevolx/_src/operators/coriolis.py
def __call__(
    self,
    u: Float[Array, "Ny Nx"],
    v: Float[Array, "Ny Nx"],
    f: Float[Array, "Ny Nx"],
) -> tuple[Float[Array, "Ny Nx"], Float[Array, "Ny Nx"]]:
    """Coriolis tendencies (du_cor, dv_cor).

    du_cor[j, i+1/2] = +f_on_u * v_on_u
    dv_cor[j+1/2, i] = -f_on_v * u_on_v

    Parameters
    ----------
    u : Float[Array, "Ny Nx"]
        x-velocity at U-points (east faces).
    v : Float[Array, "Ny Nx"]
        y-velocity at V-points (north faces).
    f : Float[Array, "Ny Nx"]
        Coriolis parameter at T-points.

    Returns
    -------
    tuple[Float[Array, "Ny Nx"], Float[Array, "Ny Nx"]]
        ``(du_cor, dv_cor)`` — Coriolis tendencies at U-points and
        V-points respectively, both zero in the ghost ring.  When
        ``self.mask`` is set, ``du_cor`` is zeroed at dry U-faces
        via ``* mask.u`` and ``dv_cor`` at dry V-faces via
        ``* mask.v``.
    """
    # Interpolate f from T-points to velocity points
    # f_on_u[j, i+1/2] = 1/2 * (f[j, i] + f[j, i+1])
    f_on_u = self.interp.T_to_U(f)
    # f_on_v[j+1/2, i] = 1/2 * (f[j, i] + f[j+1, i])
    f_on_v = self.interp.T_to_V(f)

    # Cross-face velocity averages (4-point bilinear)
    # v_on_u[j, i+1/2] = 1/4*(v[j+1/2,i] + v[j-1/2,i] + v[j+1/2,i+1] + v[j-1/2,i+1])
    v_on_u = self.interp.V_to_U(v)
    # u_on_v[j+1/2, i] = 1/4*(u[j,i+1/2] + u[j+1,i+1/2] + u[j,i-1/2] + u[j+1,i-1/2])
    u_on_v = self.interp.U_to_V(u)

    # du_cor[j, i+1/2] = +f_on_u * v_on_u
    du_cor = interior(f_on_u[1:-1, 1:-1] * v_on_u[1:-1, 1:-1], u)
    # dv_cor[j+1/2, i] = -f_on_v * u_on_v
    dv_cor = interior(-f_on_v[1:-1, 1:-1] * u_on_v[1:-1, 1:-1], v)

    if self.mask is not None:
        du_cor = du_cor * self.mask.u
        dv_cor = dv_cor * self.mask.v

    return du_cor, dv_cor

finitevolx.Coriolis3D

Bases: Module

Coriolis force operator for 3-D Arakawa C-grids.

Applies the same horizontal Coriolis stencil as :class:Coriolis2D independently at each z-level. The Coriolis parameter f is 2-D (depth-independent) and broadcast over all z-levels.

Parameters:

Name Type Description Default
grid CartesianGrid3D

The underlying 3-D grid.

required
mask Mask3D or None

Optional land/ocean mask. Pattern A (post-compute) — the inner :class:Coriolis2D is constructed mask=None and the 3-D result is post-multiplied by mask.u / mask.v after the vmap.

Takes a :class:Mask3D (not Mask2D): even though f is 2-D and depth-independent, the velocity mask is natively 3-D on the Arakawa C-grid, and using Mask3D keeps the 3-D operator suite type-uniform per #209. Issue #209's Q4 locked this in for Advection3D and this commit extends the same decision to Coriolis3D.

None

Examples:

>>> import jax.numpy as jnp
>>> from finitevolx import CartesianGrid3D, Coriolis3D
>>> grid = CartesianGrid3D.from_interior(6, 6, 4, 1.0, 1.0, 1.0)
>>> cor = Coriolis3D(grid=grid)
>>> u = jnp.zeros((grid.Nz, grid.Ny, grid.Nx))
>>> v = jnp.ones((grid.Nz, grid.Ny, grid.Nx))
>>> f = jnp.ones((grid.Ny, grid.Nx))
>>> du_cor, dv_cor = cor(u, v, f)
Source code in finitevolx/_src/operators/coriolis.py
class Coriolis3D(eqx.Module):
    """Coriolis force operator for 3-D Arakawa C-grids.

    Applies the same horizontal Coriolis stencil as :class:`Coriolis2D`
    independently at each z-level.  The Coriolis parameter f is 2-D
    (depth-independent) and broadcast over all z-levels.

    Parameters
    ----------
    grid : CartesianGrid3D
        The underlying 3-D grid.
    mask : Mask3D or None, optional
        Optional land/ocean mask.  Pattern A (post-compute) — the
        inner :class:`Coriolis2D` is constructed ``mask=None`` and
        the 3-D result is post-multiplied by ``mask.u`` / ``mask.v``
        after the vmap.

        Takes a :class:`Mask3D` (not ``Mask2D``): even though ``f``
        is 2-D and depth-independent, the velocity mask is natively
        3-D on the Arakawa C-grid, and using ``Mask3D`` keeps the
        3-D operator suite type-uniform per #209.  Issue #209's Q4
        locked this in for Advection3D and this commit extends the
        same decision to Coriolis3D.

    Examples
    --------
    >>> import jax.numpy as jnp
    >>> from finitevolx import CartesianGrid3D, Coriolis3D
    >>> grid = CartesianGrid3D.from_interior(6, 6, 4, 1.0, 1.0, 1.0)
    >>> cor = Coriolis3D(grid=grid)
    >>> u = jnp.zeros((grid.Nz, grid.Ny, grid.Nx))
    >>> v = jnp.ones((grid.Nz, grid.Ny, grid.Nx))
    >>> f = jnp.ones((grid.Ny, grid.Nx))
    >>> du_cor, dv_cor = cor(u, v, f)
    """

    grid: CartesianGrid3D
    mask: Mask3D | None
    _cor2d: Coriolis2D

    def __init__(
        self,
        grid: CartesianGrid3D,
        mask: Mask3D | None = None,
    ) -> None:
        self.grid = grid
        self.mask = mask
        # Pattern A: inner 2-D op is mask-free; the 3-D wrapper owns the mask.
        self._cor2d = Coriolis2D(grid=grid.horizontal_grid())

    def __call__(
        self,
        u: Float[Array, "Nz Ny Nx"],
        v: Float[Array, "Nz Ny Nx"],
        f: Float[Array, "Ny Nx"],
    ) -> tuple[Float[Array, "Nz Ny Nx"], Float[Array, "Nz Ny Nx"]]:
        """Coriolis tendencies over all z-levels.

        du_cor[k, j, i+1/2] = +f_on_u[j, i+1/2] * v_on_u[k, j, i+1/2]
        dv_cor[k, j+1/2, i] = -f_on_v[j+1/2, i] * u_on_v[k, j+1/2, i]

        Parameters
        ----------
        u : Float[Array, "Nz Ny Nx"]
            x-velocity at U-points.
        v : Float[Array, "Nz Ny Nx"]
            y-velocity at V-points.
        f : Float[Array, "Ny Nx"]
            Coriolis parameter at T-points (depth-independent).

        Returns
        -------
        tuple[Float[Array, "Nz Ny Nx"], Float[Array, "Nz Ny Nx"]]
            ``(du_cor, dv_cor)`` — Coriolis tendencies at U-points and
            V-points, both zero in the ghost ring.  When ``self.mask``
            is set, ``du_cor`` is zeroed at dry U-faces via
            ``* mask.u`` and ``dv_cor`` at dry V-faces via ``* mask.v``.
        """
        # Vmap the 2D Coriolis operator over z-levels.
        # f is 2D (depth-independent) and broadcast to each z-slice.
        du_cor, dv_cor = eqx.filter_vmap(lambda u_k, v_k: self._cor2d(u_k, v_k, f))(
            u, v
        )

        # Zero the z-ghost slices to match the 3D ghost-ring convention.
        du_cor = zero_z_ghosts(du_cor)
        dv_cor = zero_z_ghosts(dv_cor)

        if self.mask is not None:
            du_cor = du_cor * self.mask.u
            dv_cor = dv_cor * self.mask.v

        return du_cor, dv_cor

__call__(u, v, f)

Coriolis tendencies over all z-levels.

du_cor[k, j, i+1/2] = +f_on_u[j, i+1/2] * v_on_u[k, j, i+1/2] dv_cor[k, j+1/2, i] = -f_on_v[j+1/2, i] * u_on_v[k, j+1/2, i]

Parameters:

Name Type Description Default
u Float[Array, 'Nz Ny Nx']

x-velocity at U-points.

required
v Float[Array, 'Nz Ny Nx']

y-velocity at V-points.

required
f Float[Array, 'Ny Nx']

Coriolis parameter at T-points (depth-independent).

required

Returns:

Type Description
tuple[Float[Array, 'Nz Ny Nx'], Float[Array, 'Nz Ny Nx']]

(du_cor, dv_cor) — Coriolis tendencies at U-points and V-points, both zero in the ghost ring. When self.mask is set, du_cor is zeroed at dry U-faces via * mask.u and dv_cor at dry V-faces via * mask.v.

Source code in finitevolx/_src/operators/coriolis.py
def __call__(
    self,
    u: Float[Array, "Nz Ny Nx"],
    v: Float[Array, "Nz Ny Nx"],
    f: Float[Array, "Ny Nx"],
) -> tuple[Float[Array, "Nz Ny Nx"], Float[Array, "Nz Ny Nx"]]:
    """Coriolis tendencies over all z-levels.

    du_cor[k, j, i+1/2] = +f_on_u[j, i+1/2] * v_on_u[k, j, i+1/2]
    dv_cor[k, j+1/2, i] = -f_on_v[j+1/2, i] * u_on_v[k, j+1/2, i]

    Parameters
    ----------
    u : Float[Array, "Nz Ny Nx"]
        x-velocity at U-points.
    v : Float[Array, "Nz Ny Nx"]
        y-velocity at V-points.
    f : Float[Array, "Ny Nx"]
        Coriolis parameter at T-points (depth-independent).

    Returns
    -------
    tuple[Float[Array, "Nz Ny Nx"], Float[Array, "Nz Ny Nx"]]
        ``(du_cor, dv_cor)`` — Coriolis tendencies at U-points and
        V-points, both zero in the ghost ring.  When ``self.mask``
        is set, ``du_cor`` is zeroed at dry U-faces via
        ``* mask.u`` and ``dv_cor`` at dry V-faces via ``* mask.v``.
    """
    # Vmap the 2D Coriolis operator over z-levels.
    # f is 2D (depth-independent) and broadcast to each z-slice.
    du_cor, dv_cor = eqx.filter_vmap(lambda u_k, v_k: self._cor2d(u_k, v_k, f))(
        u, v
    )

    # Zero the z-ghost slices to match the 3D ghost-ring convention.
    du_cor = zero_z_ghosts(du_cor)
    dv_cor = zero_z_ghosts(dv_cor)

    if self.mask is not None:
        du_cor = du_cor * self.mask.u
        dv_cor = dv_cor * self.mask.v

    return du_cor, dv_cor