Skip to content

Cell size: one definition, one name — three are in play, the docstrings describe a fourth, and get_*_radius() is partition-dependent #694

Description

@lmoresi

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:

  1. 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.
  2. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions