Distributed spaces¶
PaNTr can distribute a tensor-product B-spline space (BsplineSpace) or a
hierarchical THB-spline space (THBSplineSpace) across MPI ranks for parallel
assembly. The design keeps a clear separation:
a serial windowing core (in
pantr.grid/pantr.bspline) that needs no MPI;the MPI layer (
pantr.mpi) that wraps it per rank — it ships with PaNTr and becomes usable oncempi4pyis installed (thempiextra is just a convenience for that; see the Installation section below).
Every rank holds a redundant, self-contained local space covering the cells it
owns plus their support halo, so basis evaluation and element assembly are purely
local. Cross-rank coupling (e.g. a global linear solve) is the consumer’s job
(typically a PETSc PtAP); PaNTr performs no MPI DOF exchange.
The pipeline¶
Everything flows through one small descriptor, the Partition (which rank owns
each cell), regardless of where it came from:
partition_grid ─┐
coupling_graph + partition_graph ─┤
from_dolfinx ─┘
│
▼
Partition ──► DistributedSpace(space, partition, comm)
│
▼
LocalSpace (this rank)
A Partition is just an integer owner per cell (-1 for an inactive/excluded
cell). Any of the three sources below produces one; DistributedSpace consumes
it identically.
Installation¶
pip install pantr # serial-only, no mpi4py dependency
pip install 'pantr[mpi]' # + mpi4py, for distributed spaces (needs an MPI library)
pip install 'pantr[metis]' # + pymetis, for partition_graph(backend="metis")
import pantr.mpi succeeds even without mpi4py; only code paths that genuinely
need MPI (e.g. pantr.mpi.require_mpi()) raise when it is absent. The
partition/windowing layer (partition_grid, partition_graph, build_local) is
pure serial and never imports MPI.
Partitioning a grid¶
pantr.grid.partition_grid(grid, n_parts, *, backend="auto", cell_weights=None, cell_active=None) splits a grid’s cells into n_parts rank subdomains:
from mpi4py import MPI
from pantr.bspline import create_uniform_space
from pantr.grid import partition_grid, tensor_product_grid
comm = MPI.COMM_WORLD
space = create_uniform_space([2, 2], [64, 64]) # identical on every rank
grid = tensor_product_grid(space) # the knot-span grid
partition = partition_grid(grid, comm.size) # deterministic -> same on all ranks
|
dependency |
balances |
shape |
good for |
|---|---|---|---|---|
|
none |
cell count |
axis-aligned boxes |
uniform tensor-product grids |
|
none |
total weight |
boxes |
THB / immersed; arbitrary / prime |
|
none |
— |
— |
|
The partitioner is deterministic, so every rank computes the same partition with no communication.
Coupling-graph partitioning¶
For irregular or hierarchical meshes, a geometric split can cut more shared functions than necessary. Build the cell-coupling graph (cells that share a basis function) and partition that to minimize cross-rank DOF coupling:
from pantr.bspline import coupling_graph, partition_graph
graph = coupling_graph(space) # METIS/Scotch CSR; TP and THB
partition = partition_graph(graph, comm.size) # backend="spectral" (default)
|
dependency |
notes |
|---|---|---|
|
scipy only |
recursive Fiedler bisection; never leaves a rank empty |
|
|
higher-quality k-way min-cut; raises a clear error if |
Consuming an external partition (dolfinx)¶
When a dolfinx-based consumer (e.g. QUGaR or tIGArx) already partitioned a mesh, ingest its cell ownership instead of re-partitioning:
from pantr.mpi import from_dolfinx
n_cells = space.num_total_intervals # TP; THB: space.grid.num_cells
partition = from_dolfinx(mesh, n_cells) # MPI-allgathers over mesh.comm
The correspondence between a dolfinx cell and a PaNTr cell is the dolfinx
original cell index; pass dolfinx_to_pantr=<array> if the mesh was not built in
PaNTr’s C-order. Cells absent from the mesh (e.g. exterior cells an immersed code
trimmed away) get owner -1.
Building the distributed space¶
pantr.mpi.DistributedSpace(global_space, partition, comm) is the per-rank,
SPMD handle. Every rank constructs its own from the same global space and
partition; construction performs no communication (it windows the global space
locally via build_local):
from pantr.mpi import DistributedSpace
ds = DistributedSpace(space, partition, comm)
ds.rank # this rank's id
ds.n_parts # number of ranks (== comm.size == partition.n_parts)
ds.owned_cells # global ids of the cells this rank owns
local = ds.local # this rank's LocalSpace, or None if it owns no cells
The LocalSpace (local) bundles a real, windowed BsplineSpace /
THBSplineSpace (local.space) with the maps relating it to the global space –
local_to_global_cell, local_to_global_dof, owned_cell_mask, owned_dof_mask.
Its basis equals the global basis pointwise over the rank’s owned cells, so
per-element assembly on local.space is exact.
Note
A rank that owns no cells (an over-provisioned run, or a from_dolfinx partition
that leaves a rank empty) gets ds.local is None and ds.owns_cells is False
rather than failing – guard with if ds.owns_cells: before assembling.
One-call shortcut¶
When you don’t need to hold the grid or partition yourself,
pantr.mpi.create_distributed_space(global_space, comm, *, method="grid", backend=None, cell_weights=None, cell_active=None) derives the grid, partitions it, and builds the
DistributedSpace in one call:
from pantr.mpi import create_distributed_space
ds = create_distributed_space(space, comm) # geometric (partition_grid)
ds = create_distributed_space(space, comm, method="graph") # coupling-graph (partition_graph)
method="grid" (default) partitions geometrically; method="graph" minimizes
cross-rank DOF coupling. backend=None picks each method’s default ("auto" /
"spectral"), and cell_weights / cell_active are forwarded to the chosen
partitioner. The explicit three-step flow above stays available when you need the grid
or partition as separate objects.
Distributing a function¶
To distribute a function (space + control points), not just a space, use
DistributedSpace.localize(control_points) to slice a global coefficient field to this
rank’s local function, or pantr.mpi.create_distributed_function(global_function, comm, ...) for the one-call path (same method / backend / cell_weights / cell_active
options as create_distributed_space):
from pantr.mpi import create_distributed_function
df = create_distributed_function(global_function, comm)
local_f = df.local # this rank's windowed Bspline / THBSpline (or None)
# Or localize many fields on one distributed space (solver loop):
ds = create_distributed_space(space, comm)
local_u = ds.localize(u) # u, residual, ... are global coefficient vectors
The global control points are identical on every rank (the SPMD model); “distributing”
windows the space and slices the coefficients to this rank’s DOFs. df.local equals the
global function pointwise over the rank’s owned cells, and is None on a rank that owns
no cells.
Immersion hooks¶
PaNTr stores no geometric classification (interior / cut / exterior). An immersed consumer expresses its classification through two transient hooks, and PaNTr never interprets why a cell is weighted or inactive:
cell_weights– per-cell assembly cost;rcband the graph backends balance total weight rather than cell count (cut cells cost more).cell_active– a boolean mask; inactive cells get owner-1and drop out of the partition (and the coupling graph).
partition = partition_grid(grid, comm.size, cell_weights=cost, cell_active=interior)
Consumer patterns¶
Native MPI (e.g. lepard), no dolfinx. Partition the grid (or coupling graph) directly and build the distributed space:
from mpi4py import MPI
from pantr.grid import partition_grid, tensor_product_grid
from pantr.mpi import DistributedSpace
comm = MPI.COMM_WORLD
partition = partition_grid(tensor_product_grid(space), comm.size)
ds = DistributedSpace(space, partition, comm)
if ds.owns_cells:
assemble_local(ds.local) # your element loop over ds.local.space
dolfinx-driven (e.g. tIGArx,
QUGaR). Let dolfinx own the partition and bridge
it in – the immersed cell_active path falls out for free (trimmed cells become
owner -1):
from pantr.mpi import from_dolfinx, DistributedSpace
n_cells = space.num_total_intervals # THB: space.grid.num_cells
partition = from_dolfinx(mesh, n_cells) # absent cells -> -1
ds = DistributedSpace(space, partition, mesh.comm)
Testing distributed code¶
The distributed objects are duck-typed on the communicator (only rank / size /
allgather are read), so most behavior is unit-testable serially with a fake
comm. Genuine multi-rank tests live under tests/mpi/, are skipped unless
PANTR_RUN_MPI is set, and run under a launcher:
PANTR_RUN_MPI=1 mpiexec -n 3 python -m pytest tests/mpi/
See the API reference for full signatures.