Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@ jobs:
example-scripts/01-cloud-in-cell-density/index.html
example-scripts/01-cloud-in-cell-density/plot-1.png
example-scripts/12-kernel-backends/index.html
numerics/index.html
numerics/kernels-linear-cell/index.html
numerics/kernels-quadratic-cell/index.html
numerics/weighting/index.html
numerics/field/index.html
api/mpifield/index.html
api/mpifield/field/index.html
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- Method cards (methodcard) on the numerics: the linear and quadratic shape functions
(`linear_cell`, `quadratic_cell`), the deposit and the gather (`Weighting.scatter_block`,
`Weighting.gather_block`) and their adjointness (`Weighting`), the block grid and the halo
exchanges, the gradient, the smoothing filters, the prescribed field and the evaluation,
with their assumptions and references. `test_method_cards.py` checks them: 19 SymPy rules
(the shape functions, partition of unity and first moment, edge folds and seams through the
compiled kernels; the operators), the second order of the two gathers, the gradient and
`electric_field_from_potential`, and the cards of the docs against those of the code. Adds
the `methodcard` dependency (`methodcard[symbolic]` in the test extra).
- A Numerics section in the docs (header and sidebar): an overview
(`docs/src/content/docs/numerics/index.mdx`) and one page per module
(starlight-theme's `methodcard: { pages: true }`, theme 0.7.4); each method's API page shows
its card.
- The cross references in the docstrings render on the API pages: code spans instead of
`:class:`, `:meth:` and `:func:` roles.
- `Field`, moved out of picnic (`picnic.field`): a field on the nodes of a uniform 1D, 2D or
3D grid, split over MPI ranks with halo nodes (an mpiarray `DistributedArray` on a
simgeometry `Grid`), with gather (`gather`, was `interpolate_field`) and scatter
Expand Down
16 changes: 16 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,25 @@ export default defineDocs({
title: 'mpifield',
description: 'Fields on uniform grids split over MPI ranks, with compiled linear gather and scatter for particle codes.',
pythonPackage: 'mpifield',
// the method cards: a Numerics section with the overview (numerics/index.mdx) and one page per
// module, from the shape functions to the field operations, and a Numerics section on each
// method's API page
methodcard: {
pages: true,
index: false,
labels: {
'mpifield.kernels.linear_cell': 'Linear weights',
'mpifield.kernels.quadratic_cell': 'Quadratic weights',
'mpifield.weighting': 'Deposit and gather',
'mpifield.field': 'Halos, gradient, smoothing',
'mpifield.prescribed': 'Prescribed fields',
'mpifield.evaluate': 'Evaluation',
},
},
nav: [
{ label: 'Getting started', link: '/getting-started/installation/' },
{ label: 'Examples', link: '/examples/' },
{ label: 'Numerics', link: '/numerics/' },
{ label: 'Development', link: '/development/docs/' },
{ label: 'API', link: '/api/mpifield/' },
],
Expand Down
72 changes: 11 additions & 61 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 102 additions & 0 deletions docs/src/content/docs/numerics/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: Overview
description: The numerics of mpifield, from the shape functions of the deposit and the gather to the halo exchange, and what it leaves out.
sidebar:
label: Overview
---

import MethodCard from '@max-models/starlight-theme/components/MethodCard.astro';

mpifield holds fields on the nodes of a uniform 1D, 2D or 3D grid, split over MPI ranks with halo
nodes, and moves values between the nodes and the markers of a particle code: a deposit
(scatter) of marker values onto the nodes and a gather (interpolation) of the field at the
markers. No field equation is solved; the Poisson or Maxwell solver, the pusher and the time
integration belong to the codes that use it.

Each equation on these pages is declared on the code that implements it, as a method card, and
checked by the tests: SymPy rules for the shape functions and the operators, run against the
compiled kernels, and the order of convergence of the two gathers, the gradient and the
prescribed field. The pages of the section list the cards module by module; this one shows how
they fit together.

## Deposit and gather

Node $k$ of axis $d$ is at $x_{d,k} = x^{\mathrm{lo}}_d + k h_d$, and a marker at $\mathbf x$ has
the relative coordinates $r_d = (x_d - x^{\mathrm{lo}}_d)/h_d$. A weighting gives node
$\mathbf k$ the weight $W_{\mathbf k}(\mathbf x) = \prod_d w(r_d - k_d)$, a tensor product of
one-dimensional B-splines:

1. **[Linear weights](/mpifield/numerics/kernels-linear-cell/)** (`LINEAR`, cloud-in-cell),
$w_1$, two nodes per axis: the default.
2. **[Quadratic weights](/mpifield/numerics/kernels-quadratic-cell/)** (`QUADRATIC`,
triangular-shaped cloud), $w_2$, three nodes per axis around the nearest one: a continuous
first derivative and less aliasing noise, for $3^n$ nodes per marker instead of $2^n$.

Both sum to one over the nodes and reproduce the position as their first moment, so a deposit
keeps the total value and a gather is exact for constant and linear fields; both gather a
smooth field to second order in $h$. At a bounded edge the weights beyond it fold onto the edge
nodes, which keeps the total; a periodic axis wraps.

The **[deposit and the gather](/mpifield/numerics/weighting/)** are a pair: the gather reads
the nodes and weights the scatter writes, so it is the transpose of the scatter, also at the
edges and the seams. With a symmetric field solver this keeps the momentum: a particle feels no
force of its own.

<MethodCard name="mpifield.weighting.Weighting" headingLevel={3} />

The deposit is a sum of weights, not a density: divide it by $\prod_d h_d$ for one.

## Over MPI ranks

Each rank stores its block of the grid with halo nodes, at least as many as the weighting needs
(one for `LINEAR`, two for `QUADRATIC`). The kernels see the stored block as a grid of its own
([`Field.block_grid`](/mpifield/numerics/field/)), without the halos beyond a bounded edge of
the domain. A cycle of a particle code then runs, on every rank:

1. select its own markers (from the block's first node up to the next rank's first node);
2. deposit them into the stored block, halo nodes included;
3. `exchange_halos`: add the halos to their owners on the neighbouring ranks;
4. set the field (a solver outside mpifield);
5. `fill_halos`: copy the owners' nodes into the halos;
6. gather the field at its own markers.

The split deposit and gather equal the serial ones.

## Operations on the field

- **[Gradient](/mpifield/numerics/field/)** of a scalar field: central differences, second
order, one-sided at the ends of a bounded axis.
- **[Smoothing](/mpifield/numerics/field/)**: separable binomial, Gaussian or uniform filters,
zero outside a bounded axis, wrapped on a periodic one.
- **[Prescribed fields](/mpifield/numerics/prescribed/)**: $\mathbf E = -\nabla\phi$ of a
given potential, e.g. a table along one axis, for test particles.
- **[Evaluation](/mpifield/numerics/evaluate/)** as xarray: linear interpolation between the
nodes, whatever the weighting.

## Conventions

Positions are component-major, `(ndim, N)`, and marker values `(ncomp, N)`; a field is
`num_gridpoints + (ncomp,)`. On a bounded axis the $N$ nodes span the domain,
$h = (x^{\mathrm{hi}} - x^{\mathrm{lo}})/(N - 1)$; on a periodic one they cover $N$ cells,
$h = (x^{\mathrm{hi}} - x^{\mathrm{lo}})/N$, and the last node neighbours the first. The compiled
kernels (Pyccel with OpenMP, CUDA, a NumPy fallback, Metal in single precision) share the same
arithmetic.

## Not covered

- No Poisson or Maxwell solver, no particle pusher, no time integration.
- No normalisation of the deposit by the cell volume or by a species weight.
- Only the gradient of a scalar field: no divergence or curl.
- Only linear and quadratic weightings are built in, and no energy-conserving (non-adjoint)
schemes; a weighting of your own is a pair of kernels.
- Markers are not moved between ranks: each rank deposits and gathers its own, and wraps them on
a split periodic axis.
- `electric_field_from_potential` differences one-sided at both ends of every axis, also a
periodic one.

## Verification

Besides the checks of the cards, the tests compare the Pyccel, CUDA (emulated) and Metal
kernels with the NumPy ones, the deposit with the transpose of the gather, the OpenMP deposit
with the serial one, and, under `mpiexec`, the split deposit, gather, gradient and smoothing
with those of one rank.
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ dependencies = [
"cunumpy>=0.7.2",
# MPI only under an MPI launcher (mpirun, srun); a serial stand-in for mpi4py otherwise
"maybempi>=0.1.2",
# the method cards of the numerical methods (equations, orders, references)
"methodcard",
"mpiarray>=0.2.1",
"numpy",
# the CPU kernels: compiled on first use; their sources import Pyccel's OpenMP module
Expand Down Expand Up @@ -56,7 +58,7 @@ optional-dependencies.metal = [ "mlx; sys_platform == 'darwin' and platform_mach
optional-dependencies.mpi = [ "mpi4py" ]
# Field.plot draws with plasma-plots
optional-dependencies.plot = [ "matplotlib>=3.10", "plasma-plots>=0.2.5" ]
optional-dependencies.test = [ "mpifield[plot]", "pytest", "pytest-cov", "pytest-mpi" ]
optional-dependencies.test = [ "methodcard[symbolic]", "mpifield[plot]", "pytest", "pytest-cov", "pytest-mpi" ]
urls.Source = "https://github.com/max-models/mpifield"

[project.entry-points."scope_profiler.opt_in_tags"]
Expand Down Expand Up @@ -97,7 +99,10 @@ reportMissingImports = "error"
ini_options.pythonpath = [ "src" ]
ini_options.testpaths = [ "src" ]
ini_options.addopts = "-ra"
ini_options.markers = [ "mpi: tests run under mpiexec with --with-mpi (pytest-mpi)" ]
ini_options.markers = [
"convergence: an order-of-accuracy test of a method card",
"mpi: tests run under mpiexec with --with-mpi (pytest-mpi)",
]

[tool.coverage]
run.omit = [ "*/tests/*" ]
Expand All @@ -111,6 +116,7 @@ report.show_missing = true
# cunumpy = { git = "https://github.com/max-models/cunumpy", branch = "devel" }
cunumpy = { git = "https://github.com/max-models/cunumpy", branch = "devel" }
maybempi = { git = "https://github.com/max-models/maybempi", branch = "devel" }
methodcard = { git = "https://github.com/max-models/methodcard", branch = "devel" }
mpiarray = { git = "https://github.com/max-models/mpiarray", branch = "devel" }
plasma-plots = { git = "https://github.com/max-models/plasma-plots", branch = "devel" }
scope-profiler = { git = "https://github.com/max-models/scope-profiler", branch = "devel" }
Expand Down
12 changes: 6 additions & 6 deletions src/mpifield/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Fields on uniform grids split over MPI ranks, with compiled gather and scatter.

:class:`Field` holds a field on the nodes of a uniform 1D, 2D or 3D grid as an
:class:`mpiarray.DistributedArray`; it interpolates the field at marker positions
(:meth:`Field.gather`) and deposits marker values onto the nodes
(:meth:`Field.scatter`) with a :class:`Weighting`: :data:`LINEAR` (the default),
:data:`QUADRATIC` or one of your own. The built-in ones run compiled kernels of
:mod:`mpifield.kernels`, on the CPU (Pyccel, NumPy) or the GPU (CUDA, Metal).
`Field` holds a field on the nodes of a uniform 1D, 2D or 3D grid as an
`mpiarray.DistributedArray`; it interpolates the field at marker positions
(`Field.gather`) and deposits marker values onto the nodes
(`Field.scatter`) with a `Weighting`: `LINEAR` (the default),
`QUADRATIC` or one of your own. The built-in ones run compiled kernels of
`mpifield.kernels`, on the CPU (Pyccel, NumPy) or the GPU (CUDA, Metal).
"""

from importlib.metadata import PackageNotFoundError, version
Expand Down
Loading
Loading