Background
Per the ND<->units boundary contract (added with #267/#278): the DM/PETSc/solver layer is non-dimensional-only; units live above it in the MeshVariable API.
Today that boundary is convention, not enforced, and there is a silent trap: uw.function.evaluate / global_evaluate strip the UnitAwareArray subclass with np.array() but keep the numeric value. So a dimensional coordinate array (.coords ≈ 1e9 m) passed in is kept as 1e9 and located against a 0..1000 ND DM — no exception, wrong location, wrong answer.
This is exactly how #267 slipped in (the SL trace-back fed dimensional .coords into the ND DM). #277 fixed that one call site, but the same class of mistake can recur anywhere a developer reaches for .coords/.array instead of .coords_nd/.data.
Proposal
Make the ND entry points guard the boundary instead of silently stripping:
uw.function.evaluate / global_evaluate: if handed a UnitAwareArray for the query coordinates (or a unit-bearing expression where ND is expected), either
- reject it with a clear error ('pass non-dimensional coordinates — use
mesh.X.coords_nd / var.coords_nd, or uw.non_dimensionalise(...)'), or
- correctly non-dimensionalise it (not just strip the label) before locating.
- Consider the same guard on the DM coordinate setters / any other ND entry point.
Rejecting is the safer default (forces the caller to be explicit); auto-nondimensionalising is more convenient but hides the boundary. Prefer reject-with-actionable-message unless there's a strong ergonomic case.
Acceptance
A regression test that passing a dimensional UnitAwareArray of coordinates to evaluate/global_evaluate no longer silently mislocates (errors clearly, or returns the correctly-located result).
Tracked from the #267 boundary-contract discussion.
Underworld development team with AI support from Claude Code
Background
Per the ND<->units boundary contract (added with #267/#278): the DM/PETSc/solver layer is non-dimensional-only; units live above it in the MeshVariable API.
Today that boundary is convention, not enforced, and there is a silent trap:
uw.function.evaluate/global_evaluatestrip theUnitAwareArraysubclass withnp.array()but keep the numeric value. So a dimensional coordinate array (.coords≈ 1e9 m) passed in is kept as 1e9 and located against a 0..1000 ND DM — no exception, wrong location, wrong answer.This is exactly how #267 slipped in (the SL trace-back fed dimensional
.coordsinto the ND DM). #277 fixed that one call site, but the same class of mistake can recur anywhere a developer reaches for.coords/.arrayinstead of.coords_nd/.data.Proposal
Make the ND entry points guard the boundary instead of silently stripping:
uw.function.evaluate/global_evaluate: if handed aUnitAwareArrayfor the query coordinates (or a unit-bearing expression where ND is expected), eithermesh.X.coords_nd/var.coords_nd, oruw.non_dimensionalise(...)'), orRejecting is the safer default (forces the caller to be explicit); auto-nondimensionalising is more convenient but hides the boundary. Prefer reject-with-actionable-message unless there's a strong ergonomic case.
Acceptance
A regression test that passing a dimensional
UnitAwareArrayof coordinates toevaluate/global_evaluateno longer silently mislocates (errors clearly, or returns the correctly-located result).Tracked from the #267 boundary-contract discussion.
Underworld development team with AI support from Claude Code