Skip to content

Capacitance Matrix Method

Extends fast spectral solvers to irregular / masked domains using the Sherman-Morrison capacitance correction (Buzbee, Golub & Nielson 1970).

Solver

finitevolx.CapacitanceSolver

Bases: Module

Spectral Poisson/Helmholtz solver for masked irregular domains.

Uses the capacitance matrix method (Buzbee, Golub & Nielson 1970) to extend a fast rectangular spectral solver to a domain defined by a binary mask.

The algorithm (Buzbee, Golub & Nielson 1970):

  1. Solve the PDE on the full rectangle using a fast spectral solver (DST/DCT/FFT), ignoring the mask. Call this u.
  2. u generally violates ψ = 0 at inner-boundary points. Correct it: ψ = u − Σ_k α_k g_k, where g_k are precomputed Green's functions (rectangular-domain response to δ-sources at each boundary point b_k).
  3. The coefficients α are found by requiring ψ(b_k) = 0 at all N_b boundary points, giving the linear system C α = u[B] where C[k,l] = g_l(b_k) is the capacitance matrix.

The capacitance correction is delegated to :class:gaussx.CapacitanceSolver; this class adds the field reshaping and the exterior masking. Construct with :func:build_capacitance_solver.

Attributes:

Name Type Description
solver CapacitanceSolver

The generic capacitance solver operating on flat vectors.

mask Float[Array, 'Ny Nx']

Domain mask (1.0 = interior, 0.0 = exterior). Applied to the output so that values outside the physical domain are exactly zero.

shape tuple[int, int]

Grid shape (Ny, Nx).

dx float

Grid spacing in x.

dy float

Grid spacing in y.

lambda_ float

Helmholtz parameter.

base_bc str

Spectral solver used as the rectangular base.

Source code in .venv/lib/python3.12/site-packages/spectraldiffx/_src/fourier/capacitance.py
class CapacitanceSolver(eqx.Module):
    """Spectral Poisson/Helmholtz solver for masked irregular domains.

    Uses the **capacitance matrix method** (Buzbee, Golub & Nielson 1970) to
    extend a fast rectangular spectral solver to a domain defined by a binary
    mask.

    The algorithm (Buzbee, Golub & Nielson 1970):

    1. Solve the PDE on the **full rectangle** using a fast spectral solver
       (DST/DCT/FFT), ignoring the mask.  Call this ``u``.
    2. ``u`` generally violates ψ = 0 at inner-boundary points.  Correct it:
       ``ψ = u − Σ_k α_k g_k``, where ``g_k`` are precomputed Green's
       functions (rectangular-domain response to δ-sources at each
       boundary point b_k).
    3. The coefficients α are found by requiring ψ(b_k) = 0 at all
       N_b boundary points, giving the linear system ``C α = u[B]``
       where ``C[k,l] = g_l(b_k)`` is the **capacitance matrix**.

    The capacitance correction is delegated to :class:`gaussx.CapacitanceSolver`;
    this class adds the field reshaping and the exterior masking. Construct with
    :func:`build_capacitance_solver`.

    Attributes
    ----------
    solver : gaussx.CapacitanceSolver
        The generic capacitance solver operating on flat vectors.
    mask : Float[Array, "Ny Nx"]
        Domain mask (1.0 = interior, 0.0 = exterior).  Applied to the output
        so that values outside the physical domain are exactly zero.
    shape : tuple[int, int]
        Grid shape ``(Ny, Nx)``.
    dx : float
        Grid spacing in x.
    dy : float
        Grid spacing in y.
    lambda_ : float
        Helmholtz parameter.
    base_bc : str
        Spectral solver used as the rectangular base.
    """

    solver: _GaussxCapacitanceSolver
    mask: Float[Array, "Ny Nx"]
    shape: tuple[int, int] = eqx.field(static=True)
    dx: float
    dy: float
    lambda_: float = eqx.field(static=True)
    base_bc: str = eqx.field(static=True)

    def __call__(
        self,
        rhs: Float[Array, "Ny Nx"],
    ) -> Float[Array, "Ny Nx"]:
        """Solve (∇² − λ)ψ = rhs on the masked domain.

        The generic solver enforces ψ = 0 at the inner-boundary points and
        returns the corrected field; this method reshapes between fields and
        flat vectors and zeroes the exterior via the mask.

        Parameters
        ----------
        rhs : Float[Array, "Ny Nx"]
            Right-hand side on the full rectangular grid.
            Values outside the physical domain (mask = False) are ignored.

        Returns
        -------
        Float[Array, "Ny Nx"]
            Solution ψ on the full rectangular grid.  Exactly zero outside
            the mask, and ψ ≈ 0 at inner-boundary points.
        """
        ny, nx = self.shape
        # Honor the documented contract: exterior (mask = False) values are
        # ignored by zeroing them before the base solve.
        rhs_masked = rhs * self.mask
        psi_flat = self.solver(rhs_masked.reshape(ny * nx))
        return psi_flat.reshape(ny, nx) * self.mask

__call__(rhs)

Solve (∇² − λ)ψ = rhs on the masked domain.

The generic solver enforces ψ = 0 at the inner-boundary points and returns the corrected field; this method reshapes between fields and flat vectors and zeroes the exterior via the mask.

Parameters:

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

Right-hand side on the full rectangular grid. Values outside the physical domain (mask = False) are ignored.

required

Returns:

Type Description
Float[Array, 'Ny Nx']

Solution ψ on the full rectangular grid. Exactly zero outside the mask, and ψ ≈ 0 at inner-boundary points.

Source code in .venv/lib/python3.12/site-packages/spectraldiffx/_src/fourier/capacitance.py
def __call__(
    self,
    rhs: Float[Array, "Ny Nx"],
) -> Float[Array, "Ny Nx"]:
    """Solve (∇² − λ)ψ = rhs on the masked domain.

    The generic solver enforces ψ = 0 at the inner-boundary points and
    returns the corrected field; this method reshapes between fields and
    flat vectors and zeroes the exterior via the mask.

    Parameters
    ----------
    rhs : Float[Array, "Ny Nx"]
        Right-hand side on the full rectangular grid.
        Values outside the physical domain (mask = False) are ignored.

    Returns
    -------
    Float[Array, "Ny Nx"]
        Solution ψ on the full rectangular grid.  Exactly zero outside
        the mask, and ψ ≈ 0 at inner-boundary points.
    """
    ny, nx = self.shape
    # Honor the documented contract: exterior (mask = False) values are
    # ignored by zeroing them before the base solve.
    rhs_masked = rhs * self.mask
    psi_flat = self.solver(rhs_masked.reshape(ny * nx))
    return psi_flat.reshape(ny, nx) * self.mask

Factory

finitevolx.build_capacitance_solver(mask, dx, dy, lambda_=0.0, base_bc='fft')

Pre-compute the capacitance matrix and return a ready-to-use solver.

This is an offline function that performs N_b rectangular spectral solves (N_b = number of inner-boundary points). The result is a :class:CapacitanceSolver whose __call__ method is JIT-compilable.

Parameters:

Name Type Description Default
mask np.ndarray of bool shape (Ny, Nx), or Mask2D

Physical domain mask. True = interior (ocean/fluid), False = exterior (land/walls).

When an :class:Mask2D is passed, the psi staggering mask is extracted automatically.

required
dx float

Grid spacing in x.

required
dy float

Grid spacing in y.

required
lambda_ float

Helmholtz parameter λ. Use 0.0 for pure Poisson.

0.0
base_bc ('fft', 'dst', 'dct')

Rectangular spectral solver used as the base.

"fft"

Returns:

Type Description
CapacitanceSolver

A callable equinox Module with all precomputed arrays baked in.

Source code in finitevolx/_src/solvers/elliptic.py
def build_capacitance_solver(
    mask: np.ndarray | Mask2D,
    dx: float,
    dy: float,
    lambda_: float = 0.0,
    base_bc: str = "fft",
) -> CapacitanceSolver:
    """Pre-compute the capacitance matrix and return a ready-to-use solver.

    This is an **offline** function that performs *N_b* rectangular spectral
    solves (``N_b`` = number of inner-boundary points).  The result is a
    :class:`CapacitanceSolver` whose ``__call__`` method is JIT-compilable.

    Parameters
    ----------
    mask : np.ndarray of bool shape (Ny, Nx), or Mask2D
        Physical domain mask.  ``True`` = interior (ocean/fluid),
        ``False`` = exterior (land/walls).

        When an :class:`Mask2D` is passed, the ``psi``
        staggering mask is extracted automatically.
    dx : float
        Grid spacing in x.
    dy : float
        Grid spacing in y.
    lambda_ : float
        Helmholtz parameter λ.  Use ``0.0`` for pure Poisson.
    base_bc : {"fft", "dst", "dct"}
        Rectangular spectral solver used as the base.

    Returns
    -------
    CapacitanceSolver
        A callable equinox Module with all precomputed arrays baked in.
    """
    if isinstance(mask, Mask2D):
        mask = np.asarray(mask.xy_corner_strict, dtype=bool)
    return _build_capacitance_solver_base(mask, dx, dy, lambda_, base_bc)

Masked Laplacian

finitevolx.masked_laplacian(psi, mask, dx, dy, lambda_=0.0)

Apply the masked discrete Helmholtz operator (∇² − λ)·ψ.

Enforces homogeneous Dirichlet conditions at the mask boundary by zeroing psi outside the mask before applying the 5-point stencil. The output is also zeroed outside the mask.

Neighbors at the rectangle edges wrap around (periodic roll), which is consistent with using the FFT as a preconditioner.

Parameters:

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

Field to which the operator is applied.

required
mask Float[Array, 'Ny Nx'] or Mask2D

Binary mask: 1 inside the physical domain, 0 outside (land/exterior). When an :class:Mask2D is passed, the psi staggering mask is used (all four surrounding h-cells must be wet).

required
dx float

Grid spacing in x.

required
dy float

Grid spacing in y.

required
lambda_ float

Helmholtz parameter. Default: 0.0 (pure Laplacian).

0.0

Returns:

Type Description
Float[Array, 'Ny Nx']

Result of (∇² − λ)·(ψ·mask), zeroed outside the mask.

Source code in finitevolx/_src/solvers/iterative.py
def masked_laplacian(
    psi: Float[Array, "Ny Nx"],
    mask: Float[Array, "Ny Nx"] | Mask2D,
    dx: float,
    dy: float,
    lambda_: float = 0.0,
) -> Float[Array, "Ny Nx"]:
    """Apply the masked discrete Helmholtz operator (∇² − λ)·ψ.

    Enforces homogeneous Dirichlet conditions at the mask boundary by zeroing
    *psi* outside the mask before applying the 5-point stencil.  The output
    is also zeroed outside the mask.

    Neighbors at the rectangle edges wrap around (periodic roll), which is
    consistent with using the FFT as a preconditioner.

    Parameters
    ----------
    psi : Float[Array, "Ny Nx"]
        Field to which the operator is applied.
    mask : Float[Array, "Ny Nx"] or Mask2D
        Binary mask: 1 inside the physical domain, 0 outside (land/exterior).
        When an :class:`Mask2D` is passed, the ``psi`` staggering
        mask is used (all four surrounding h-cells must be wet).
    dx : float
        Grid spacing in x.
    dy : float
        Grid spacing in y.
    lambda_ : float
        Helmholtz parameter.  Default: 0.0 (pure Laplacian).

    Returns
    -------
    Float[Array, "Ny Nx"]
        Result of (∇² − λ)·(ψ·mask), zeroed outside the mask.
    """
    if isinstance(mask, Mask2D):
        mask_arr = mask.xy_corner_strict.astype(psi.dtype)
    else:
        mask_arr = mask
    psi_m = psi * mask_arr  # enforce zero outside domain
    # 5-point finite-difference stencil with periodic roll at edges
    lap = (
        jnp.roll(psi_m, 1, axis=1) + jnp.roll(psi_m, -1, axis=1) - 2.0 * psi_m
    ) / dx**2 + (
        jnp.roll(psi_m, 1, axis=0) + jnp.roll(psi_m, -1, axis=0) - 2.0 * psi_m
    ) / dy**2
    return (lap - lambda_ * psi_m) * mask_arr