Skip to content

Spatialhash regional spherical improvements#2744

Open
fluidnumericsJoe wants to merge 6 commits into
mainfrom
spatialhash-regional-spherical-improvements
Open

Spatialhash regional spherical improvements#2744
fluidnumericsJoe wants to merge 6 commits into
mainfrom
spatialhash-regional-spherical-improvements

Conversation

@fluidnumericsJoe

Copy link
Copy Markdown
Contributor

Description

This PR changes the spatialhash initialization for spherical mesh types. Previously the bounding box was set to the unit cube for spherical mesh types. However, spherical mesh types might be desirable for regional simulations (e.g. for improved accuracy in barycentric coordinates, dealing with regions close to the poles, or for leveraging built in unit conversions in Parcels velocity field interpolators).

The main problem with the unit cube for regional configurations is that the hash grid resolution is often far too coarse for a regional domain. This results in a high number of mesh cells being aligned with a single morton key. Calling spatialhash.query() then results in an abnormally high number of particle in cell checks.

This is remedied here by choosing the extents of the hash grid as the bounding cube (x,y,z) of the regional domain.

Checklist

  • Tests added
  • This PR targets the correct branch (main for normal development, v3-support for v3 support)

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.
    • I take responsibility for any AI-generated content in my PR.
    • Describe how you used it (e.g., by pasting your prompt): Claude Code was used to identify and implement documentation changes.

When the mesh type is spherical, we now use the actual x,y,z bounding
box rather than the unit cube. For regional runs that leverage the
spherical barycentric coordinates and built in unit conversion in the
velocity field interpolators, this reduces the hit count per hash cell
and can improve the search time in the spatialhash.query()

Note that quantize_coordinates now clips in float space before casting to uint32.
This matters now that spherical queries can fall outside the hash-grid bounds
(previously impossible with the unit cube). A negative normalized value cast to
uint32 wraps to a huge number, which would have mapped below-range queries to the
top bin (with morton code 2^32) erroneously.

For spherical grids, points outside the regional box now quantize to edge bins
rather than interior bins. In this case, they either fail the exact Morton-key
match or are rejected by the point-in-cell check; either case results in the
same outcome as before (GRID_SEARCH_ERROR)
Tests for both points inside the domain and explicitly outside the domain
to confirm correct behavior for out of bounds PIC checks
@fluidnumericsJoe

Copy link
Copy Markdown
Contributor Author

I still need to test this out on the example you shared @erikvansebille, but wanted to get this in and make sure that existing tests all pass

@fluidnumericsJoe

Copy link
Copy Markdown
Contributor Author

looks like there's a few tests to clean up here.

@erikvansebille

Copy link
Copy Markdown
Member

Thanks for this PR, @fluidnumericsJoe! Seems like there are some segmentation errors in the tests. Do you want to try fix them; or can I help?

@fluidnumericsJoe

Copy link
Copy Markdown
Contributor Author

Thanks for this PR, @fluidnumericsJoe! Seems like there are some segmentation errors in the tests. Do you want to try fix them; or can I help?

I'll get to it today

@fluidnumericsJoe

Copy link
Copy Markdown
Contributor Author

So, I've narrowed down what's going on here. To recap what we primarily changed here - we've changed the spatialhash initialization to use a more narrow bounding box, based on the actual x,y,z extents for spherical mesh types. effectively, for regional domains this makes the hash grid size smaller. This happens because the number of hash grid cells is fixed, but we're now using a smaller domain extent for the hash grid.

The upshot of this change is that we have fewer faces in the "parent" mesh that we need to check, when a particle is found in a given hash cell. This reduces the number of calls to the particle in-cell checks.

The issue here is in the construction of the hash table that maps a hash cell index (the morton code) to a list of faces. During construction we have to use the bounding box of a face in the parent grid to find the range of hash cells that overlap. This information is used to pre-allocate a lookup table that maps from parent grid face to hash grid cells; the hash table is contructed as the inverse of that table. If the hash cells are significantly smaller than the parent model face sizes, we end up with a fairly large memory footprint (high number of hash cells per face).

I'm looking at tidying up that initializer here to reduce the memory footprint..

@fluidnumericsJoe

Copy link
Copy Markdown
Contributor Author
hashgrid_overview This figure shows the antimeridian spanning grid that the CI is failing on with OOM errors. The grid locations are plotted in cartesian (x,y,z) space after mapping the latitude, longitude through a spherical coordinate transformation. A box shows a region that we'll zoom in on in the next figure hashgrid_zoom This zoomed in region shows the hash grid (light gray lines) and the parent grid edges (blue lines). The bouding cube for the cell shaded light blue has 55x59x46 hash cells. Summing the number of hash cells per face across all faces (to get the size of the work arrays for constructing the hash table), each work array is roughly 193 million entries (too big).

In addition to tidying up in the initializer, I'll probably look at putting in a lever to manipulate the hash cell size - with the morton cells, this is controlled by the bit width . A higher bitwidth gives more hash cells per direction. At the moment, we default to 1024.

Another thing we could possibly do is be a bit smarter about tossing out hash cells that are clearly outside of elements (bounding box overlaps are kind of wasteful). In both plane views in the zoomed in plot, there's a large number of hash cells that are completely outside the element.

fluidnumericsJoe and others added 2 commits July 15, 2026 15:55
num_hash_per_face is never zero (quantization is monotone), so the valid
mask was always all-True and the scatter an identity permutation.
Removing them drops three entry-length temporaries (~35% peak build
memory). Accumulate entry counts in int64 to avoid overflow.
Hash table output verified byte-identical.

Co-authored-by: Claude <noreply@anthropic.com>
…e build

Fusing each pair as (code << 32) | face_id and sorting in place replaces
argsort, its int64 permutation array, and two gather passes. Pairs are
unique so the ordering is deterministic (ties by ascending face id).
Also free decode temporaries before the sort. Peak build memory drops
17.2 GB -> 6.35 GB (with previous commit) and build time 40 s -> 32 s on
a 101M-entry spherical UxGrid table. CSR arrays verified byte-identical;
per-cell face sets verified identical.

Co-authored-by: Claude <noreply@anthropic.com>
@fluidnumericsJoe

Copy link
Copy Markdown
Contributor Author

I pushed up a couple of commits created with claude that focus on memory footprint reduction in the spatialhash._initialize_hash_table method.

The first commit removes a conditional path that is never traversed; that branch would only be executed if a face is found that doesn't overlap a single hash cell. Since the hash grid bounds are the bounding box of the whole grid, it's safe to assume each face will overlap at least 1 hash cell. Relying on this assumption allows us to trim back on a number of intermediate arrays.

The second commit clears previously used intermediate arrays from memory after all the morton codes are calculated. Then since an entry in the hash table lookup consists of the morton code with a corresponding face id and each is a uint32, we can store the combination is a single uint64. This allows us to use a single sort call, rather than an argsort followed by a sort (as we previously did). This single sort gives us a list of integers sorted by the morton code first then face id.

Using the fesom2_square_delaunay_uniform_z_coordinate example dataset, I tracked the max memory usage of a benchmark that just loads the dataset and constructs the spatialhash object.

main 34af903 35dd7d9
peak build RSS (101M-entry UxGrid) 17.2 GB 11.3 GB 6.35 GB

Even with these improvements, I still think we might need to provide controls to coarsen the hash grid.

@erikvansebille

Copy link
Copy Markdown
Member

Thanks for this further digging, @fluidnumericsJoe! I must say I don't entirely understand what's going on, but taking a step back I think this is the problem:

  1. We had a working morton/hashing procedure for full-sphere (global) grids before this PR

  2. For grids with small, regional domains, this procedure is suboptimal because many of the global hash cells (those outside the grid) remain empty - and other hash cells have lots of entries

  3. The obvious solution is to bound the hashtable by the grid extent; which is what this PR implements

    (and the next step is where I'm unclear)

  4. However, this creates problems with grids that cross the antemeridian (right?)

    (If so, is the following approach suitable?)

  5. We keep the original full-sphere/global hashtable for any grid spanning the antemeridian - and use a bounding box hashtable for regional grids

  6. If users do want to improve their hash performance on regional grids across the antemeridian, they could(?) do a coordinate transformation from longitude [180, 180] to [0, 360]. Since these are per definition regional grids, the grid would then not span the antemeridian anymore?

  7. The only remaining issue is polar caps (so spanning all longitudes but only a limited range of latitudes). For that, this wouldn't be a solution

@fluidnumericsJoe

fluidnumericsJoe commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for this further digging, @fluidnumericsJoe! I must say I don't entirely understand what's going on, but taking a step back I think this is the problem:

  1. We had a working morton/hashing procedure for full-sphere (global) grids before this PR

It was working. Reducing the extent of the hash grid ballooned the number of hash cells per face. This happens because reducing the hash grid extent while keeping the number of hash cells the same causes the hash cells to become smaller. With smaller hash cells, more of them are withing each parent mesh face. The number of hash cells per face determines the size of work arrays during hash table construction. For some grids, this led to oom errors during construction. Now the game is to reduce the memory footprint of the hash table construction.

  1. For grids with small, regional domains, this procedure is suboptimal because many of the global hash cells (those outside the grid) remain empty - and other hash cells have lots of entries

Ultimately, once the hash table is constructed, since we use a compressed sparse row matrix format, hash entries that have no face overlaps don't get stored in the lookup table.

  1. The obvious solution is to bound the hashtable by the grid extent; which is what this PR implements

Yes. This optimizes query, but balloons the memory footprint for the hash table creation

_(and the next step is where I'm unclear)_
  1. However, this creates problems with grids that cross the antemeridian (right?)
    It just so happened that I caught it on the anti meridian grid. Nothing about the anti meridian crossing was part of the problem. The faces are quite coarse resolution relative to the hash cells after reducing the hash grid extent. Again,the number of hash cells per face became quite large causing work arrays used in hash table construction to balloon in size. I found other grids where the memory footprint went up as well during construction, but just happened to be below the allotted memory in the GitHub runners.
_(If so, is the following approach suitable?)_
  1. We keep the original full-sphere/global hashtable for any grid spanning the antemeridian - and use a bounding box hashtable for regional grids
    No. That's not necessary. I've reduced the memory footprint of the hash table creation by removing unnecessary work arrays. This also speeds up hash table construction. The other route is to provide an option to coarsen the hash grid; this could be implemented by using a lower bit width.
  1. If users do want to improve their hash performance on regional grids across the antemeridian, they could(?) do a coordinate transformation from longitude [180, 180] to [0, 360]. Since these are per definition regional grids, the grid would then not span the antemeridian anymore?

Antimeridian is not the issue. It's a ratio of hash grid cell size to parent mesh face/cell size.

  1. The only remaining issue is polar caps (so spanning all longitudes but only a limited range of latitudes). For that, this wouldn't be a solution

This is not an issue..for polar caps we have to use spherical mesh types. Flat mesh types would end up with colinear cells . The changes made in this pr will improve construction time of the hash grid for these cases and help reduce the number of particle in cell checks.

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

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants