Skip to content

Meshes/Domains: range-check vertex indices, promote stretching and sphere-radius parameters - #2613

Open
imreddyTeja wants to merge 3 commits into
tr/geometry-coordinatesfrom
tr/meshes-fixes
Open

Meshes/Domains: range-check vertex indices, promote stretching and sphere-radius parameters#2613
imreddyTeja wants to merge 3 commits into
tr/geometry-coordinatesfrom
tr/meshes-fixes

Conversation

@imreddyTeja

Copy link
Copy Markdown
Member

Stack 2/4 — base tr/geometry-coordinates (#2612). Next: tr/space-ndims.
Review only the last three commits; the first four belong to #2612.

1. Meshes.coordinates does not range-check the vertex index (#1026)

coordinates(mesh, elem, vert) picks a vertex with a chain of vert == ...
comparisons, so an out-of-range vert fell through to the last branch and
returned another vertex's coordinates instead of erroring:

julia> Meshes.coordinates(mesh, CartesianIndex(1,1,1), 5)   # returns vertex 3
Cartesian123Point(2.5885686283830505, -1.0722202330097799, -1.0722202330097799)

Added a shared check_vertex_index, called from the AbstractMesh2D,
AbstractCubedSphere and RectilinearMesh methods (vert ∈ 1:4).

IntervalMesh has the same bug in 1D — coordinates(mesh, 1, 3) indexes past
the element's two faces into the next element's, so on a 2-element mesh it
returned element 2's upper face — so it is checked there too (vert ∈ 1:2).

Two things worth a reviewer's attention:

  • This changes the error for an out-of-range 1D vert from BoundsError to
    ArgumentError, which two existing tests asserted on. ArgumentError matches
    how unit_interval.jl already reports other invalid parameters (nelems).
  • It turned up five call sites in the interval tests that were passing a face
    index as vert
    coordinates(mesh, 1, nelems), coordinates(mesh, 1, nelems + 1), and vert = 28 in "Truncated IntervalMesh". They only worked
    because the index was unchecked. They are rewritten as the equivalent in-range
    (elem, vert) pair; every asserted value is unchanged.

2. No IntFloat promotion for stretching rules (#1434)

GeneralizedExponentialStretching(30, 5000.0) raised a MethodError, and so
did IntervalMesh(domain, ExponentialStretching(7500)) on a Float64 domain —
the rule had no promoting constructor, and the IntervalMesh methods pinned the
rule's float type to the domain's with a shared FT typevar.

  • GeneralizedExponentialStretching now promotes mixed parameters. It has to
    call the parametric constructor rather than recursing: a (::Real, ::Real)
    method is more specific than the generated diagonal (::FT, ::FT) where {FT} (whose typevar is implicitly bounded by Any), so every Real pair
    dispatches to it and a plain promote(...) forward stack-overflows. I hit
    this; the reasoning is in a source comment.
  • IntervalMesh now accepts a rule of any float type and converts it to the
    domain's via the new Meshes.convert_stretch. This also covers a Float64
    rule on a Float32 domain, not just integers. The mesh stores the converted
    rule, so mesh.stretch always matches the domain.

3. SphereDomain does not support Ints (#1468)

SphereDomain(1000) built a SphereDomain{Int}. The intended constraint was
written

struct SphereDomain{FT} <: AbstractDomain where {FT <: AbstractFloat}

where the where clause attaches to the (unparameterized) supertype and is
silently discarded — so it never applied. The radius fixes the float type of
everything built on the domain, so this surfaced far away as no method matching legendre(::Type{Int64}, ...) when the space's quadrature rule was built. The
bound is moved onto the type parameter, and a non-float Real radius is
promoted rather than rejected.

Testing

The reproducers from #1434 and #1468 both run; the #1468 sphere space integrates
to 4πR². Domains, Meshes, Geometry, Spaces, Fields, InputOutput
(including unit_hybrid2dbox_stretched) and Remapping unit tests pass.

Closes #1026, closes #1434, closes #1468

🤖 Generated with Claude Code

https://claude.ai/code/session_01HJ8VbwbMadrhvCtBV3jkjc

imreddyTeja and others added 3 commits August 31, 2026 14:44
`Meshes.coordinates(mesh, elem, vert)` selects a vertex with a chain of
`vert == ...` comparisons, so an out-of-range `vert` fell through to the
last branch and returned another vertex's coordinates instead of erroring:
`coordinates(mesh, CartesianIndex(1,1,1), 5)` returned vertex 3. Add a
shared `check_vertex_index` and call it from the `AbstractMesh2D`,
`AbstractCubedSphere` and `RectilinearMesh` methods (`vert` in `1:4`).

`IntervalMesh` has the same bug in 1D — `coordinates(mesh, 1, 3)` indexes
past the element's two faces into the next element's, so on a 2-element
mesh it returned element 2's upper face — so check it there too (`1:2`).

That turned up four call sites in the interval tests that were passing a
*face* index as `vert` (`coordinates(mesh, 1, nelems)`, and `vert = 28` in
"Truncated IntervalMesh"). They only worked because the index was
unchecked; rewrite them as the equivalent in-range `(elem, vert)` pair.
The values asserted are unchanged.

Note this changes the error type for an out-of-range 1D `vert` from
`BoundsError` to `ArgumentError`, which two existing tests asserted on.

Closes #1026

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJ8VbwbMadrhvCtBV3jkjc
`GeneralizedExponentialStretching(30, 5000.0)` raised a `MethodError`, and
so did `IntervalMesh(domain, ExponentialStretching(7500))` on a `Float64`
domain: the two-parameter rule had no promoting constructor, and the
`IntervalMesh` methods pinned the rule's float type to the domain's with a
shared `FT` typevar.

- `GeneralizedExponentialStretching` now promotes mixed parameters. Note it
  has to call the parametric constructor rather than recursing: a `(::Real,
  ::Real)` method is *more* specific than the generated diagonal `(::FT,
  ::FT) where {FT}` (whose typevar is implicitly bounded by `Any`), so every
  `Real` pair dispatches to it and `promote` alone would not terminate.
- `IntervalMesh` now accepts a stretching rule of any float type and
  converts it to the domain's via the new `Meshes.convert_stretch`. This
  also covers a `Float64` rule on a `Float32` domain, not just integers.

The mesh stores the converted rule, so `mesh.stretch` always matches the
domain's float type.

Closes #1434

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJ8VbwbMadrhvCtBV3jkjc
`SphereDomain(1000)` built a `SphereDomain{Int}`. The declared constraint
was written `struct SphereDomain{FT} <: AbstractDomain where {FT <:
AbstractFloat}`, where the `where` clause attaches to the (unparameterized)
supertype and is silently discarded, so it never applied.

The radius is what fixes the float type of everything built on the domain,
so this surfaced far away as `no method matching legendre(::Type{Int64},
::Int64, ::GaussQuadrature.EndPt)` when the space's quadrature rule was
built. Move the bound onto the type parameter where it binds, and promote a
non-float `Real` radius instead of rejecting it.

Closes #1468

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJ8VbwbMadrhvCtBV3jkjc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant