The defect
get_min_radius(), get_max_radius() and get_mean_radius() present as the
canonical global "mesh length" API. Their docstrings say so explicitly:
get_min_radius: "the smallest cell anywhere in the mesh, not just on this
rank. Parallel-safe via MPI allreduce."
get_max_radius: "the largest cell anywhere in the mesh."
get_mean_radius: "Use this anywhere you need a representative h0 ...
rather than reaching for the rank-local self._radii array, which gives
different answers on different MPI ranks and leaks downstream."
All three reduce over self._radii — the array that last docstring warns
against. The allreduce makes the answer identical on every rank. It does not
make it identical for every rank count, because the values being reduced are
themselves partition-dependent: _radii comes from a kd-tree over this rank's
centroids, so a cell near a partition boundary gets a different size depending
on how the mesh was split.
"Same on every rank" and "same for every rank count" are different properties.
These functions deliver the first and their names and docstrings promise the
second.
Measured
UnstructuredSimplexBox, cellSize=0.12, 198 cells, on development:
| accessor |
np=1 |
np=2 |
np=4 |
np=8 |
get_min_radius() |
0.0502908499358 |
same |
same |
same |
get_max_radius() |
0.0670934714626 |
same |
same |
0.070373950134 |
get_mean_radius() |
0.0592989430362 |
0.0594222426856 |
0.0594775301273 |
0.0597593860929 |
get_max_radius() moves by 4.9% at np=8. get_mean_radius() moves at every
rank count. get_min_radius() is stable here — but incidentally, because the
minimising cell does not happen to sit where the kd-tree lookup differs, not
because anything guarantees it.
Ruling (maintainer, 2026-09-05)
These ought to be rank-independent — that is part of the parallel contract they
advertise. If a quantity is local-only, its name has to say so.
The fix is already half-built
#692 adds _radii_own: each cell's characteristic length from its own
vertices and centroid, which cannot depend on the partition. Measured on that
branch, per-cell values are bit-identical at np=1/2/4 (max abs diff 0.000e+00)
where the legacy field differs by 3.3e-03 and 4.1e-03.
Building the three accessors on _radii_own makes all of them genuinely
rank-count-independent and makes the docstrings true.
Two things to get right when doing it:
min and max become exactly reproducible; mean needs care. A
distributed sum reduces in partition order, so even over identical per-cell
values the mean can differ in the last bits between rank counts. If
bit-identity is wanted rather than agreement to ~1e-16, the reduction has to
be made order-independent.
- Timesteps are currently insulated, and that should not be relied on.
Every estimate_dt path reduces to a global minimum, and estimate_dt()
measures bit-identical at np=1/2/4/8. It is stable for the same incidental
reason get_min_radius() is.
Also
follow_metric's docstring in src/underworld3/meshing/smoothing/api.py
recommends gradient_smoothing_length=2.0 * mesh._radii.mean() — a rank-local
mean, a different value on every rank. Docstring only, but it teaches exactly the
pattern get_mean_radius's docstring warns against.
Related: #569 and #687 (the same defect in mesh.cell_size(), fixed by #692).
The defect
get_min_radius(),get_max_radius()andget_mean_radius()present as thecanonical global "mesh length" API. Their docstrings say so explicitly:
get_min_radius: "the smallest cell anywhere in the mesh, not just on thisrank. Parallel-safe via MPI allreduce."
get_max_radius: "the largest cell anywhere in the mesh."get_mean_radius: "Use this anywhere you need a representative h0 ...rather than reaching for the rank-local
self._radiiarray, which givesdifferent answers on different MPI ranks and leaks downstream."
All three reduce over
self._radii— the array that last docstring warnsagainst. The allreduce makes the answer identical on every rank. It does not
make it identical for every rank count, because the values being reduced are
themselves partition-dependent:
_radiicomes from a kd-tree over this rank'scentroids, so a cell near a partition boundary gets a different size depending
on how the mesh was split.
"Same on every rank" and "same for every rank count" are different properties.
These functions deliver the first and their names and docstrings promise the
second.
Measured
UnstructuredSimplexBox,cellSize=0.12, 198 cells, ondevelopment:get_min_radius()get_max_radius()get_mean_radius()get_max_radius()moves by 4.9% at np=8.get_mean_radius()moves at everyrank count.
get_min_radius()is stable here — but incidentally, because theminimising cell does not happen to sit where the kd-tree lookup differs, not
because anything guarantees it.
Ruling (maintainer, 2026-09-05)
These ought to be rank-independent — that is part of the parallel contract they
advertise. If a quantity is local-only, its name has to say so.
The fix is already half-built
#692 adds
_radii_own: each cell's characteristic length from its ownvertices and centroid, which cannot depend on the partition. Measured on that
branch, per-cell values are bit-identical at np=1/2/4 (max abs diff 0.000e+00)
where the legacy field differs by 3.3e-03 and 4.1e-03.
Building the three accessors on
_radii_ownmakes all of them genuinelyrank-count-independent and makes the docstrings true.
Two things to get right when doing it:
minandmaxbecome exactly reproducible;meanneeds care. Adistributed sum reduces in partition order, so even over identical per-cell
values the mean can differ in the last bits between rank counts. If
bit-identity is wanted rather than agreement to ~1e-16, the reduction has to
be made order-independent.
Every
estimate_dtpath reduces to a global minimum, andestimate_dt()measures bit-identical at np=1/2/4/8. It is stable for the same incidental
reason
get_min_radius()is.Also
follow_metric's docstring insrc/underworld3/meshing/smoothing/api.pyrecommends
gradient_smoothing_length=2.0 * mesh._radii.mean()— a rank-localmean, a different value on every rank. Docstring only, but it teaches exactly the
pattern
get_mean_radius's docstring warns against.Related: #569 and #687 (the same defect in
mesh.cell_size(), fixed by #692).