Skip to content
Projects

mpifield.field

A field on the nodes of a uniform grid, split over MPI ranks, with gather and scatter.

:class:Field is a :class:simgeometry.grid.Grid holding an :class:mpiarray.DistributedArray of shape num_gridpoints + (num_components,): the grid axes are split over the ranks’ process grid with num_ghost_points halo nodes, the component axis is never split. On it:

  • :meth:Field.gather interpolates the field at marker positions and :meth:Field.scatter deposits marker values onto the nodes, both with a :class:~mpifield.weighting.Weighting (linear cloud-in-cell by default, or quadratic, or one of your own) through compiled kernels (Pyccel on the CPU, CUDA for CuPy arrays, a NumPy fallback);
  • elementwise arithmetic and NumPy ufuncs return new fields; sum, min, … reduce over all ranks;
  • :meth:Field.evaluate returns the field, or a line or plane of it, as an :class:xarray.DataArray, and :meth:Field.plot draws it with plasma-plots.

Positions are component-major, (ndim, N), and per-marker values (ncomp, N), as the kernels take them.

Classes

NameDescription
BlockGridThe grid of one rank's stored block, as the kernels see it (:meth:Field.block_grid).
FieldA field on the nodes of a uniform grid, split over MPI ranks.

BlockGridclass#

Re-exported from mpifield._typesView source
class BlockGrid(NamedTuple)

Bases: NamedTuple

The grid of one rank’s stored block, as the kernels see it (:meth:Field.block_grid).

geometry is the same grid as the kernels’ trailing arguments, Python scalars: (*lower, *cell_size, *num_nodes, *periodic), one entry per axis each (periodic as 0 or 1). index selects the nodes of the grid in :attr:Field.local_with_halos.

boundsattributeinstance attribute#

bounds: Bounds

geometryattributeinstance attribute#

geometry: tuple[float | int, ...]

grid_spacingattributeinstance attribute#

grid_spacing: Array

indexattributeinstance attribute#

index: tuple[slice, ...]

num_gridpointsattributeinstance attribute#

num_gridpoints: Array

periodicattributeinstance attribute#

periodic: tuple[bool, ...]

from_geometrymethodclassmethod#

def from_geometry(lower: tuple[float, ...], cell_size: tuple[float, ...], num_nodes: tuple[int, ...], periodic: tuple[bool, ...] | None = None) -> BlockGrid

Return the grid of a whole array of num_nodes nodes from lower on.

For running a weighting on an array that is not a field’s stored block (:meth:Weighting.scatter_block). The arguments may be sequences or arrays on either side (a :meth:Field.block_grid on the GPU holds CuPy arrays). periodic defaults to bounded axes.

Fieldclass#

class Field(ndim: int, lower_bound: tuple, upper_bound: tuple, num_gridpoints: int | tuple[int, ...], num_components: int = 1, comm: MPI.Comm | None = None, num_ghost_points: int = 0, domain_decomposition: Any = None, periodic: bool | tuple[bool, ...] | None = None, dtype: type = float)

Bases: Grid

A field on the nodes of a uniform grid, split over MPI ranks.

num_gridpoints nodes (one count per axis, or one for all) span lower_bound to upper_bound on a bounded axis; on a periodic axis the last node neighbours the first, so there are num_gridpoints cells over the length. comm and domain_decomposition split the grid axes over the ranks as :class:simgeometry.grid.Grid does; each rank stores its block with num_ghost_points halo nodes on every side (:attr:local_with_halos). Without comm every rank holds the whole field.

Examples:

phi = Field(ndim=2, lower_bound=(0, 0), upper_bound=(1, 1), num_gridpoints=33) phi.set_field_data(np.zeros((33, 33))) values = phi.gather(np.array([[0.5], [0.25]])) # (1, 1)

distributedproperty#

distributed: bool

Whether the ranks hold different parts of the grid.

dtypeproperty#

dtype: Any

Return the data type.

field_dataproperty#

field_data: DistributedArray

Return the distributed array holding the field.

localproperty#

local: Array

This rank’s nodes, without halo nodes: a writable view.

local_with_halosproperty#

local_with_halos: Array

This rank’s stored block, halo nodes included: a writable view.

This is the array the kernels read and write, on the grid of :meth:block_grid.

num_componentsproperty#

num_components: int

Return the number of field components.

num_ghost_pointsproperty#

num_ghost_points: int

Return the number of halo nodes on each side of a rank’s block.

astypemethod#

def astype(dtype: type, copy: bool = True) -> Self

Return a field with data converted to dtype.

block_gridmethod#

def block_grid(cpu: bool = False, inside_domain: bool = False) -> BlockGrid

The grid of this rank’s stored block (halo nodes included), for the kernels.

Under a spatial decomposition the kernels deposit into and interpolate from :attr:local_with_halos, a block of the global grid that starts halo nodes before the block’s first node. They see it as a grid of its own: its lower bound is that node, its node count the stored extent. A periodic axis is periodic for the kernels only without halo nodes; with them the halo nodes hold the wrapped-around nodes (:meth:fill_halos, :meth:exchange_halos). cpu returns host arrays, for the NumPy and Pyccel kernels. Cached per layout.

With inside_domain the grid leaves out the halo nodes beyond a bounded edge of the domain, which no exchange fills or collects: the kernels then clamp there as on an unsplit field, and a stencil wider than a cell neither deposits into nor reads from nodes outside the domain. This is the grid of :meth:scatter and :meth:gather. local_with_halos[grid.index] is the part of the stored block it describes; without inside_domain that is all of it.

componentmethod#

def component(component: int) -> DistributedArray

Return one component as a distributed array sharing this field’s storage.

copymethod#

def copy(copy_data: bool = True) -> Self

Return a field on the same grid, with a copy of the data (or zeros).

crossmethod#

def cross(other: Self) -> Self

Return the pointwise cross product of two 3-component fields.

dotmethod#

def dot(other: Self) -> Self

Return the pointwise dot product as a scalar field.

evaluatemethod#

def evaluate(x: Any = None, y: Any = None, z: Any = None, component: int | None = None, name: str | None = None, units: str | None = None) -> xr.DataArray

Evaluate the field as an xarray.DataArray with x, y, z dims.

Each axis takes a scalar, a list, a one-dimensional array or a range in physical coordinates. A scalar fixes that axis and drops its dimension (it stays as a scalar coordinate); a sequence becomes a dimension, and several sequences form their tensor-product mesh. An omitted axis keeps the grid nodes, so field.evaluate() is the field data itself, without interpolation. For scattered points, pass xarray.DataArray coordinates that share a dimension, e.g. dims="points". Values between nodes are interpolated linearly; periodic axes wrap around. Coordinates outside the domain raise ValueError.

A field with several components has a component dimension, unless one is chosen with component=; a single-component field has none. name and units label the result (units goes to attrs["units"], which plots show on the axis or colorbar).

Under MPI this gathers the global field, so call it on every rank.

Examples:

phi.evaluate() # (x, y) on the grid nodes phi.evaluate(y=0.0) # a line profile along x phi.evaluate(x=np.linspace(0, 1, 200), y=[0.25, 0.5]) E.evaluate(z=0.1, component=0)

exchange_halosmethod#

def exchange_halos() -> None

Add the halo nodes to their owners on the neighbouring ranks and clear them.

Call it after a deposit (:meth:scatter) into a distributed field.

exportmethod#

def export(file_path: PathLikeStr) -> None

Write the field to a NetCDF file (.nc), through xarray.

The file holds the node values (x, y, z and, for several components, component), the bounds and the periodic flags; the split over ranks is not stored. Collective under MPI: rank 0 writes.

fill_halosmethod#

def fill_halos() -> None

Fill the halo nodes from the neighbouring ranks (update_halos).

from_arraymethodclassmethod#

def from_array(data: Array, lower_bound: tuple, upper_bound: tuple, comm: MPI.Comm | None = None, num_ghost_points: int = 0, domain_decomposition: Any = None, periodic: bool | tuple[bool, ...] | None = None, dtype: type | None = None) -> Self

Create a field from global scalar (num_gridpoints) or component-last data.

gathermethod#

def gather(positions: Array | float, weighting: Weighting = LINEAR) -> Array

Interpolate the field at marker positions with weighting.

positions has shape (ndim, N) (or is one point as a 1-D array of length ndim); the result has shape (num_components, N). With the built-in weightings a position outside the grid uses its edge nodes; on a periodic axis without halos it wraps around. Under MPI each rank interpolates from its own block, so its markers must lie in it, and the halo nodes must be filled (:meth:fill_halos) for markers near its edge.

get_field_datamethod#

def get_field_data(coordinate: tuple[int, ...] | int | None = None, component: int | None = None, gathered: bool = False) -> Array

Return the stored block (or the gathered field), optionally one node or component.

coordinate indexes the stored block (halo nodes included), or the global field with gathered=True; gathering is collective.

get_valuemethod#

def get_value(index: IndexLike, component: int = 0) -> Any

Return the node at global index on every rank (an allreduce: collective).

gradientmethod#

def gradient() -> Self

Return the gradient of a scalar field: a field with ndim components.

Second-order central differences inside the domain and across a periodic seam, first-order one-sided differences at the ends of a bounded axis (as :func:numpy.gradient). A distributed field is gathered first (collective), so the result does not depend on the split.

loadmethodclassmethod#

def load(file_path: PathLikeStr, comm: MPI.Comm | None = None, num_ghost_points: int = 0, domain_decomposition: Any = None) -> Self

Read a field written by :meth:export, split as the arguments say.

magnitudemethod#

def magnitude() -> Self

Return the pointwise vector magnitude as a scalar field.

maxmethod#

def max(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> Any

Return the maximum over all ranks, optionally for one component.

meanmethod#

def mean(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> Any

Return the mean over all ranks, optionally for one component.

minmethod#

def min(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> Any

Return the minimum over all ranks, optionally for one component.

on_gridmethodclassmethod#

def on_grid(grid: Grid, num_components: int = 1, split: bool = True) -> Self

Return a zero field on the nodes of grid, split as grid is.

On a spatially decomposed grid each rank holds its own nodes plus one halo node on every side, which the deposit accumulates into and the interpolation reads from (:meth:block_grid); otherwise, or with split=False, every rank holds the whole grid.

plotmethod#

def plot(ax: Any = None, x: Any = None, y: Any = None, z: Any = None, component: int | None = None, name: str | None = None, units: str | None = None, **kwargs: Any) -> Any

Plot the field with plasma-plots and return its PlotResult.

x, y, z, component, name and units are as in :meth:evaluate. A one-dimensional result is drawn as a line, a two-dimensional one as a colour map. A 3D field with no axis fixed is shown on its z midplane. Other keyword arguments (cmap, vmin, title, backend, …) go to plasma_plots. Needs the plot extra.

Under MPI, call it on every rank: the field is gathered everywhere.

Examples:

phi.plot() phi.plot(y=0.5) # line profile E.plot(ax=ax, component=1, cmap=“RdBu_r”, symmetric=True)

scattermethod#

def scatter(positions: Array | float, values: Array | float, weighting: Weighting = LINEAR) -> None

Add marker values to the nodes with weighting (linear by default).

positions has shape (ndim, N) and values (ncomp, N); a 1-D values of length N is one component, a number is the same value for every marker. The values are added to the stored block, halo nodes included; under MPI call :meth:exchange_halos afterwards. A marker with a zero value contributes nothing, whatever its position.

set_field_datamethod#

def set_field_data(data: Array | float | DistributedArray, component: int | None = None) -> None

Set the field from a scalar, a global array or a distributed array.

  • a scalar sets every stored node (of one component, or all);
  • a global array of the field’s shape num_gridpoints + (ncomp,), or of num_gridpoints for one component (or a scalar field): each rank copies its own nodes and the halos are zeroed;
  • a :class:~mpiarray.DistributedArray with the same split (the field’s shape, or the grid’s for one component) is copied block by block, halo nodes included, without communication;
  • an array of the stored block’s shape (:attr:local_with_halos) is copied as is.

set_insidemethod#

def set_inside(shape: Shape, value: float, component: int = 0) -> None

Set the stored nodes inside shape (a simgeometry shape) to value.

Every rank sets its own block, halo nodes included, from the shape’s vectorised contains_points; no communication.

set_valuemethod#

def set_value(index: IndexLike, value: float, component: int = 0) -> None

Set the node at global index; ranks that do not own it do nothing.

smoothmethod#

def smooth(filter_name: Literal['binomial', 'gaussian', 'uniform'] = 'binomial', passes: int = 1, component: int | None = None, in_place: bool = True) -> Self

Smooth the field with a separable low-pass filter.

Smoothing is applied independently per component and per axis. For periodic axes, values wrap around. For non-periodic axes, out-of-domain values are treated as zero. Under MPI it exchanges halos of the filter’s width when every rank’s block is at least that wide, and gathers otherwise.

smooth_binomialmethod#

def smooth_binomial(passes: int = 1, component: int | None = None, in_place: bool = True) -> Self

Smooth the field with the 3-point binomial kernel [1, 2, 1] / 4.

smooth_gaussianmethod#

def smooth_gaussian(passes: int = 1, component: int | None = None, in_place: bool = True) -> Self

Smooth the field with the 5-point Gaussian-like kernel [1, 4, 6, 4, 1] / 16.

smooth_uniformmethod#

def smooth_uniform(passes: int = 1, component: int | None = None, in_place: bool = True) -> Self

Smooth the field with a 3-point moving-average kernel.

summethod#

def sum(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> Any

Sum the field over all ranks, optionally for one component.

to_cupymethod#

def to_cupy() -> Array

Return the gathered field data on the active cunumpy backend (collective).

to_ndarraymethod#

def to_ndarray() -> Array

Return the global field data, num_gridpoints + (ncomp,) (collective).

to_numpymethod#

def to_numpy() -> np.ndarray

Return the gathered field data as a numpy.ndarray (collective).