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.gatherinterpolates the field at marker positions and :meth:Field.scatterdeposits 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.evaluatereturns the field, or a line or plane of it, as an :class:xarray.DataArray, and :meth:Field.plotdraws it with plasma-plots.
Positions are component-major, (ndim, N), and per-marker values (ncomp, N),
as the kernels take them.
BlockGridclass#
mpifield._typesView sourceclass 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: Boundsgeometryattributeinstance attribute#
geometry: tuple[float | int, ...]grid_spacingattributeinstance attribute#
grid_spacing: Arrayindexattributeinstance attribute#
index: tuple[slice, ...]num_gridpointsattributeinstance attribute#
num_gridpoints: Arrayperiodicattributeinstance 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) -> BlockGridReturn 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: boolWhether the ranks hold different parts of the grid.
dtypeproperty#
dtype: AnyReturn the data type.
field_dataproperty#
field_data: DistributedArrayReturn the distributed array holding the field.
localproperty#
local: ArrayThis rank’s nodes, without halo nodes: a writable view.
local_with_halosproperty#
local_with_halos: ArrayThis 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: intReturn the number of field components.
num_ghost_pointsproperty#
num_ghost_points: intReturn the number of halo nodes on each side of a rank’s block.
astypemethod#
def astype(dtype: type, copy: bool = True) -> SelfReturn a field with data converted to dtype.
block_gridmethod#
def block_grid(cpu: bool = False, inside_domain: bool = False) -> BlockGridThe 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) -> DistributedArrayReturn one component as a distributed array sharing this field’s storage.
copymethod#
def copy(copy_data: bool = True) -> SelfReturn a field on the same grid, with a copy of the data (or zeros).
crossmethod#
def cross(other: Self) -> SelfReturn the pointwise cross product of two 3-component fields.
dotmethod#
def dot(other: Self) -> SelfReturn 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.DataArrayEvaluate 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() -> NoneAdd 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) -> NoneWrite 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() -> NoneFill 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) -> SelfCreate a field from global scalar (num_gridpoints) or component-last data.
gathermethod#
def gather(positions: Array | float, weighting: Weighting = LINEAR) -> ArrayInterpolate 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) -> ArrayReturn 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) -> AnyReturn the node at global index on every rank (an allreduce: collective).
gradientmethod#
def gradient() -> SelfReturn 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) -> SelfRead a field written by :meth:export, split as the arguments say.
magnitudemethod#
def magnitude() -> SelfReturn the pointwise vector magnitude as a scalar field.
maxmethod#
def max(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> AnyReturn the maximum over all ranks, optionally for one component.
meanmethod#
def mean(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> AnyReturn the mean over all ranks, optionally for one component.
minmethod#
def min(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> AnyReturn the minimum over all ranks, optionally for one component.
on_gridmethodclassmethod#
def on_grid(grid: Grid, num_components: int = 1, split: bool = True) -> SelfReturn 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) -> AnyPlot 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) -> NoneAdd 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) -> NoneSet 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 ofnum_gridpointsfor onecomponent(or a scalar field): each rank copies its own nodes and the halos are zeroed; - a :class:
~mpiarray.DistributedArraywith the same split (the field’s shape, or the grid’s for onecomponent) 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) -> NoneSet 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) -> NoneSet 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) -> SelfSmooth 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) -> SelfSmooth 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) -> SelfSmooth 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) -> SelfSmooth the field with a 3-point moving-average kernel.
summethod#
def sum(axis: int | tuple[int, ...] | None = None, component: int | None = None) -> AnySum the field over all ranks, optionally for one component.
to_cupymethod#
def to_cupy() -> ArrayReturn the gathered field data on the active cunumpy backend (collective).
to_ndarraymethod#
def to_ndarray() -> ArrayReturn the global field data, num_gridpoints + (ncomp,) (collective).
to_numpymethod#
def to_numpy() -> np.ndarrayReturn the gathered field data as a numpy.ndarray (collective).