Skip to content

Boundary Conditions

Boundary condition primitives and helpers for staggered C-grid fields.

BC Atoms (1-D)

finitevolx.Dirichlet1D

Bases: Module

Apply a Dirichlet value on one domain face.

The boundary value is imposed at the physical wall between the first interior cell and the ghost cell:

phi_boundary = 0.5 * (phi_interior + phi_ghost)

Parameters:

Name Type Description Default
face Literal['south', 'north', 'west', 'east']

Domain face to update.

required
value float

Target boundary value.

required
Source code in finitevolx/_src/boundary/bc_1d.py
class Dirichlet1D(eqx.Module):
    """Apply a Dirichlet value on one domain face.

    The boundary value is imposed at the physical wall between the first
    interior cell and the ghost cell:

    ``phi_boundary = 0.5 * (phi_interior + phi_ghost)``

    Parameters
    ----------
    face : Literal["south", "north", "west", "east"]
        Domain face to update.
    value : float
        Target boundary value.
    """

    face: Face = eqx.field(static=True)
    value: float

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with one Dirichlet ghost face updated."""
        interior = _adjacent_interior(field, self.face)
        ghost = (2.0 * self.value) - interior
        return _set_face(field, self.face, ghost)

__call__(field, dx, dy)

Return field with one Dirichlet ghost face updated.

Source code in finitevolx/_src/boundary/bc_1d.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with one Dirichlet ghost face updated."""
    interior = _adjacent_interior(field, self.face)
    ghost = (2.0 * self.value) - interior
    return _set_face(field, self.face, ghost)

finitevolx.Neumann1D

Bases: Module

Apply an outward-normal gradient on one domain face.

The gradient is interpreted as dphi/dn along the outward normal, evaluated over the half-cell distance between the first interior cell and the ghost cell.

Parameters:

Name Type Description Default
face Literal['south', 'north', 'west', 'east']

Domain face to update.

required
value float

Outward-normal gradient. Defaults to 0.0 for zero-gradient boundaries.

required
Source code in finitevolx/_src/boundary/bc_1d.py
class Neumann1D(eqx.Module):
    """Apply an outward-normal gradient on one domain face.

    The gradient is interpreted as ``dphi/dn`` along the outward normal,
    evaluated over the half-cell distance between the first interior cell and
    the ghost cell.

    Parameters
    ----------
    face : Literal["south", "north", "west", "east"]
        Domain face to update.
    value : float, optional
        Outward-normal gradient. Defaults to ``0.0`` for zero-gradient
        boundaries.
    """

    face: Face = eqx.field(static=True)
    value: float = 0.0

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with one Neumann ghost face updated."""
        spacing = _normal_spacing(self.face, dx=dx, dy=dy)
        interior = _adjacent_interior(field, self.face)
        ghost = interior + (_outward_sign(self.face) * self.value * spacing)
        return _set_face(field, self.face, ghost)

__call__(field, dx, dy)

Return field with one Neumann ghost face updated.

Source code in finitevolx/_src/boundary/bc_1d.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with one Neumann ghost face updated."""
    spacing = _normal_spacing(self.face, dx=dx, dy=dy)
    interior = _adjacent_interior(field, self.face)
    ghost = interior + (_outward_sign(self.face) * self.value * spacing)
    return _set_face(field, self.face, ghost)

finitevolx.Periodic1D

Bases: Module

Apply periodic wrapping on one domain face.

Parameters:

Name Type Description Default
face Literal['south', 'north', 'west', 'east']

Domain face to update.

required
Source code in finitevolx/_src/boundary/bc_1d.py
class Periodic1D(eqx.Module):
    """Apply periodic wrapping on one domain face.

    Parameters
    ----------
    face : Literal["south", "north", "west", "east"]
        Domain face to update.
    """

    face: Face = eqx.field(static=True)

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with one periodic ghost face updated."""
        del dx, dy
        ghost = _opposite_interior(field, self.face)
        return _set_face(field, self.face, ghost)

__call__(field, dx, dy)

Return field with one periodic ghost face updated.

Source code in finitevolx/_src/boundary/bc_1d.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with one periodic ghost face updated."""
    del dx, dy
    ghost = _opposite_interior(field, self.face)
    return _set_face(field, self.face, ghost)

finitevolx.Reflective1D

Bases: Module

Apply an even-symmetry reflective boundary on one face.

This mirrors the nearest interior values into the ghost cells, which is appropriate for scalar tracers or tangential components with reflective symmetry.

Parameters:

Name Type Description Default
face Literal['south', 'north', 'west', 'east']

Domain face to update.

required
Source code in finitevolx/_src/boundary/bc_1d.py
class Reflective1D(eqx.Module):
    """Apply an even-symmetry reflective boundary on one face.

    This mirrors the nearest interior values into the ghost cells, which is
    appropriate for scalar tracers or tangential components with reflective
    symmetry.

    Parameters
    ----------
    face : Literal["south", "north", "west", "east"]
        Domain face to update.
    """

    face: Face = eqx.field(static=True)

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with one reflective ghost face updated."""
        del dx, dy
        ghost = _adjacent_interior(field, self.face)
        return _set_face(field, self.face, ghost)

__call__(field, dx, dy)

Return field with one reflective ghost face updated.

Source code in finitevolx/_src/boundary/bc_1d.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with one reflective ghost face updated."""
    del dx, dy
    ghost = _adjacent_interior(field, self.face)
    return _set_face(field, self.face, ghost)

finitevolx.Slip1D

Bases: Module

Slip boundary condition for tangential velocity at a solid wall.

Controls the tangential velocity at the wall via a slip coefficient a in [0, 1]:

  • a = 1.0 — free-slip: ghost = +interior (zero gradient, frictionless wall).
  • a = 0.0 — no-slip: ghost = -interior (zero tangential velocity at wall).
  • 0 < a < 1 — partial-slip: linear interpolation between the two.

The ghost cell is set using:

phi_ghost = (2*a - 1) * phi_interior

so that the extrapolated wall value phi_wall = 0.5 * (phi_ghost + phi_interior) = a * phi_interior smoothly varies from zero (no-slip) to the interior value (free-slip).

Parameters:

Name Type Description Default
face Literal['south', 'north', 'west', 'east']

Domain face to update.

required
coefficient float

Slip parameter a in [0, 1]. Default is 1.0 (free-slip).

required

Examples:

Free-slip on the west wall (tangential velocity preserved):

>>> bc = Slip1D("west", coefficient=1.0)
>>> field_out = bc(field, dx=1.0, dy=1.0)

No-slip on the north wall (tangential velocity -> 0):

>>> bc = Slip1D("north", coefficient=0.0)
>>> field_out = bc(field, dx=1.0, dy=1.0)

Partial-slip on the south wall:

>>> bc = Slip1D("south", coefficient=0.5)
>>> field_out = bc(field, dx=1.0, dy=1.0)
Source code in finitevolx/_src/boundary/bc_1d.py
class Slip1D(eqx.Module):
    """Slip boundary condition for tangential velocity at a solid wall.

    Controls the tangential velocity at the wall via a slip coefficient
    ``a in [0, 1]``:

    - ``a = 1.0`` — **free-slip**: ghost = +interior (zero gradient, frictionless wall).
    - ``a = 0.0`` — **no-slip**: ghost = -interior (zero tangential velocity at wall).
    - ``0 < a < 1`` — **partial-slip**: linear interpolation between the two.

    The ghost cell is set using:

    ``phi_ghost = (2*a - 1) * phi_interior``

    so that the extrapolated wall value
    ``phi_wall = 0.5 * (phi_ghost + phi_interior) = a * phi_interior``
    smoothly varies from zero (no-slip) to the interior value (free-slip).

    Parameters
    ----------
    face : Literal["south", "north", "west", "east"]
        Domain face to update.
    coefficient : float
        Slip parameter ``a`` in ``[0, 1]``. Default is ``1.0`` (free-slip).

    Examples
    --------
    Free-slip on the west wall (tangential velocity preserved):

    >>> bc = Slip1D("west", coefficient=1.0)
    >>> field_out = bc(field, dx=1.0, dy=1.0)

    No-slip on the north wall (tangential velocity -> 0):

    >>> bc = Slip1D("north", coefficient=0.0)
    >>> field_out = bc(field, dx=1.0, dy=1.0)

    Partial-slip on the south wall:

    >>> bc = Slip1D("south", coefficient=0.5)
    >>> field_out = bc(field, dx=1.0, dy=1.0)
    """

    face: Face = eqx.field(static=True)
    coefficient: float = 1.0

    def __check_init__(self) -> None:
        if not 0.0 <= self.coefficient <= 1.0:
            raise ValueError("Slip1D coefficient must lie in [0, 1].")

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with one slip ghost face updated.

        Parameters
        ----------
        field : Float[Array, "Ny Nx"]
            Input array with ghost-cell ring.
        dx : float
            Grid spacing in x (unused, kept for interface consistency).
        dy : float
            Grid spacing in y (unused, kept for interface consistency).

        Returns
        -------
        Float[Array, "Ny Nx"]
            Field with the ghost face set according to the slip coefficient.
        """
        del dx, dy
        interior = _adjacent_interior(field, self.face)
        # ghost = (2*a - 1) * interior
        # wall value = 0.5 * (ghost + interior) = a * interior
        ghost = (2.0 * self.coefficient - 1.0) * interior
        return _set_face(field, self.face, ghost)

__call__(field, dx, dy)

Return field with one slip ghost face updated.

Parameters:

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

Input array with ghost-cell ring.

required
dx float

Grid spacing in x (unused, kept for interface consistency).

required
dy float

Grid spacing in y (unused, kept for interface consistency).

required

Returns:

Type Description
Float[Array, 'Ny Nx']

Field with the ghost face set according to the slip coefficient.

Source code in finitevolx/_src/boundary/bc_1d.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with one slip ghost face updated.

    Parameters
    ----------
    field : Float[Array, "Ny Nx"]
        Input array with ghost-cell ring.
    dx : float
        Grid spacing in x (unused, kept for interface consistency).
    dy : float
        Grid spacing in y (unused, kept for interface consistency).

    Returns
    -------
    Float[Array, "Ny Nx"]
        Field with the ghost face set according to the slip coefficient.
    """
    del dx, dy
    interior = _adjacent_interior(field, self.face)
    # ghost = (2*a - 1) * interior
    # wall value = 0.5 * (ghost + interior) = a * interior
    ghost = (2.0 * self.coefficient - 1.0) * interior
    return _set_face(field, self.face, ghost)

finitevolx.Outflow1D

Bases: Module

Apply a one-face outflow boundary condition.

Outflow is modelled here as a zero-gradient copy from the nearest interior cell into the ghost ring.

Parameters:

Name Type Description Default
face Literal['south', 'north', 'west', 'east']

Domain face to update.

required
Source code in finitevolx/_src/boundary/bc_1d.py
class Outflow1D(eqx.Module):
    """Apply a one-face outflow boundary condition.

    Outflow is modelled here as a zero-gradient copy from the nearest
    interior cell into the ghost ring.

    Parameters
    ----------
    face : Literal["south", "north", "west", "east"]
        Domain face to update.
    """

    face: Face = eqx.field(static=True)

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with one outflow ghost face updated."""
        del dx, dy
        ghost = _adjacent_interior(field, self.face)
        return _set_face(field, self.face, ghost)

__call__(field, dx, dy)

Return field with one outflow ghost face updated.

Source code in finitevolx/_src/boundary/bc_1d.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with one outflow ghost face updated."""
    del dx, dy
    ghost = _adjacent_interior(field, self.face)
    return _set_face(field, self.face, ghost)

finitevolx.Sponge1D

Bases: Module

Relax one ghost face toward a background value.

The ghost cells are updated as:

phi_ghost = (1 - weight) * phi_interior + weight * background

Parameters:

Name Type Description Default
face Literal['south', 'north', 'west', 'east']

Domain face to update.

required
background float

Background state toward which the ghost cells relax.

required
weight float

Relaxation weight in [0, 1].

required
Source code in finitevolx/_src/boundary/bc_1d.py
class Sponge1D(eqx.Module):
    """Relax one ghost face toward a background value.

    The ghost cells are updated as:

    ``phi_ghost = (1 - weight) * phi_interior + weight * background``

    Parameters
    ----------
    face : Literal["south", "north", "west", "east"]
        Domain face to update.
    background : float
        Background state toward which the ghost cells relax.
    weight : float
        Relaxation weight in ``[0, 1]``.
    """

    face: Face = eqx.field(static=True)
    background: float
    weight: float

    def __check_init__(self) -> None:
        if not 0.0 <= self.weight <= 1.0:
            raise ValueError("Sponge1D weight must lie in [0, 1].")

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with one sponge ghost face updated."""
        del dx, dy
        interior = _adjacent_interior(field, self.face)
        ghost = ((1.0 - self.weight) * interior) + (self.weight * self.background)
        return _set_face(field, self.face, ghost)

__call__(field, dx, dy)

Return field with one sponge ghost face updated.

Source code in finitevolx/_src/boundary/bc_1d.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with one sponge ghost face updated."""
    del dx, dy
    interior = _adjacent_interior(field, self.face)
    ghost = ((1.0 - self.weight) * interior) + (self.weight * self.background)
    return _set_face(field, self.face, ghost)

BC Composition

finitevolx.BoundaryConditionSet

Bases: Module

Apply a different boundary condition on each domain face.

The application order is south, north, west, then east, so west/east updates overwrite the corner values written by south/north updates.

Parameters:

Name Type Description Default
south BoundaryCondition1D | None

Boundary condition for the south ghost row.

required
north BoundaryCondition1D | None

Boundary condition for the north ghost row.

required
west BoundaryCondition1D | None

Boundary condition for the west ghost column.

required
east BoundaryCondition1D | None

Boundary condition for the east ghost column.

required
Source code in finitevolx/_src/boundary/bc_set.py
class BoundaryConditionSet(eqx.Module):
    """Apply a different boundary condition on each domain face.

    The application order is south, north, west, then east, so west/east
    updates overwrite the corner values written by south/north updates.

    Parameters
    ----------
    south : BoundaryCondition1D | None, optional
        Boundary condition for the south ghost row.
    north : BoundaryCondition1D | None, optional
        Boundary condition for the north ghost row.
    west : BoundaryCondition1D | None, optional
        Boundary condition for the west ghost column.
    east : BoundaryCondition1D | None, optional
        Boundary condition for the east ghost column.
    """

    south: BoundaryCondition1D | None = None
    north: BoundaryCondition1D | None = None
    west: BoundaryCondition1D | None = None
    east: BoundaryCondition1D | None = None

    @classmethod
    def periodic(cls) -> BoundaryConditionSet:
        """Return a fully periodic boundary-condition set."""
        return cls(
            south=Periodic1D("south"),
            north=Periodic1D("north"),
            west=Periodic1D("west"),
            east=Periodic1D("east"),
        )

    @classmethod
    def open(cls) -> BoundaryConditionSet:
        """Return a zero-gradient boundary-condition set on all faces."""
        return cls(
            south=Outflow1D("south"),
            north=Outflow1D("north"),
            west=Outflow1D("west"),
            east=Outflow1D("east"),
        )

    def __call__(
        self, field: Float[Array, "Ny Nx"], dx: float, dy: float
    ) -> Float[Array, "Ny Nx"]:
        """Return ``field`` with all configured ghost faces updated.

        Boundary conditions are applied in south, north, west, east order, so
        west/east updates overwrite the corner values written by south/north.
        """
        out = field
        for bc in (self.south, self.north, self.west, self.east):
            if bc is not None:
                out = bc(out, dx=dx, dy=dy)
        return out

__call__(field, dx, dy)

Return field with all configured ghost faces updated.

Boundary conditions are applied in south, north, west, east order, so west/east updates overwrite the corner values written by south/north.

Source code in finitevolx/_src/boundary/bc_set.py
def __call__(
    self, field: Float[Array, "Ny Nx"], dx: float, dy: float
) -> Float[Array, "Ny Nx"]:
    """Return ``field`` with all configured ghost faces updated.

    Boundary conditions are applied in south, north, west, east order, so
    west/east updates overwrite the corner values written by south/north.
    """
    out = field
    for bc in (self.south, self.north, self.west, self.east):
        if bc is not None:
            out = bc(out, dx=dx, dy=dy)
    return out

open() classmethod

Return a zero-gradient boundary-condition set on all faces.

Source code in finitevolx/_src/boundary/bc_set.py
@classmethod
def open(cls) -> BoundaryConditionSet:
    """Return a zero-gradient boundary-condition set on all faces."""
    return cls(
        south=Outflow1D("south"),
        north=Outflow1D("north"),
        west=Outflow1D("west"),
        east=Outflow1D("east"),
    )

periodic() classmethod

Return a fully periodic boundary-condition set.

Source code in finitevolx/_src/boundary/bc_set.py
@classmethod
def periodic(cls) -> BoundaryConditionSet:
    """Return a fully periodic boundary-condition set."""
    return cls(
        south=Periodic1D("south"),
        north=Periodic1D("north"),
        west=Periodic1D("west"),
        east=Periodic1D("east"),
    )

finitevolx.FieldBCSet

Bases: Module

Dispatch per-face boundary conditions across multiple fields.

Parameters:

Name Type Description Default
bc_map dict[str, BoundaryConditionSet]

Mapping from state-variable name to per-face boundary conditions.

required
default BoundaryConditionSet | None

Boundary-condition set used when a variable is not present in bc_map. If omitted, unmatched state entries are returned unchanged.

required
Source code in finitevolx/_src/boundary/bc_field.py
class FieldBCSet(eqx.Module):
    """Dispatch per-face boundary conditions across multiple fields.

    Parameters
    ----------
    bc_map : dict[str, BoundaryConditionSet]
        Mapping from state-variable name to per-face boundary conditions.
    default : BoundaryConditionSet | None, optional
        Boundary-condition set used when a variable is not present in
        ``bc_map``. If omitted, unmatched state entries are returned unchanged.
    """

    bc_map: dict[str, BoundaryConditionSet]
    default: BoundaryConditionSet | None = None

    def __call__(
        self, state: dict[str, Float[Array, "Ny Nx"]], dx: float, dy: float
    ) -> dict[str, Float[Array, "Ny Nx"]]:
        """Return a new state dictionary with boundary conditions applied."""
        out: dict[str, Float[Array, "Ny Nx"]] = {}
        for name, field in state.items():
            bc = self.bc_map.get(name, self.default)
            out[name] = field if bc is None else bc(field, dx=dx, dy=dy)
        return out

__call__(state, dx, dy)

Return a new state dictionary with boundary conditions applied.

Source code in finitevolx/_src/boundary/bc_field.py
def __call__(
    self, state: dict[str, Float[Array, "Ny Nx"]], dx: float, dy: float
) -> dict[str, Float[Array, "Ny Nx"]]:
    """Return a new state dictionary with boundary conditions applied."""
    out: dict[str, Float[Array, "Ny Nx"]] = {}
    for name, field in state.items():
        bc = self.bc_map.get(name, self.default)
        out[name] = field if bc is None else bc(field, dx=dx, dy=dy)
    return out

Helpers

finitevolx.enforce_periodic(field)

Fill the ghost-cell ring with periodic boundary conditions.

Copies the last interior row/column into the opposite ghost row/column::

ghost south  <- last interior row   (row  Ny-2)
ghost north  <- first interior row  (row  1   )
ghost west   <- last interior col   (col  Nx-2)
ghost east   <- first interior col  (col  1   )

Parameters:

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

Input array of shape [Ny, Nx].

required

Returns:

Type Description
Float[Array, 'Ny Nx']

Array with periodic ghost cells.

Source code in finitevolx/_src/boundary/boundary.py
def enforce_periodic(
    field: Float[Array, "Ny Nx"],
) -> Float[Array, "Ny Nx"]:
    """Fill the ghost-cell ring with periodic boundary conditions.

    Copies the last interior row/column into the opposite ghost row/column::

        ghost south  <- last interior row   (row  Ny-2)
        ghost north  <- first interior row  (row  1   )
        ghost west   <- last interior col   (col  Nx-2)
        ghost east   <- first interior col  (col  1   )

    Parameters
    ----------
    field : Float[Array, "Ny Nx"]
        Input array of shape [Ny, Nx].

    Returns
    -------
    Float[Array, "Ny Nx"]
        Array with periodic ghost cells.
    """
    # Periodic1D wraps opposite interior values and does not use dx/dy.
    return BoundaryConditionSet.periodic()(field, dx=1.0, dy=1.0)

finitevolx.pad_interior(field, mode='edge')

Extract physical interior and re-pad to original size.

Strips the ghost-cell ring, then re-pads using the requested mode. Useful for enforcing boundary conditions by re-filling the ghost ring.

Parameters:

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

Input array of shape [Ny, Nx].

required
mode str

Padding mode passed to jnp.pad. Defaults to 'edge' (copy nearest interior value into ghost cells).

'edge'

Returns:

Type Description
Float[Array, 'Ny Nx']

Array of shape [Ny, Nx] with ghost cells re-filled.

Examples:

>>> import jax.numpy as jnp
>>> f = jnp.ones((6, 6))
>>> pad_interior(f).shape
(6, 6)
Source code in finitevolx/_src/boundary/boundary.py
def pad_interior(
    field: Float[Array, "Ny Nx"], mode: str = "edge"
) -> Float[Array, "Ny Nx"]:
    """Extract physical interior and re-pad to original size.

    Strips the ghost-cell ring, then re-pads using the requested mode.
    Useful for enforcing boundary conditions by re-filling the ghost ring.

    Parameters
    ----------
    field : Float[Array, "Ny Nx"]
        Input array of shape [Ny, Nx].
    mode : str, optional
        Padding mode passed to ``jnp.pad``.  Defaults to ``'edge'``
        (copy nearest interior value into ghost cells).

    Returns
    -------
    Float[Array, "Ny Nx"]
        Array of shape [Ny, Nx] with ghost cells re-filled.

    Examples
    --------
    >>> import jax.numpy as jnp
    >>> f = jnp.ones((6, 6))
    >>> pad_interior(f).shape
    (6, 6)
    """
    if mode == "edge":
        # BoundaryConditionSet.open() is zero-gradient and does not use dx/dy.
        return BoundaryConditionSet.open()(field, dx=1.0, dy=1.0)
    interior = field[1:-1, 1:-1]  # shape [Ny-2, Nx-2]
    return jnp.pad(interior, pad_width=1, mode=mode)  # shape [Ny, Nx]