Skip to content

Cmd/esmf overflow bug - #1659

Draft
cmdupuis3 wants to merge 6 commits into
mainfrom
cmd/esmf_overflow_bug
Draft

Cmd/esmf overflow bug#1659
cmdupuis3 wants to merge 6 commits into
mainfrom
cmd/esmf_overflow_bug

Conversation

@cmdupuis3

@cmdupuis3 cmdupuis3 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #1658

Overview

Several related bugs in how fill values are used in IO routines. Original case is the first one, where a downcast of INT_FILL_VALUE results in valid indices, resulting in spurious extra nodes and incorrect polygons.

# File Line (was) Function Error mode
1 uxarray/io/_esmf.py 148 _encode_esmf +1 applied to fill slots, then int32 encoding truncates -2**63+11. Padding written as node 0.
2 uxarray/io/_esmf.py 96 _read_esmf Cast to int before fill check. Relies on NaN → INT64_MIN; arm64 gives 0, so padding decodes to -1 — a negative index that wraps.
3 uxarray/io/_exodus.py 204 _encode_exodus Matched padding on == -1, but padding is INT_FILL_VALUE. Never fired → one max-width block, wrong element types, INT_FILL_VALUE+1 written as an index.
4 uxarray/io/_exodus.py _encode_exodus (exposed by #3) Block regrouping reorders faces with no elem_num_map; face-centered data silently misaligns.
5 uxarray/io/_scrip.py 213 _encode_scrip NaN written to padded corners; reader deduplicates it into a real node. n_node 5→6, NaN coordinates.

PR Checklist

General

  • An issue is created and linked
  • Added appropriate labels (if your uxarray repo permissions allow it)
  • Filled out Overview and Expected Usage (if applicable) sections

Testing & Benchmarking

  • Adequate tests are created if there is new functionality
  • Tests are not too basic (such as simply calling a function and nothing else)
  • Tests cover all major paths in your new functions
  • If this PR could affect performance, ran ASV benchmarks and confirmed they show expected behavior (add a new benchmark if necessary)

Documentation

  • Docstrings have been added to all new functions
  • Docstrings have been updated with any function changes
  • User (public) functions have been added to docs/api.rst
  • Internal (private) function names start with an underscore (_)

AI Disclosure

Claude Opus 5, for bug search and generated code.

AI Usage:

  • I take responsibility for all AI-generated content in my PR.
  • I have tested all AI-generated content in my PR.

The ESMF writer applied the 1-based index offset to every entry of
face_node_connectivity, including padded slots, then encoded the result
as int32. INT_FILL_VALUE + 1 (-2**63 + 1) truncates to 1 under that cast,
so padding was written as a valid node index instead of the declared
_FillValue of -1. Ragged grids silently gained vertices: a triangle
padded to width 4 was written as a quad whose extra vertex was node 0.
The numElementConn fallback, which counts entries != -1, was wrong for
the same reason and reported every face at maximum width.

The reader had the mirror-image defect. CF decoding turns the on-disk
fill into NaN, and the code cast straight to INT_DTYPE and compared
against INT_FILL_VALUE. That comparison only holds where NaN casts to
INT64_MIN; on arm64 it casts to 0, so padding decoded to -1 -- a
negative index that silently wraps to the last node rather than raising.

Mask the padding explicitly on both sides. Existing ESMF fixtures are
all pure-quad meshes with no padding, which is why the round-trip test
never exercised this.
_encode_exodus searched for padding with `row == -1`, but connectivity
padding is stored as INT_FILL_VALUE. The comparison never matched, so
every face was treated as full width: mixed meshes were written as a
single block typed for the widest face, the per-block element counts
were wrong, and the padding itself was written out as a node index of
INT_FILL_VALUE + 1.

uxarray's own reader happened to invert that -- Exodus connectivity is
int64, so the writer's +1 and the reader's -1 cancel exactly at
INT_FILL_VALUE -- which is why the round-trip test passed. The emitted
file is still not valid Exodus for any other consumer.

Match on INT_FILL_VALUE so faces are grouped into correctly typed
blocks. Because Exodus blocks are homogeneous, that regroups a mixed
mesh, so also write elem_num_map recording each element's original
position and have _read_exodus invert it when it is a true permutation.
Without that the faces come back reordered and any face-centered data
silently misaligns -- a worse failure than the one being fixed.
_encode_scrip wrote NaN into grid_corner_lat/lon for every padded slot.
SCRIP has no fill value for corners, so on read-back those NaNs dedupe
into a real node: a grid with one padded triangle came back with an
extra node whose coordinates are NaN, inflating n_node and feeding NaN
into every downstream geometry calculation.

Write the SCRIP-conventional degenerate polygon instead, repeating the
face's last valid corner into the padded slots. Node count and node
coordinates now round-trip correctly.

Note this is not an exact connectivity round-trip: the padded face comes
back as a degenerate quad with a repeated vertex rather than a triangle,
which is what SCRIP can express. Collapsing trailing duplicate corners
back to INT_FILL_VALUE would need a reader change affecting every
existing SCRIP file, including legitimately degenerate ones.
Every writable format shared the same untested path: connectivity padding
on a mesh with mixed face sizes. All the ESMF, Exodus, and SCRIP fixtures
are uniform meshes that pad nothing, so the encoders' fill-value handling
was never exercised and three separate corruption bugs went unnoticed.

Add TestIOWriteRoundTrip in test_io_common.py, parametrized over the
WRITABLE_FORMATS list that was already defined there but unused. One
ragged fixture (a quad and two triangles) now covers all four writers:
node count and coordinates must survive, no negative leftovers may
remain in the connectivity, and the index-based formats must restore it
verbatim including face order.

SCRIP is held to a weaker contract. It stores corner coordinates rather
than indices, so its reader renumbers nodes and a short face round-trips
as a degenerate polygon; _face_geometry compares faces by coordinate
instead of by index so it can still be checked.

Round-trip assertions alone miss the Exodus bug: connectivity there is
int64, so the writer's +1 and the reader's -1 cancel exactly at
INT_FILL_VALUE and the grid reloads intact from a file holding an index
no other reader could use. test_ragged_grid_encodes_usable_indices
inspects the encoded output directly to catch it.

Consolidation: test_esmf_round_trip_consistency was 68 lines of manual
file handling covering one format on a uniform mesh, now subsumed by the
parametrized version and reduced to a structural check. The empty
test_encode_exodus placeholder is filled in, and the Exodus block
splitting and elem_num_map get their own test alongside it.

Verified by reverting all three fixes: 6 of these fail, covering each bug.
@cmdupuis3 cmdupuis3 added the bug Something isn't working label Aug 7, 2026
@cmdupuis3

Copy link
Copy Markdown
Collaborator Author

pre-commit.ci autofix

@cmdupuis3 cmdupuis3 moved this to 📝 To-Do in UXarray Development Aug 8, 2026
@cmdupuis3 cmdupuis3 moved this from 📝 To-Do to 🏗 In progress in UXarray Development Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: 🏗 In progress

Development

Successfully merging this pull request may close these issues.

INT_FILL_VALUE overflows in downcast, corrupting polygon meshes

1 participant