Skip to content

Add space filling curve (SFC) and hybrid SFC/graph partitioning algorithms for building parallel meshes - #4403

Draft
garth-wells wants to merge 71 commits into
mainfrom
garth/mesh-optimise-2
Draft

Add space filling curve (SFC) and hybrid SFC/graph partitioning algorithms for building parallel meshes #4403
garth-wells wants to merge 71 commits into
mainfrom
garth/mesh-optimise-2

Conversation

@garth-wells

@garth-wells garth-wells commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds space-filling curve (SFC) and hybrid SFC/graph partitioning, so that mesh construction cost can stay roughly flat as the rank count grows, instead of scaling with the (increasingly expensive) cost of graph partitioning. Also includes a general mesh-construction performance pass, a ParMETIS re-partitioner for rebalancing, and demo_partition.py, a new demo comparing all available partitioners on the same input under varying degrees of input-distribution randomness.

What's new

Geometric and hybrid partitioning types (graph/partition.h)

  • graph::partition_fn — graph-only partitioners (required dual graph, no coordinates).
  • graph::geom_partition_fn — partitioners that use node positions and optionally the graph (e.g. for ghosting only); the graph argument is std::optional.
  • graph::hybrid_partition_fn — partitioners that need both the graph and node positions unconditionally, because the graph edges are part of the partitioning decision itself, not only used for ghosting.

Space-filling curve partitioning (graph/sfc.h{cpp})

  • graph::partition_sfc_morton / partition_sfc_hilbert: partition points along a Morton or Hilbert curve. No external library required. Cost is nearly independent of rank count, at the price of a larger edge cut than graph partitioning.
  • graph::sfc::partitioner: wraps either curve as a geom_partition_fn.

ParMETIS additions (graph/partitioners.h{cpp})

  • graph::parmetis::geom_partitioner (ParMETIS_V3_PartGeom) and geom_partitioner_kway (ParMETIS_V3_PartGeomKway, a hybrid_partition_fn since it uses the graph edges as well as the coordinates).
  • graph::parmetis::repartitioner (ParMETIS_V3_AdaptiveRepart): treats the current data distribution as the current partition and computes a new one that balances load while limiting how many nodes move — suited to rebalancing after non-uniform refinement, as opposed to partitioning from scratch.
  • Default PT-SCOTCH strategy changed to strategy::speed (see performance data below).

Mesh-level wrappers (mesh/partition.h{cpp})

  • mesh::create_geometric_cell_partitioner: computes cell centroids and applies a geom_partition_fn; builds the mesh dual graph only when ghosting is requested (skipped otherwise, since a purely geometric partitioner does not need it for the partitioning decision itself).
  • mesh::create_hybrid_cell_partitioner: as above but for a hybrid_partition_fn — always builds and supplies the dual graph, since these partitioners need it unconditionally.
  • Split out of mesh/utils.h{cpp} into their own files.

create_mesh performance and structure

  • Removed redundant copies/sorts in the serial path (measured 4.29 s → 3.48 s for 6M tetrahedra, Release build, single rank): avoided a second copy of the concatenated cell array, reused the sorted unique vertex indices that topology construction already computes instead of re-deriving them, and skipped the (identity) topology-extraction step for 'P1 geometry'.
  • Extracted the cell-partitioning/redistribution logic into mesh::impl::partition_cells, shrinking create_mesh to a clearer sequence of named steps.

demo_partition.py

  • Compares every available partitioner (ParMETIS Kway/GeomKway/Geom, PT-SCOTCH, SFC Morton/Hilbert) on the same distributed input, reporting cell imbalance and edge cut, across input distributions ranging from fully locality-preserving to fully randomised.
  • A "two-stage partitioning" comparison: partition with the space-filling curve first (cheap, insensitive to input distribution), then re-partition the result with a graph partitioner (now running on a well-distributed input) — the general idea ParMETIS GeomKway embodies, but applicable around any graph partitioner.

Performance data

  • create_mesh copy/sort removal: 4.29 s → 3.48 s, 6M tetrahedra, serial, Release build.
  • SFC vs. graph partitioning for mesh construction: 1.48x faster at 40 ranks for 15.4M tetrahedra, at the cost of 24% more ghost cells.
  • PT-SCOTCH strategy::speed vs. default: ~20% faster with no measurable change in cut edges (20 ranks/3.1M cells: 0.88–0.99 s → 0.69–0.77 s, 148.8k → 149.5k cut edges; 40 ranks/6M cells: 2.36 s → 2.19 s, 329.6k → 328.3k cut edges). strategy::quality was measured slower and worse (more cut edges) than the default.
  • ParMETIS Kway vs. GeomKway crossover (n=128, 12.6M tetrahedra, 20 ranks): with locality-preserving input, Kway is faster (2.35 s vs. 2.58 s); with fully randomised input, GeomKway wins (4.78 s vs. 5.99 s) — Kway degrades 2.55x under randomisation against GeomKway's 1.85x.
  • ParMETIS re-partitioner vs. partitioning from scratch (20 ranks, 3.1M cells, deliberately skewed starting distribution, imbalance 3.5): re-partitioning rebalances to 1.014 while moving 74.5% of cells, against 81.8% moved for partitioning from scratch, at the cost of a 7% larger edge cut.
  • Two-stage SFC-then-PT-SCOTCH vs. PT-SCOTCH alone, fully randomised input:
    • 12.6M tetrahedra, 20 ranks: time inside SCOTCH falls from 9.52 s to 2.25 s (4.2x), edge cut slightly smaller (385506 → 375596); end-to-end mesh creation 11.7 s → 7.1 s.
    • 42.5M tetrahedra, 20 ranks (measured separately): SCOTCH alone 46.6 s total (38.1 s inside SCOTCH) vs. two-stage 24.2 s total (14.1 s inside SCOTCH) — SCOTCH itself is ~2.7x faster once the input has been pre-localised, edge cut essentially a wash (859442 vs. 853204, within PT-SCOTCH's own run-to-run variability).
    • At 50% (rather than fully) randomised input, 42.5M tetrahedra: SCOTCH alone 36.4 s vs. two-stage 24.2 s; the time advantage shrinks with less randomness, as expected, since there is less locality to lose in the first place.

Scaling data (demo_partition.py)

Rank-count scaling, fixed mesh, fully randomised input

82,944 tetrahedra, fraction=1.0 (fully randomised). Time in seconds; edge cut in parentheses:

Ranks ParMETIS Kway ParMETIS GeomKway ParMETIS Geom PT-SCOTCH SFC Morton SFC Hilbert
1 0.085 (0) 0.098 (0) 0.100 (0) 0.086 (0) 0.111 (0) 0.110 (0)
2 0.094 (2760) 0.096 (2556) 0.077 (2390) 0.102 (2344) 0.074 (2364) 0.080 (2464)
3 0.080 (4466) 0.074 (4186) 0.046 (5808) 0.126 (4052) 0.038 (5828) 0.043 (5570)
5 0.060 (6756) 0.055 (6470) 0.044 (8684) 0.111 (5688) 0.037 (8660) 0.040 (7188)

(At 1 rank there is nothing to cut, hence 0.) At this mesh size, absolute times are dominated by fixed per-call overhead rather than by the cost of partitioning itself, so this mainly demonstrates correct behaviour at low rank counts. It is not a demonstration of asymptotic scaling — see below and the two-stage numbers in Performance data above for where the scaling advantage of the SFC/hybrid approach actually shows up.

Problem-size scaling at fixed rank count (20 ranks, fully randomised input)

Tetrahedra ParMETIS Kway ParMETIS GeomKway ParMETIS Geom PT-SCOTCH SFC→PT-SCOTCH (two-stage) SFC Morton SFC Hilbert
12.6M 5.82 s (436382) 4.61 s (434496) 3.41 s (509306) 11.82 s (385506) 7.96 s (382700) 3.24 s (510414) 3.30 s (458188)
42.5M 21.04 s (985510) 16.43 s (974388) 11.94 s (1144526) 41.02 s (853204) 26.19 s (859442) 11.48 s (1148982) 12.04 s (1028312)
time ratio for 3.4x more cells 3.62x 3.57x 3.50x 3.47x 3.29x 3.54x 3.65x

At fixed rank count, all partitioners scale roughly linearly with cell count over this range (3.3–3.65x time for a 3.4x increase in cells) — none is dramatically worse than linear here. PT-SCOTCH remains the most expensive in absolute terms at both sizes, by a wide margin; the two-stage route is consistently faster than PT-SCOTCH alone at both sizes (7.96 s vs. 11.82 s, and 26.19 s vs. 41.02 s) and also has the lowest time ratio of the seven columns (3.29x vs. PT-SCOTCH alone's 3.47x), i.e. it scales somewhat more mildly with problem size here, not just faster in absolute terms — consistent with the SFC stage taming PT-SCOTCH's own scale-sensitive coarsening cost rather than only offering a one-off saving. The rank-count-independent cost advantage that motivates the SFC/hybrid approach more broadly is not fully visible in a same-rank-count comparison like this one, and would be expected to grow more pronounced at higher rank counts than tested here.

Known follow-up

Redistributing cells for the two-stage comparison currently goes through building a full intermediate mesh, most of whose cost (topology, geometry, ghost cells) is wasted since only the cell destinations are needed before the second stage. A redistribute_by_partitioner helper that calls the partitioner directly and exchanges only the cell rows was prototyped in the demo and measured at roughly half the cost of the full-mesh route for this step, but is not yet part of the public API.

Filed #4402: for mixed-topology meshes, cell redistribution currently pays for one NBX neighbourhood-discovery round per cell type; the destination rank sets for different cell types typically overlap substantially, so this is a candidate for future optimisation. Does not affect single-cell-type meshes, which are unaffected (one cell type, one round).

Testing

C++ unit tests pass on 1, 2 and 3 ranks; Python test suite passes; demo_partition.py runs via the demo test harness across multiple rank counts and mesh sizes, including a 42.5M-tetrahedron mesh on 20 ranks.

garth-wells and others added 30 commits August 9, 2026 19:29
Avoids materialising and sorting a separate ghost-edges vector just
to count distinct values; a single boost::unordered_flat_map pass
over the edge array does it in one O(n) sweep instead of an O(n)
filter plus an O(m log m) sort. Also flush the SCOTCH sub-timers so
they show up in per-call timing reports.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
std::ranges::sort's generic path, when instantiated for a span whose
size the compiler cannot bound at compile time, triggers a spurious
array-bounds warning against the fixed-size row_sorted_storage scratch
buffer it is sliced from. Replace the unreachable (no supported
CellType has an entity with more than 4 vertices) generic-sort
fallback with an explicit bound check, which also turns a latent
buffer overflow into a defined error should a future entity type ever
exceed 4 vertices.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Serial mesh creation: remove redundant copies and sorts in create_mesh
(4.29 s -> 3.48 s for 6M tetrahedra, Release build):

- `nodes2` was a second, identical concatenation of `cells1`, used only
  as a span by create_geometry. For a single cell type it is now
  `cells1.front()`, with no copy.
- `create_topology` already builds the sorted, unique input global vertex
  indices but discarded them. A new `mesh::impl::create_topology` returns
  them alongside the Topology, and for 'P1 geometry' create_mesh uses
  them instead of radix sorting the whole cell array and calling unique.
  The public create_topology overloads are unchanged.
- `extract_topology` is the identity for 'P1 geometry', so it is skipped:
  a new `mesh::is_vertex_dof_layout` drives the check and `cells1_v`
  becomes spans that alias `cells1`.

Add geometric cell partitioners, which partition on cell position rather
than on the dual graph edges. These are much cheaper than graph
partitioning, and the cost is nearly independent of the rank count, at
the price of a larger edge cut (measured 1.48x faster mesh creation at
40 ranks for 15.4M tetrahedra, with 24% more ghost cells):

- `graph::partition_sfc` partitions points by Morton key, with splitters
  drawn from a gathered sample. No external library required.
- `graph::geom_partition_fn` is `partition_fn` plus a coordinate per
  node, with implementations `graph::sfc::partitioner` and
  `graph::parmetis::geom_partitioner` (`ParMETIS_V3_PartGeomKway` and
  `ParMETIS_V3_PartGeom`).
- `mesh::create_geometric_cell_partitioner` computes cell centroids and
  applies a `geom_partition_fn`, and returns an ordinary
  CellPartitionFunction, so create_mesh is unchanged.

Pass num_threads to the dual graph build in create_cell_partitioner. It
was defaulted to 1, so the partitioner's dual graph build could not be
threaded whatever the caller asked for.

Note: adding the defaulted num_threads parameter changes the mangled
name of create_cell_partitioner, so this is an ABI break. Reinstall the
Python module rather than relying on an incremental rebuild.

Add demo_partition.py, which builds a mesh from distributed input data
with each available partitioner and reports the cell imbalance and edge
cut, and a C++ unit test asserting that global entity counts do not
depend on the partitioner.

Tests that imported create_cell_partitioner from dolfinx.cpp.mesh now
use the pure-Python dolfinx.mesh interface, which carries the default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main's #4396 landed the mesh construction performance work that this
branch also carried locally, so the shared parts conflicted. Resolutions:

- graph/ordering.cpp: took main, which removes the GPS re-ordering code
  (#4391). The only local change was a std::ranges::distance tweak inside
  a function that main deletes.
- graph/partitioners.cpp: took main's compute_destination_ranks, dropping
  the local 'Halo dest ranks' timers, and re-applied
  parmetis::geom_partitioner on top.
- graph/partition.cpp: kept the <cstdint> and <limits> includes that
  partition_sfc needs.

main's column-major packing of unmatched facets in
create_boundary_vertices_fn and the create_mesh copy elimination on this
branch touch the same lambda but different parts of it, and merged
cleanly.

Verified after merging: C++ unit tests pass on 1, 2 and 3 ranks; Python
test suite passes (3085 passed, 3 CFFI tests deselected for a
pre-existing setuptools issue); demo_partition runs via the demo test
harness. Mesh creation timings are unchanged by the merge: 3.49 s serial
for 6M tetrahedra, and 2.08 s (graph) / 1.46 s (SFC) at 40 ranks for
15.4M tetrahedra.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Explain the relationship between the two arrays returned by cube_block:
`cells` holds global vertex indices, while a row of `x` holds the
coordinates of the point whose global index is that row plus the number
of points on lower ranks. The two distributions are independent, so a
rank's cells generally refer to vertices whose coordinates are held by
other ranks, and create_mesh (and the geometric partitioners) fetch them.

Add `redistribute_cells`, which moves a given fraction of each rank's
cells to a random rank, and report the comparison for input that is
locality preserving, half randomised and fully randomised. Partition
quality turns out to be insensitive to the input distribution, whereas
the cost of graph partitioning is not, which is what ParMETIS GeomKway
addresses.

Report a mesh creation time alongside the quality measures, since
otherwise the shuffle has no visible effect. The demo mesh is small and
the time covers all of mesh creation, so the prose is explicit that only
a hint of the effect is visible at this size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured on the dual graph of a structured tetrahedral mesh,
strategy::speed is around 20% faster than SCOTCH's own default strategy
with no measurable change in the number of cut edges (20 ranks, 3.1M
cells: 0.88-0.99 s and 148.8k cut edges for strategy::none, 0.69-0.77 s
and 149.5k for strategy::speed; 40 ranks, 6M cells: 2.36 s/329.6k versus
2.19 s/328.3k).

Note in passing that strategy::quality is both slower and cuts more edges
than the default for these graphs, and that the imbalance parameter does
not affect the run time appreciably (under 5% over 0.025 to 0.2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The effect of the input cell distribution on partitioning cost is visible
only on a large mesh, so use n = 128 (12.6M tetrahedra). Reduce n when
there is nothing to partition (a single rank) and in CI, where the demo
must run quickly, following the pattern in demo_cahn-hilliard. Add a
DEMO_PARTITION_N override, as the full size is slow on few ranks.

At n = 128 on 20 ranks the crossover that ParMETIS GeomKway exists for is
clear: with locality-preserving input Kway is faster than GeomKway (2.35 s
versus 2.58 s), whereas with a randomised input distribution GeomKway wins
(4.78 s versus 5.99 s), Kway degrading 2.55x against GeomKway's 1.85x.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add graph::parmetis::repartitioner, which wraps
ParMETIS_V3_AdaptiveRepart. Unlike a partitioner, it treats the current
distribution of the graph as the current partition and computes a new
partition that balances the load while limiting how many nodes have to
move, which suits re-balancing a distributed mesh, e.g. after non-uniform
refinement.

It has the graph::partition_fn signature, so no new type is needed: the
current partition of a node is the rank that holds it. This does mean the
number of parts must equal the communicator size, which is checked.

Note that PT-SCOTCH has no distributed equivalent to offer here.
SCOTCH_dgraphRepart does not exist in PT-SCOTCH 7.0; its re-partitioning
entry points (SCOTCH_graphRepart, SCOTCH_graphRemap) are sequential only,
and would require gathering the whole dual graph onto one rank.

Also remove the unused adaptive_repartition() and refine() helpers this
replaces. They were never called, were written against an AdjacencyList
interface that no longer exists, and passed vsize in the adjwgt argument
of ParMETIS_V3_AdaptiveRepart.

Measured on 20 ranks with 3.1M cells, starting from a deliberately skewed
distribution (imbalance 3.5): re-partitioning rebalances to 1.014 while
moving 74.5% of cells, against 81.8% for partitioning from scratch, and
gives a 7% larger edge cut. The benefit is modest here because the
starting distribution is a poor partition, so preserving it is worth
little; the intended case of a good partition perturbed by refinement is
not yet measured, and there is no unit test for this function yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Partition a randomly distributed mesh with the space-filling curve, which
is insensitive to the input distribution, then re-partition the result
with PT-SCOTCH, which now runs on a well-distributed input. This is the
idea behind ParMETIS GeomKway, applied around any graph partitioner.

On 20 ranks with 12.6M tetrahedra and a fully randomised input
distribution, the time inside SCOTCH falls from 9.52 s to 2.25 s, a
factor of 4.2, and the edge cut is slightly smaller (375596 against
385506). End-to-end mesh creation goes from 11.7 s to 7.1 s.

Note that the reported total for the two-stage route is pessimistic: the
first stage builds a complete intermediate mesh where only the cell
destinations are needed, which is 3.2 s of the 7.1 s. Redistributing the
cells without building a mesh is not possible through the public
interface, so the demo pays for it.

The second stage needs the first-stage cells in the original input vertex
numbering, which the geometry's input_global_indices provides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lint tools were not available in the environment used to develop the
preceding commits, so these slipped through:

- The GeometricPartitioningFunc alias declared the cell coordinates as
  npt.NDArray[np.floating], while the interface takes double only.
  Callable parameters are contravariant, so passing a partitioner from
  dolfinx.graph did not type check (mypy arg-type).
- create_cell_partitioner was inserted at the head of three test import
  blocks rather than in sorted position (ruff I001).
- A docstring line in demo_partition exceeded the doc line length (W505).

Verified with the same invocations as the lint CI job: ruff check and ruff
format over cpp/ and python/, gersemi, clang-format, and mypy over
dolfinx, demo and test with the CI flags. All pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
garth-wells and others added 30 commits August 21, 2026 07:28
Co-authored-by: Jørgen Schartum Dokken <dokken92@gmail.com>
Co-authored-by: Jørgen Schartum Dokken <dokken92@gmail.com>
# Conflicts:
#	cpp/dolfinx/graph/partition.h
#	cpp/dolfinx/mesh/utils.cpp
#	cpp/dolfinx/mesh/utils.h
#	python/dolfinx/wrappers/dolfinx_wrappers/graph.h
#	python/dolfinx/wrappers/graph.cpp
redistribute_by_partitioner called the low-level partitioner without
the cell_weights/edge_weights arguments added when main's weighted
partitioner support was merged in, breaking the MPI demo test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Remove create_cell_partitioner, create_geometric_cell_partitioner and
create_hybrid_cell_partitioner, and the CellPartitionFunction,
GeometricPartitionFunction and HybridCellPartitionFunction type
aliases, all now redundant with graph::partition_fn,
graph::geom_partition_fn and graph::hybrid_partition_fn. The mesh
dual graph is built once by the caller and passed to the partitioner
as an AdjacencyList, rather than rebuilt inside it on every call.

Also fixes the PR #4403 CI failure: GeometricPartitioner::__call__
and HybridPartitioner::__call__ in the Python bindings still passed a
commg argument that a prior partition.h simplification had dropped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
redistribute_by_partitioner called GeometricPartitioner objects with
the old (comm, nparts, commg, x) signature; commg was dropped when
GeometricPartitioner::__call__ was updated to match
graph::geom_partition_fn.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
model_to_mesh/read_from_msh's partitioner Callable type hint still
described the pre-dual_graph-refactor CellPartitionFunction shape
(cell_types, cells, max_facet_to_cell_links, ...), causing a mypy
CI failure. Update to the current 6-argument graph::partition_fn
shape (comm, nparts, dual_graph, cell_weights, edge_weights,
ghosting).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract split_and_build_node_disp from graph::parmetis::partitioner,
repartitioner, geom_partitioner and geom_partitioner_kway, which all
independently split comm (ParMETIS crashes on empty ranks) and built
an identical node displacement array. Also drops the dead split_comm
flag this made obsolete in partitioner.

Use dolfinx::radix_sort, not std::ranges::sort, for the duplicate-
vertex-index removal in mesh::impl::reorder_cells, matching the sort
already used for the same idiom elsewhere in this refactor.

Collapse GeometricPartitioner/HybridPartitioner into a single
OpaquePartitioner<Fn> template with two aliases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Fix stale ghosting doc comment on graph::geom_partition_fn.
- Remove sfc.cpp's redundant partition_curve pass-through wrapper.
- Widen create_box/create_rectangle/create_interval (and their
  impl::build_* helpers) to accept mesh::AnyCellPartitionFunction
  instead of only graph::partition_fn, so a geometric or hybrid
  partitioner can be used with the built-in mesh generators, not
  just create_mesh directly. Widen the matching Python bindings.
- Drop the synthetic-argument padding in
  create_geom_partitioner_py/create_geometric_cell_partitioner: the
  Python geometric partitioner wrapper now matches
  graph::geom_partition_fn's natural 3-arg shape instead of being
  forced through the 5-arg hybrid-partitioner shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hybrid_partition_fn was only used inside the HAS_PARMETIS-guarded
geom_partitioner_parmetis_kway binding, so declaring it unconditionally
tripped -Werror=unused-local-typedefs on builds without ParMETIS.
Move the alias inside the #ifdef, next to its only use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sion

- mesh::partition_cells: the partitioner is only invoked on ranks with
  commt != MPI_COMM_NULL (a possibly strict subset of comm, e.g. cells
  built on rank 0 only for create_interval/create_rectangle). A
  partitioner such as graph::parmetis::geom_partitioner, which requires
  nparts to equal the number of ranks calling it, can then throw on
  only those ranks, leaving the rest of comm blocked forever on the
  graph::build::distribute collective that follows. Catch the
  partitioner call and turn a data-dependent failure into a
  comm-wide decision via MPI_Allreduce before any rank proceeds.
- refinement::uniform_refine lost its ghost_mode parameter when its
  partitioner argument was simplified from mesh::CellPartitionFunction
  (which bundled ghost mode) to a bare graph::partition_fn, silently
  hardcoding GhostMode::none regardless of what partitioner was
  supplied. Add an explicit ghost_mode parameter, mirroring refine(),
  and thread it through to create_mesh. Update the Python binding and
  pure-Python wrapper to match.
- Fix stale "throws if ghosting true" doc comments on geom_partitioner
  and sfc::partitioner: graph::geom_partition_fn has no `ghosting`
  parameter at all.
- Factor the repeated "does this AnyCellPartitionFunction hold a
  callable" std::visit check into mesh::has_partitioner.
- Add missing <vector> include to graph/sfc.h (IWYU).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AdjacencyList._cpp_object is typed as a 3-way Union regardless of the
Generic[Index] parameter, so mypy cannot narrow graph.adjacencylist(...
dtype=np.int32)._cpp_object down to AdjacencyList_int32, even though
that's the only variant it can be at runtime. Silence with the same
type: ignore[arg-type] pattern already used elsewhere in graph.py for
this class's untyped _cpp_object.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- mesh::partition_cells: merge the bool failed + int local_failed
  translation step into a single int failed, passed directly to
  MPI_Allreduce.
- graph/partitioners.cpp: the comm-split half of partitioner(),
  repartitioner() and geom_partitioner_kway() was deduplicated into
  split_and_build_node_disp earlier in this PR, but each still
  repeated an identical "ghost, free pcomm, return" block afterwards.
  Factor it into finalise_partition, alongside split_and_build_node_disp.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A deadlocked demo (e.g. an MPI collective mismatch, or the documented
demo_navier-stokes.py MUMPS hang) currently blocks the "Run Python
demos" CI step for hours until the runner's own job timeout kicks in,
with no indication of which demo is responsible. subprocess.run's
timeout raises TimeoutExpired naming the command, giving a fast,
attributable failure instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both demos used to build their gmsh-generated mesh with a partitioner
built via _create_cell_partitioner_from_ghost_mode(GhostMode.shared_facet,
2), which bundled the ghost mode into the partitioner itself. The
"Simplify interfaces" refactor moved to the plain create_cell_partitioner(),
which carries no ghost mode -- that's now controlled separately by
model_to_mesh's own ghost_mode argument, which defaults to
GhostMode.none. Neither demo was updated to pass shared_facet
explicitly, so both silently lost ghosting.

For demo_axis.py this is a real bug, not just a quality regression:
it computes an interior facet integral (dS(scatt_tag)) at line ~717,
which requires shared_facet ghosting. Without it, only ranks whose
local facet happens to be an inter-process facet raise "Cannot compute
interior facet integral over interprocess facet"; the remaining
rank(s) sail past that line and block forever on the next collective
in a later call, hanging CI indefinitely (see
https://github.com/FEniCS/dolfinx/actions/runs/33060291735/job/98477154522,
reproduced locally as a subprocess.TimeoutExpired after the demo test
timeout added in 71af260). demo_pml.py has no interior facet
integral, so it degrades silently rather than hanging, but restoring
shared_facet there too matches its original, evidently intentional,
ghosting choice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
It was a thin one-liner (call create_partitioner_cpp, or return
nullptr if empty) whose null-check was already duplicated at every
call site via has_value()/ternary guards. Call create_partitioner_cpp
directly instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… directly

Since ghost mode and the dual graph are now controlled at create_mesh
call time rather than bundled into the partitioner, both overloads of
create_cell_partitioner had degenerated into pure identity/pass-through
wrappers: create_cell_partitioner() just returned the default graph
partitioner (dolfinx::graph::partition_graph, already exposed as
dolfinx.graph.partitioner()), and create_cell_partitioner(part) did
nothing but round-trip an already Python-callable graph partitioner
through C++ and back.

Remove the Python function and its two C++ nanobind bindings (and the
now-unused create_cell_partitioner_py wrapper they were the only
caller of), and update every call site to pass a graph partitioner --
dolfinx.graph.partitioner(), partitioner_scotch(), etc. -- directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Match the level of detail already given to its siblings
create_geom_partitioner_py/create_hybrid_partitioner_py: state which
dolfinx::graph type it wraps and how its signature differs from theirs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants