|
| 1 | +import warnings |
| 2 | + |
1 | 3 | import numpy as np |
2 | 4 |
|
3 | 5 | from parcels._core.index_search import ( |
|
6 | 8 | curvilinear_point_in_cell, |
7 | 9 | uxgrid_point_in_cell, |
8 | 10 | ) |
| 11 | +from parcels._core.warnings import FieldSetWarning |
9 | 12 | from parcels._python import isinstance_noimport |
10 | 13 |
|
11 | 14 |
|
@@ -88,6 +91,23 @@ def __init__( |
88 | 91 | self._zlow = np.min(_zbound, axis=-1) |
89 | 92 | self._zhigh = np.max(_zbound, axis=-1) |
90 | 93 |
|
| 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 | + |
91 | 111 | else: |
92 | 112 | # Boundaries of the hash grid are the bounding box of the source grid |
93 | 113 | self._xmin = self._source_grid.lon.min() |
@@ -483,6 +503,53 @@ def _dilate_bits(n): |
483 | 503 | return n |
484 | 504 |
|
485 | 505 |
|
| 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 | + |
486 | 553 | def quantize_coordinates(x, y, z, xmin, xmax, ymin, ymax, zmin, zmax, bitwidth=1023): |
487 | 554 | """ |
488 | 555 | Normalize (x, y, z) to [0, 1] over their bounding box, then quantize to 10 bits each (0..1023). |
|
0 commit comments