Skip to content

Commit a2f497d

Browse files
fluidnumericsJoepre-commit-ci[bot]VeckoTheGecko
authored
Issue 2521 emit warning (#2543)
* Add check for degenerate xgrid faces; emit warning if found * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update src/parcels/_core/spatialhash.py Co-authored-by: Nick Hodgskin <36369090+VeckoTheGecko@users.noreply.github.com> * Update warning language Co-authored-by: Nick Hodgskin <36369090+VeckoTheGecko@users.noreply.github.com> * Add more informative warning; provide first few (j,i) locations * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Nick Hodgskin <36369090+VeckoTheGecko@users.noreply.github.com>
1 parent a0bc6f5 commit a2f497d

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

src/parcels/_core/spatialhash.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import numpy as np
24

35
from parcels._core.index_search import (
@@ -6,6 +8,7 @@
68
curvilinear_point_in_cell,
79
uxgrid_point_in_cell,
810
)
11+
from parcels._core.warnings import FieldSetWarning
912
from parcels._python import isinstance_noimport
1013

1114

@@ -88,6 +91,23 @@ def __init__(
8891
self._zlow = np.min(_zbound, axis=-1)
8992
self._zhigh = np.max(_zbound, axis=-1)
9093

94+
degenerate_mask = _find_degenerate_xgrid_faces(x, y, z)
95+
degeneracy_count = np.sum(degenerate_mask)
96+
if degeneracy_count > 0:
97+
degen_locs = np.argwhere(degenerate_mask) # shape (N, 2), columns are (j, i)
98+
max_shown = np.min([degeneracy_count, 5])
99+
shown = degen_locs[:max_shown]
100+
loc_str = ", ".join(f"(j={loc[0]}, i={loc[1]})" for loc in shown)
101+
warnings.warn(
102+
f"Grid contains {degeneracy_count} degenerate faces that span a large portion of the "
103+
"hash grid. This is most likely due to a mesh that isn't fully defined (e.g., points corresponding to land with lat/lon masked to 0). "
104+
"You may experience runtime crashes due to high memory usage in the hash table or cell lookup failures for particles"
105+
"in the vicinity of these degenerate cells."
106+
f"First degenerate face location(s): {loc_str}.",
107+
FieldSetWarning,
108+
stacklevel=2,
109+
)
110+
91111
else:
92112
# Boundaries of the hash grid are the bounding box of the source grid
93113
self._xmin = self._source_grid.lon.min()
@@ -483,6 +503,53 @@ def _dilate_bits(n):
483503
return n
484504

485505

506+
def _find_degenerate_xgrid_faces(x, y, z, threshold_factor=10):
507+
"""Identify faces in structured grids that potentially span large portions of
508+
the underlying hash grid (e.g., due to the mesh being incomplete, with 0.0 stored in missing lon/lat points). Such degenerate faces can result in high memory requirements
509+
for the hash table.
510+
511+
Detection is based on the maximum great-circle edge length of each cell. A cell
512+
is flagged as degenerate when its longest edge exceeds ``threshold_factor`` multiplied by
513+
the 99th percentile of all edge lengths.
514+
515+
Parameters
516+
----------
517+
x, y, z : ndarray, shape (ny, nx)
518+
Unit-sphere Cartesian coordinates of the grid nodes.
519+
threshold_factor : float, optional
520+
Multiplier applied to the 99th-percentile edge length to set the threshold.
521+
Default is 10.
522+
523+
Returns
524+
-------
525+
degenerate : ndarray of bool, shape (ny-1, nx-1)
526+
True for each cell whose maximum edge length exceeds the threshold.
527+
"""
528+
529+
# Chord length between two sets of points on the unit sphere, shape (ny-1, nx-1)
530+
def _chord(p1, p2):
531+
return np.sqrt(((p1 - p2) ** 2).sum(axis=-1))
532+
533+
pts = np.stack([x, y, z], axis=-1)
534+
c00, c01 = pts[:-1, :-1], pts[:-1, 1:]
535+
c10, c11 = pts[1:, :-1], pts[1:, 1:]
536+
537+
# Maximum chord across all four edges and both diagonals
538+
max_chord = np.maximum.reduce(
539+
[
540+
_chord(c00, c01),
541+
_chord(c10, c11),
542+
_chord(c00, c10),
543+
_chord(c01, c11),
544+
_chord(c00, c11),
545+
_chord(c01, c10),
546+
]
547+
)
548+
549+
threshold = threshold_factor * np.percentile(max_chord, 99)
550+
return max_chord > threshold
551+
552+
486553
def quantize_coordinates(x, y, z, xmin, xmax, ymin, ymax, zmin, zmax, bitwidth=1023):
487554
"""
488555
Normalize (x, y, z) to [0, 1] over their bounding box, then quantize to 10 bits each (0..1023).

0 commit comments

Comments
 (0)