Skip to content

Grad-Shafranov

These helpers provide the axisymmetric finite-difference flux solve path.

Key preconditions:

  • flux_solver() and solve_flux_axisymmetric() require strictly increasing, regular r and z grids with r > 0 and at least 7 points on each axis.
  • solve_flux_axisymmetric() expects meshes = np.meshgrid(*grids, indexing="ij") and current_density on the same (nr, nz) layout, with zero current on the finite-difference boundary.
  • gradient_order4() and calc_flux_density_from_flux() require at least 6 points on each axis, and calc_flux_density_from_flux() also requires rmesh > 0.

cfsem.gs_operator_order2

gs_operator_order2(
    rs: NDArray[float64], zs: NDArray[float64]
) -> Array3xN

Build second-order Grad-Shafranov operator in triplet format. Assumes regular grid spacing.

Parameters:

Name Type Description Default
rs NDArray[float64]

[m] r-coordinates of finite difference grid

required
zs NDArray[float64]

[m] z-coordinates of finite difference grid

required

Returns:

Type Description
Array3xN

Differential operator as triplet format sparse matrix

Source code in cfsem/bindings.py
def gs_operator_order2(rs: NDArray[float64], zs: NDArray[float64]) -> Array3xN:
    """Build second-order Grad-Shafranov operator in triplet format.
    Assumes regular grid spacing.

    Args:
        rs: [m] r-coordinates of finite difference grid
        zs: [m] z-coordinates of finite difference grid

    Returns:
        Differential operator as triplet format sparse matrix
    """
    rs, zs = _2tup_contig((rs, zs))
    return em_gs_operator_order2(rs, zs)

cfsem.gs_operator_order4

gs_operator_order4(
    rs: NDArray[float64], zs: NDArray[float64]
) -> Array3xN

Build fourth-order Grad-Shafranov operator in triplet format. Assumes regular grid spacing.

Parameters:

Name Type Description Default
rs NDArray[float64]

[m] r-coordinates of finite difference grid

required
zs NDArray[float64]

[m] z-coordinates of finite difference grid

required

Returns:

Type Description
Array3xN

Differential operator as triplet format sparse matrix

Source code in cfsem/bindings.py
def gs_operator_order4(rs: NDArray[float64], zs: NDArray[float64]) -> Array3xN:
    """
    Build fourth-order Grad-Shafranov operator in triplet format.
    Assumes regular grid spacing.

    Args:
        rs: [m] r-coordinates of finite difference grid
        zs: [m] z-coordinates of finite difference grid

    Returns:
        Differential operator as triplet format sparse matrix
    """
    rs, zs = _2tup_contig((rs, zs))
    return em_gs_operator_order4(rs, zs)

cfsem.gradient_order4

gradient_order4(
    z: NDArray, xmesh: NDArray, ymesh: NDArray
) -> tuple[NDArray, NDArray]

Calculate gradient by 4th-order finite difference.

numpy.gradient exists and is fast and convenient, but only uses a second-order difference, which produces unacceptable error in B-fields (well over 1% for typical geometries).

Errors
* If the input grids are not regular
* If any input grid dimensions have size less than 6
References
* [1] “Finite difference coefficient,” Wikipedia. Aug. 22, 2023.
      Accessed: Mar. 29, 2024. [Online].
      Available: https://en.wikipedia.org/w/index.php?title=Finite_difference_coefficient

Parameters:

Name Type Description Default
z NDArray

[] 2D array of values on which to calculate the gradient

required
xmesh NDArray

[m] 2D array of coordinates of first dimension

required
ymesh NDArray

[m] 2D array of coordinates of second dimension

required

Returns:

Type Description
tuple[NDArray, NDArray]

(dzdx, dzdy) [/m] 2D arrays of gradient components

Source code in cfsem/flux_solver.py
def gradient_order4(z: NDArray, xmesh: NDArray, ymesh: NDArray) -> tuple[NDArray, NDArray]:
    """
    Calculate gradient by 4th-order finite difference.

    `numpy.gradient` exists and is fast and convenient, but only uses a second-order difference,
    which produces unacceptable error in B-fields (well over 1% for typical geometries).

    ## Errors

        * If the input grids are not regular
        * If any input grid dimensions have size less than 6

    ## References

        * [1] “Finite difference coefficient,” Wikipedia. Aug. 22, 2023.
              Accessed: Mar. 29, 2024. [Online].
              Available: https://en.wikipedia.org/w/index.php?title=Finite_difference_coefficient

    Args:
        z: [<xunits>] 2D array of values on which to calculate the gradient
        xmesh: [m] 2D array of coordinates of first dimension
        ymesh: [m] 2D array of coordinates of second dimension

    Returns:
        (dzdx, dzdy) [<xunits>/m] 2D arrays of gradient components
    """
    nx, ny = z.shape
    assert nx >= 6 and ny >= 6, "gradient_order4 requires each grid dimension to have at least 6 points"
    dx = xmesh[1][0] - xmesh[0][0]
    dy = ymesh[0][1] - ymesh[0][0]

    # Check regular grid assumption
    assert np.all(
        np.abs(np.diff(xmesh[:, 0]) - dx) / dx < 1e-6
    ), "This method is only implemented for a regular grid"
    assert np.all(
        np.abs(np.diff(ymesh[0, :]) - dy) / dy < 1e-6
    ), "This method is only implemented for a regular grid"

    accumulator_dtype = np.result_type(z, np.float64)
    dzdx = np.zeros(z.shape, dtype=accumulator_dtype)
    for offs, w in _DDX_CENTRAL_ORDER4:
        start = int(2 + offs)
        end = int(nx - 2 + offs)
        dzdx[2:-2, :] += w * z[start:end, :] / dx  # Central difference on interior points
    left_rows = np.arange(2)
    for offs, w in _DDX_FWD_ORDER4:
        dzdx[0:2, :] += w * z[left_rows + int(offs), :] / dx  # One-sided difference on left side
    right_rows = np.arange(nx - 2, nx)
    for offs, w in _DDX_BWD_ORDER4:
        dzdx[-2:, :] += w * z[right_rows + int(offs), :] / dx  # One-sided difference on right side

    dzdy = np.zeros(z.shape, dtype=accumulator_dtype)
    for offs, w in _DDX_CENTRAL_ORDER4:
        start = int(2 + offs)
        end = int(ny - 2 + offs)
        dzdy[:, 2:-2] += w * z[:, start:end] / dy  # Interior points
    bottom_cols = np.arange(2)
    for offs, w in _DDX_FWD_ORDER4:
        dzdy[:, 0:2] += w * z[:, bottom_cols + int(offs)] / dy  # One-sided difference on bottom
    top_cols = np.arange(ny - 2, ny)
    for offs, w in _DDX_BWD_ORDER4:
        dzdy[:, -2:] += w * z[:, top_cols + int(offs)] / dy  # One-sided difference on top

    return dzdx, dzdy

cfsem.calc_flux_density_from_flux

calc_flux_density_from_flux(
    psi: NDArray, rmesh: NDArray, zmesh: NDArray
) -> tuple[NDArray, NDArray]

Back-calculate B-field from poloidal flux per Wesson eqn 3.2.2 by 4th-order finite difference, modified to use total poloidal flux instead of flux per radian.

This avoids an expensive sum over filamentized contributions at the expense of some numerical error.

Errors

* If the input grids are not regular
* If any input grid dimensions have size less than 6
* If `rmesh` contains non-positive radii

References

* [1] J. Wesson, Tokamaks. Oxford, New York: Clarendon Press, 1987.

Parameters:

Name Type Description Default
psi NDArray

[Wb] poloidal flux

required
rmesh NDArray

[m] 2D r-coordinates

required
zmesh NDArray

[m] 2D z-coordinates

required

Returns:

Type Description
tuple[NDArray, NDArray]

(br, bz) [T] 2D arrays of poloidal flux density

Source code in cfsem/flux_solver.py
def calc_flux_density_from_flux(psi: NDArray, rmesh: NDArray, zmesh: NDArray) -> tuple[NDArray, NDArray]:
    """
    Back-calculate B-field from poloidal flux per Wesson eqn 3.2.2 by 4th-order finite difference,
    modified to use total poloidal flux instead of flux per radian.

    This avoids an expensive sum over filamentized contributions at the expense of some numerical error.

    # Errors

        * If the input grids are not regular
        * If any input grid dimensions have size less than 6
        * If `rmesh` contains non-positive radii

    # References

        * [1] J. Wesson, Tokamaks. Oxford, New York: Clarendon Press, 1987.

    Args:
        psi: [Wb] poloidal flux
        rmesh: [m] 2D r-coordinates
        zmesh: [m] 2D z-coordinates

    Returns:
        (br, bz) [T] 2D arrays of poloidal flux density
    """
    assert not np.any(rmesh <= 0.0), "rmesh must be strictly positive"

    dpsidr, dpsidz = gradient_order4(psi, rmesh, zmesh)

    r_inv = rmesh**-1

    br = -r_inv * dpsidz / (2.0 * np.pi)  # [T]
    bz = r_inv * dpsidr / (2.0 * np.pi)  # [T]

    return (br, bz)

cfsem.flux_solver

Axisymmetric finite-difference flux solve helpers.

calc_flux_density_from_flux

calc_flux_density_from_flux(
    psi: NDArray, rmesh: NDArray, zmesh: NDArray
) -> tuple[NDArray, NDArray]

Back-calculate B-field from poloidal flux per Wesson eqn 3.2.2 by 4th-order finite difference, modified to use total poloidal flux instead of flux per radian.

This avoids an expensive sum over filamentized contributions at the expense of some numerical error.

Errors
* If the input grids are not regular
* If any input grid dimensions have size less than 6
* If `rmesh` contains non-positive radii
References
* [1] J. Wesson, Tokamaks. Oxford, New York: Clarendon Press, 1987.

Parameters:

Name Type Description Default
psi NDArray

[Wb] poloidal flux

required
rmesh NDArray

[m] 2D r-coordinates

required
zmesh NDArray

[m] 2D z-coordinates

required

Returns:

Type Description
tuple[NDArray, NDArray]

(br, bz) [T] 2D arrays of poloidal flux density

Source code in cfsem/flux_solver.py
def calc_flux_density_from_flux(psi: NDArray, rmesh: NDArray, zmesh: NDArray) -> tuple[NDArray, NDArray]:
    """
    Back-calculate B-field from poloidal flux per Wesson eqn 3.2.2 by 4th-order finite difference,
    modified to use total poloidal flux instead of flux per radian.

    This avoids an expensive sum over filamentized contributions at the expense of some numerical error.

    # Errors

        * If the input grids are not regular
        * If any input grid dimensions have size less than 6
        * If `rmesh` contains non-positive radii

    # References

        * [1] J. Wesson, Tokamaks. Oxford, New York: Clarendon Press, 1987.

    Args:
        psi: [Wb] poloidal flux
        rmesh: [m] 2D r-coordinates
        zmesh: [m] 2D z-coordinates

    Returns:
        (br, bz) [T] 2D arrays of poloidal flux density
    """
    assert not np.any(rmesh <= 0.0), "rmesh must be strictly positive"

    dpsidr, dpsidz = gradient_order4(psi, rmesh, zmesh)

    r_inv = rmesh**-1

    br = -r_inv * dpsidz / (2.0 * np.pi)  # [T]
    bz = r_inv * dpsidr / (2.0 * np.pi)  # [T]

    return (br, bz)

flux_solver

flux_solver(
    grids: tuple[NDArray, NDArray],
) -> Callable[[NDArray], NDArray]

Linear solver for extracting a flux field from a toroidal current density distribution using a 4th-order finite difference approximation of the Grad-Shafranov PDE. For jtor toroidal current density shaped like (nr, nz), call like psi = flux_solver(rhs) to get psi in [Wb] or [V-s], where rhs = -2.0 * np.pi * mu_0 * rmesh * jtor with the boundary values set to the circular-filament solved flux.

Parameters:

Name Type Description Default
grids tuple[NDArray, NDArray]

[m] regular 1D r,z grids. The fourth-order Grad-Shafranov stencil requires strictly increasing grids with positive radius and at least 7 points on each axis.

required

Returns:

Name Type Description
solver Callable[[NDArray], NDArray]

factorized solver for Grad-Shafranov differential operator

Source code in cfsem/flux_solver.py
def flux_solver(grids: tuple[NDArray, NDArray]) -> Callable[[NDArray], NDArray]:
    """
    Linear solver for extracting a flux field from a toroidal current density distribution
    using a 4th-order finite difference approximation of the Grad-Shafranov PDE.
    For `jtor` toroidal current density shaped like (nr, nz), call like `psi = flux_solver(rhs)`
    to get `psi` in [Wb] or [V-s], where `rhs = -2.0 * np.pi * mu_0 * rmesh * jtor` with the boundary
    values set to the circular-filament solved flux.

    Args:
        grids: [m] regular 1D r,z grids. The fourth-order Grad-Shafranov stencil
            requires strictly increasing grids with positive radius and at least
            7 points on each axis.

    Returns:
        solver: factorized solver for Grad-Shafranov differential operator
    """
    # Build Grad-Shafranov Delta* linear operator for finite difference
    # as a sparse matrix
    _ = _check_regular(grids, min_points=7)
    rgrid, zgrid = grids
    nr = rgrid.size
    nz = zgrid.size
    vals, rows, cols = gs_operator_order4(*grids)
    operator = csc_matrix((vals, (rows, cols)), shape=(nr * nz, nr * nz))
    # Store LU factorization of operator matrix to allow fast, reusable
    # solves using different right-hand-side (different current density)
    return factorized(operator)

gradient_order4

gradient_order4(
    z: NDArray, xmesh: NDArray, ymesh: NDArray
) -> tuple[NDArray, NDArray]

Calculate gradient by 4th-order finite difference.

numpy.gradient exists and is fast and convenient, but only uses a second-order difference, which produces unacceptable error in B-fields (well over 1% for typical geometries).

Errors
* If the input grids are not regular
* If any input grid dimensions have size less than 6
References
* [1] “Finite difference coefficient,” Wikipedia. Aug. 22, 2023.
      Accessed: Mar. 29, 2024. [Online].
      Available: https://en.wikipedia.org/w/index.php?title=Finite_difference_coefficient

Parameters:

Name Type Description Default
z NDArray

[] 2D array of values on which to calculate the gradient

required
xmesh NDArray

[m] 2D array of coordinates of first dimension

required
ymesh NDArray

[m] 2D array of coordinates of second dimension

required

Returns:

Type Description
tuple[NDArray, NDArray]

(dzdx, dzdy) [/m] 2D arrays of gradient components

Source code in cfsem/flux_solver.py
def gradient_order4(z: NDArray, xmesh: NDArray, ymesh: NDArray) -> tuple[NDArray, NDArray]:
    """
    Calculate gradient by 4th-order finite difference.

    `numpy.gradient` exists and is fast and convenient, but only uses a second-order difference,
    which produces unacceptable error in B-fields (well over 1% for typical geometries).

    ## Errors

        * If the input grids are not regular
        * If any input grid dimensions have size less than 6

    ## References

        * [1] “Finite difference coefficient,” Wikipedia. Aug. 22, 2023.
              Accessed: Mar. 29, 2024. [Online].
              Available: https://en.wikipedia.org/w/index.php?title=Finite_difference_coefficient

    Args:
        z: [<xunits>] 2D array of values on which to calculate the gradient
        xmesh: [m] 2D array of coordinates of first dimension
        ymesh: [m] 2D array of coordinates of second dimension

    Returns:
        (dzdx, dzdy) [<xunits>/m] 2D arrays of gradient components
    """
    nx, ny = z.shape
    assert nx >= 6 and ny >= 6, "gradient_order4 requires each grid dimension to have at least 6 points"
    dx = xmesh[1][0] - xmesh[0][0]
    dy = ymesh[0][1] - ymesh[0][0]

    # Check regular grid assumption
    assert np.all(
        np.abs(np.diff(xmesh[:, 0]) - dx) / dx < 1e-6
    ), "This method is only implemented for a regular grid"
    assert np.all(
        np.abs(np.diff(ymesh[0, :]) - dy) / dy < 1e-6
    ), "This method is only implemented for a regular grid"

    accumulator_dtype = np.result_type(z, np.float64)
    dzdx = np.zeros(z.shape, dtype=accumulator_dtype)
    for offs, w in _DDX_CENTRAL_ORDER4:
        start = int(2 + offs)
        end = int(nx - 2 + offs)
        dzdx[2:-2, :] += w * z[start:end, :] / dx  # Central difference on interior points
    left_rows = np.arange(2)
    for offs, w in _DDX_FWD_ORDER4:
        dzdx[0:2, :] += w * z[left_rows + int(offs), :] / dx  # One-sided difference on left side
    right_rows = np.arange(nx - 2, nx)
    for offs, w in _DDX_BWD_ORDER4:
        dzdx[-2:, :] += w * z[right_rows + int(offs), :] / dx  # One-sided difference on right side

    dzdy = np.zeros(z.shape, dtype=accumulator_dtype)
    for offs, w in _DDX_CENTRAL_ORDER4:
        start = int(2 + offs)
        end = int(ny - 2 + offs)
        dzdy[:, 2:-2] += w * z[:, start:end] / dy  # Interior points
    bottom_cols = np.arange(2)
    for offs, w in _DDX_FWD_ORDER4:
        dzdy[:, 0:2] += w * z[:, bottom_cols + int(offs)] / dy  # One-sided difference on bottom
    top_cols = np.arange(ny - 2, ny)
    for offs, w in _DDX_BWD_ORDER4:
        dzdy[:, -2:] += w * z[:, top_cols + int(offs)] / dy  # One-sided difference on top

    return dzdx, dzdy

solve_flux_axisymmetric

solve_flux_axisymmetric(
    grids: tuple[NDArray, NDArray],
    meshes: tuple[NDArray, NDArray],
    current_density: NDArray,
    solver: Callable[[NDArray], NDArray] | None = None,
) -> NDArray

Calculate the flux field associated with a given toroidal current density distribution, by solving the Grad-Shafranov PDE.

This calculation is most commonly used for the plasma, but is in fact more general, and applies to anything with an equivalent toroidal current density and axisymmetry.

Parameters:

Name Type Description Default
grids tuple[NDArray, NDArray]

[m] 1D r,z regular coordinate grids. The fourth-order solve requires strictly increasing grids with positive radius and at least 7 points on each axis.

required
meshes tuple[NDArray, NDArray]

[m] 2D meshgrids made from grids like np.meshgrid(*grids, indexing="ij")

required
current_density NDArray

[A/m^2], shape (nr, nz), toroidal current density on finite-difference mesh with zero values on the finite-difference boundary

required
solver Callable[[NDArray], NDArray] | None

Optionally, provide a pre-initialized linear solver. See cfsem.flux_solver.

None

Returns:

Type Description
NDArray

poloidal flux field, [Wb] with shape (nr, nz)

Source code in cfsem/flux_solver.py
def solve_flux_axisymmetric(
    grids: tuple[NDArray, NDArray],
    meshes: tuple[NDArray, NDArray],
    current_density: NDArray,
    solver: Callable[[NDArray], NDArray] | None = None,
) -> NDArray:
    """
    Calculate the flux field associated with a given toroidal current density distribution,
    by solving the Grad-Shafranov PDE.

    This calculation is most commonly used for the plasma, but is in fact more general,
    and applies to anything with an equivalent toroidal current density and axisymmetry.

    Args:
        grids: [m] 1D r,z regular coordinate grids. The fourth-order solve requires
            strictly increasing grids with positive radius and at least 7 points
            on each axis.
        meshes: [m] 2D meshgrids made from grids like np.meshgrid(*grids, indexing="ij")
        current_density: [A/m^2], shape (nr, nz), toroidal current density on finite-difference mesh
            with zero values on the finite-difference boundary
        solver: Optionally, provide a pre-initialized linear solver. See `cfsem.flux_solver`.

    Returns:
        poloidal flux field, [Wb] with shape (nr, nz)
    """
    _ = _check_regular(grids, min_points=7)
    _validate_flux_mesh_inputs(grids, meshes, current_density)

    # Build the differential operator, if needed
    solver = solver or flux_solver(grids)

    # Unpack and filter down to just useful inputs
    dr, dz = _check_regular(grids, min_points=7)  # [m] grid spacing
    area = dr * dz  # [m^2]
    rmesh, zmesh = meshes  # [m]
    assert not (
        np.any(current_density[0, :] != 0.0)
        or np.any(current_density[-1, :] != 0.0)
        or np.any(current_density[:, 0] != 0.0)
        or np.any(current_density[:, -1] != 0.0)
    ), "current_density must be zero on the finite-difference boundary"
    nonzero_inds = np.where(current_density != 0.0)
    current_density_nonzero = np.ascontiguousarray(current_density[nonzero_inds])  # [A/m^2]
    rmesh_nonzero = np.ascontiguousarray(rmesh[nonzero_inds])  # [m]
    zmesh_nonzero = np.ascontiguousarray(zmesh[nonzero_inds])  # [m]
    # Solve `Delta* @ psi = -mu_0 * 2pi * rmesh * jtor`
    #   Set up right-hand-side of Grad-Shafranov
    rhs = -(2.0 * np.pi * mu_0) * rmesh * current_density  # [Wb/m^2]
    #   Set flux boundary condition
    #   For most relevant grid sizes (up to 500 X 500), doing the O(N^3)
    #   circular-filament flux calc is faster than the linear solve
    #   and therefore faster than doing an extra fixed-boundary linear solve
    #   in order to use Von Hagenow's asymptotically-O(N^2logN) method.
    ifil = (area * current_density_nonzero).flatten()  # [A] plasma filament current
    rfil = rmesh_nonzero.flatten()
    zfil = zmesh_nonzero.flatten()
    for s in [[0, ...], [-1, ...], [..., 0], [..., -1]]:  # All boundary slices
        rhs[s[0], s[1]] = flux_circular_filament(ifil, rfil, zfil, rmesh[s[0], s[1]], zmesh[s[0], s[1]])
    #   Do the actual linear solve
    psi = solver(rhs.flatten()).reshape(rmesh.shape)  # [Wb]

    return psi

cfsem.solve_flux_axisymmetric

solve_flux_axisymmetric(
    grids: tuple[NDArray, NDArray],
    meshes: tuple[NDArray, NDArray],
    current_density: NDArray,
    solver: Callable[[NDArray], NDArray] | None = None,
) -> NDArray

Calculate the flux field associated with a given toroidal current density distribution, by solving the Grad-Shafranov PDE.

This calculation is most commonly used for the plasma, but is in fact more general, and applies to anything with an equivalent toroidal current density and axisymmetry.

Parameters:

Name Type Description Default
grids tuple[NDArray, NDArray]

[m] 1D r,z regular coordinate grids. The fourth-order solve requires strictly increasing grids with positive radius and at least 7 points on each axis.

required
meshes tuple[NDArray, NDArray]

[m] 2D meshgrids made from grids like np.meshgrid(*grids, indexing="ij")

required
current_density NDArray

[A/m^2], shape (nr, nz), toroidal current density on finite-difference mesh with zero values on the finite-difference boundary

required
solver Callable[[NDArray], NDArray] | None

Optionally, provide a pre-initialized linear solver. See cfsem.flux_solver.

None

Returns:

Type Description
NDArray

poloidal flux field, [Wb] with shape (nr, nz)

Source code in cfsem/flux_solver.py
def solve_flux_axisymmetric(
    grids: tuple[NDArray, NDArray],
    meshes: tuple[NDArray, NDArray],
    current_density: NDArray,
    solver: Callable[[NDArray], NDArray] | None = None,
) -> NDArray:
    """
    Calculate the flux field associated with a given toroidal current density distribution,
    by solving the Grad-Shafranov PDE.

    This calculation is most commonly used for the plasma, but is in fact more general,
    and applies to anything with an equivalent toroidal current density and axisymmetry.

    Args:
        grids: [m] 1D r,z regular coordinate grids. The fourth-order solve requires
            strictly increasing grids with positive radius and at least 7 points
            on each axis.
        meshes: [m] 2D meshgrids made from grids like np.meshgrid(*grids, indexing="ij")
        current_density: [A/m^2], shape (nr, nz), toroidal current density on finite-difference mesh
            with zero values on the finite-difference boundary
        solver: Optionally, provide a pre-initialized linear solver. See `cfsem.flux_solver`.

    Returns:
        poloidal flux field, [Wb] with shape (nr, nz)
    """
    _ = _check_regular(grids, min_points=7)
    _validate_flux_mesh_inputs(grids, meshes, current_density)

    # Build the differential operator, if needed
    solver = solver or flux_solver(grids)

    # Unpack and filter down to just useful inputs
    dr, dz = _check_regular(grids, min_points=7)  # [m] grid spacing
    area = dr * dz  # [m^2]
    rmesh, zmesh = meshes  # [m]
    assert not (
        np.any(current_density[0, :] != 0.0)
        or np.any(current_density[-1, :] != 0.0)
        or np.any(current_density[:, 0] != 0.0)
        or np.any(current_density[:, -1] != 0.0)
    ), "current_density must be zero on the finite-difference boundary"
    nonzero_inds = np.where(current_density != 0.0)
    current_density_nonzero = np.ascontiguousarray(current_density[nonzero_inds])  # [A/m^2]
    rmesh_nonzero = np.ascontiguousarray(rmesh[nonzero_inds])  # [m]
    zmesh_nonzero = np.ascontiguousarray(zmesh[nonzero_inds])  # [m]
    # Solve `Delta* @ psi = -mu_0 * 2pi * rmesh * jtor`
    #   Set up right-hand-side of Grad-Shafranov
    rhs = -(2.0 * np.pi * mu_0) * rmesh * current_density  # [Wb/m^2]
    #   Set flux boundary condition
    #   For most relevant grid sizes (up to 500 X 500), doing the O(N^3)
    #   circular-filament flux calc is faster than the linear solve
    #   and therefore faster than doing an extra fixed-boundary linear solve
    #   in order to use Von Hagenow's asymptotically-O(N^2logN) method.
    ifil = (area * current_density_nonzero).flatten()  # [A] plasma filament current
    rfil = rmesh_nonzero.flatten()
    zfil = zmesh_nonzero.flatten()
    for s in [[0, ...], [-1, ...], [..., 0], [..., -1]]:  # All boundary slices
        rhs[s[0], s[1]] = flux_circular_filament(ifil, rfil, zfil, rmesh[s[0], s[1]], zmesh[s[0], s[1]])
    #   Do the actual linear solve
    psi = solver(rhs.flatten()).reshape(rmesh.shape)  # [Wb]

    return psi