Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add spatial hashing #1169

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
89542c9
Add initial draft of SpatialHash class (triangular meshes only)
fluidnumerics-joe Feb 18, 2025
3093d48
Add `get_spatial_hash` to grid api
fluidnumerics-joe Feb 24, 2025
46dcdc6
Add example usage in spatial hashing docs
fluidnumerics-joe Feb 24, 2025
77c47c6
Merge branch 'UXARRAY:main' into main
fluidnumerics-joe Feb 24, 2025
63048b5
Add spatialhashing to userguide
fluidnumerics-joe Feb 26, 2025
3831793
Merge branch 'main' of github.com:FluidNumerics/uxarray
fluidnumerics-joe Feb 26, 2025
b79bc8b
Add basic tests
fluidnumerics-joe Feb 26, 2025
86d2d5f
Fix call to get_spatialhash
fluidnumerics-joe Feb 27, 2025
b7f3ca3
Merge branch 'main' into main
fluidnumerics-joe Feb 27, 2025
8d6e47e
Fix formatting
fluidnumerics-joe Feb 27, 2025
72c1ed6
Merge branch 'main' of github.com:FluidNumerics/uxarray
fluidnumerics-joe Feb 27, 2025
5a59918
Prepend attributes intended for internal use with _
fluidnumerics-joe Feb 27, 2025
e1adc37
Add a few patches for hashtable setup and query check
fluidnumerics-joe Feb 27, 2025
95cba52
Fix docstrings for tests; add fesom test with particle list
fluidnumerics-joe Feb 27, 2025
7afca52
Add docstring for get_spatialhash
fluidnumerics-joe Mar 3, 2025
d599276
remove empty notes
fluidnumerics-joe Mar 3, 2025
52f1728
Update uxarray/grid/grid.py
fluidnumerics-joe Mar 3, 2025
dfa4f77
Fix naming in test
fluidnumerics-joe Mar 3, 2025
4eac186
Merge branch 'main' of github.com:FluidNumerics/uxarray
fluidnumerics-joe Mar 3, 2025
8ea867d
Update uxarray/grid/neighbors.py
fluidnumerics-joe Mar 3, 2025
7197320
Update uxarray/grid/neighbors.py
fluidnumerics-joe Mar 3, 2025
254cfb6
Update test/test_spatial_hashing.py
fluidnumerics-joe Mar 3, 2025
d7dbbcb
Fix docstring for reconstruct field
fluidnumerics-joe Mar 3, 2025
7e21f0d
Update uxarray/grid/grid.py
fluidnumerics-joe Mar 3, 2025
5596cdb
Add example to docstring
fluidnumerics-joe Mar 4, 2025
0c0dd60
Use waschpress points for generalized barycentric coordinates
fluidnumerics-joe Mar 4, 2025
60e670a
Switch to mpas example in notebook
fluidnumerics-joe Mar 4, 2025
6fd8e37
Update uxarray/grid/grid.py
fluidnumerics-joe Mar 4, 2025
fc59591
Retain output in notebook. Make requested formatting changes
fluidnumerics-joe Mar 4, 2025
7ec453d
Merge branch 'main' of github.com:FluidNumerics/uxarray
fluidnumerics-joe Mar 4, 2025
8db36e6
Fix formatting
fluidnumerics-joe Mar 4, 2025
fb4bbf8
Change to smaller/simpler 4xhex grid
fluidnumerics-joe Mar 4, 2025
b655a46
Add get_spatial_hash to the Grid/Methods section
fluidnumerics-joe Mar 4, 2025
37034ae
Clear outputs from notebook.
fluidnumerics-joe Mar 4, 2025
63e8b29
Merge remote-tracking branch 'upstream/main'
fluidnumerics-joe Mar 4, 2025
e7ab031
Fix docstring; we're not computing signed area any longer.
fluidnumerics-joe Mar 4, 2025
b318f40
Update uxarray/grid/neighbors.py
fluidnumerics-joe Mar 7, 2025
305b8e3
Update uxarray/grid/neighbors.py
fluidnumerics-joe Mar 7, 2025
5fcd039
Add query optimization suggested by @philipc2
fluidnumerics-joe Mar 7, 2025
61fcc1a
Remove extra return statement
fluidnumerics-joe Mar 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ Methods
Grid.calculate_total_face_area
Grid.normalize_cartesian_coordinates
Grid.construct_face_centers
Grid.get_spatial_hash
Grid.get_faces_containing_point

Inheritance of Xarray Functionality
Expand Down
106 changes: 106 additions & 0 deletions docs/user-guide/spatial-hashing.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Spatial Hashing\n",
"UXarray supports spatial hashing, which a fast search method for finding the face that a point lies within on an unstructured grid. This can be useful for building particle-in-cell interpolators for Lagrangian fluid flow analysis. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import uxarray as ux"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For this example we will be using a simple hexagonal grid."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"grid_path = \"../../test/meshfiles/ugrid/quad-hexagon/grid.nc\"\n",
"uxgrid = ux.open_grid(grid_path)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"## SpatialHash\n",
"UXarray `SpatialHash` class provides an API for locating the unstructured grid faces that a list of $(x,y)$ points (in spherical coordinates) lie within. This search is conducted by relating unstructured grid faces and arbitrary physical positions to a uniformly spaced structured grid (the hash grid). The relationship between the unstructured grid elements and the hash grid is maintained in a hash table. \n",
"\n",
"### Parameters\n",
"The `SpatialHash` class can be accessed through the `Grid.get_spatial_hash(reconstruct)` method, which takes in the following parameters:\n",
"* `reconstruct` is a bool variable that allows the user to reconstruct the spatial hash structure. As default for performance, if a user calls `get_spatial_hash` and a spatial hash structure has already been created, it will simply use that one. If `reconstruct` is set to `True`, it will override this and reconstruct the spatial hash structure. The default parameter is `False`.\n",
"\n",
"\n",
"### Constructing a Spatial Hash\n",
"We can store the SpatialHash data structure in a variable, which allows us to access the spatial hash in a simple way for queries."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"spatial_hash = uxgrid.get_spatial_hash(\n",
" reconstruct=\"False\",\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Query\n",
"Now we can use that variable to query for the face that a point lies within in addition to its barycentric coordinates. The first parameter is the point from which to do the search, which must be in spherical coordinates."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"face_ids, bcoords = uxgrid.get_spatial_hash().query([-0.1, -0.1])\n",
"print(f\"face ids : {face_ids}\")\n",
"print(f\"Barycentric Coordinates: {bcoords}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "uxarray-dev",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.16"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
3 changes: 3 additions & 0 deletions docs/userguide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ These user guides provide detailed explanations of the core functionality in UXa
`Subsetting <user-guide/subset.ipynb>`_
Select specific regions of a grid

`Spatial Hashing <user-guide/spatial-hashing.ipynb>`_
Use spatial hashing to locate the faces a list of points reside in.

`Cross-Sections <user-guide/cross-sections.ipynb>`_
Select cross-sections of a grid

Expand Down
115 changes: 115 additions & 0 deletions test/test_spatial_hashing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import os
import numpy as np
import pytest
import xarray as xr
from pathlib import Path
import uxarray as ux

current_path = Path(os.path.dirname(os.path.realpath(__file__)))

# Sample grid file paths
gridfile_CSne8 = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc"
gridfile_RLL1deg = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug"
gridfile_RLL10deg_CSne4 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug"
gridfile_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug"
gridfile_fesom = current_path / "meshfiles" / "ugrid" / "fesom" / "fesom.mesh.diag.nc"
gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc"
gridfile_mpas = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc'

grid_files = [gridfile_CSne8,
gridfile_RLL1deg,
gridfile_RLL10deg_CSne4,
gridfile_CSne30,
gridfile_fesom,
gridfile_geoflow,
gridfile_mpas]

def test_construction():
"""Tests the construction of the SpatialHash object"""
for grid_file in grid_files:
uxgrid = ux.open_grid(grid_file)
face_ids, bcoords = uxgrid.get_spatial_hash().query([0.9, 1.8])
assert face_ids.shape[0] == bcoords.shape[0]


def test_is_inside():
"""Verifies simple test for points inside and outside an element."""
verts = [(0.0, 90.0), (-180, 0.0), (0.0, -90)]
uxgrid = ux.open_grid(verts, latlon=True)
# Verify that a point outside the element returns a face id of -1
face_ids, bcoords = uxgrid.get_spatial_hash().query([90.0, 0.0])
assert face_ids[0] == -1
# Verify that a point inside the element returns a face id of 0
face_ids, bcoords = uxgrid.get_spatial_hash().query([-90.0, 0.0])

assert face_ids[0] == 0
assert np.allclose(bcoords[0], [0.25, 0.5, 0.25], atol=1e-06)

def test_list_of_coords_simple():
"""Verifies test using list of points inside and outside an element"""
verts = [(0.0, 90.0), (-180, 0.0), (0.0, -90)]
uxgrid = ux.open_grid(verts, latlon=True)

coords = [[90.0, 0.0], [-90.0, 0.0]]
face_ids, bcoords = uxgrid.get_spatial_hash().query(coords)
assert face_ids[0] == -1
assert face_ids[1] == 0
assert np.allclose(bcoords[1], [0.25, 0.5, 0.25], atol=1e-06)

def test_list_of_coords_fesom():
"""Verifies test using list of points on the fesom grid"""
uxgrid = ux.open_grid(gridfile_fesom)

num_particles = 20
coords = np.zeros((num_particles,2))
x_min = 1.0
x_max = 3.0
y_min = 2.0
y_max = 10.0
for k in range(num_particles):
coords[k,0] = np.deg2rad(np.random.uniform(x_min, x_max))
coords[k,1] = np.deg2rad(np.random.uniform(y_min, y_max))
face_ids, bcoords = uxgrid.get_spatial_hash().query(coords)
assert len(face_ids) == num_particles
assert bcoords.shape[0] == num_particles
assert bcoords.shape[1] == 3
assert np.all(face_ids >= 0) # All particles should be inside an element

def test_list_of_coords_mpas_dual():
"""Verifies test using list of points on the dual MPAS grid"""
uxgrid = ux.open_grid(gridfile_mpas, use_dual=True)

num_particles = 20
coords = np.zeros((num_particles,2))
x_min = -40.0
x_max = 40.0
y_min = -20.0
y_max = 20.0
for k in range(num_particles):
coords[k,0] = np.deg2rad(np.random.uniform(x_min, x_max))
coords[k,1] = np.deg2rad(np.random.uniform(y_min, y_max))
face_ids, bcoords = uxgrid.get_spatial_hash().query(coords)
assert len(face_ids) == num_particles
assert bcoords.shape[0] == num_particles
assert bcoords.shape[1] == 3 # max sides of an element
assert np.all(face_ids >= 0) # All particles should be inside an element


def test_list_of_coords_mpas_primal():
"""Verifies test using list of points on the primal MPAS grid"""
uxgrid = ux.open_grid(gridfile_mpas, use_dual=False)

num_particles = 20
coords = np.zeros((num_particles,2))
x_min = -40.0
x_max = 40.0
y_min = -20.0
y_max = 20.0
for k in range(num_particles):
coords[k,0] = np.deg2rad(np.random.uniform(x_min, x_max))
coords[k,1] = np.deg2rad(np.random.uniform(y_min, y_max))
face_ids, bcoords = uxgrid.get_spatial_hash().query(coords)
assert len(face_ids) == num_particles
assert bcoords.shape[0] == num_particles
assert bcoords.shape[1] == 6 # max sides of an element
assert np.all(face_ids >= 0) # All particles should be inside an element
40 changes: 40 additions & 0 deletions uxarray/grid/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
from uxarray.grid.neighbors import (
BallTree,
KDTree,
SpatialHash,
_populate_edge_face_distances,
_populate_edge_node_distances,
)
Expand Down Expand Up @@ -252,6 +253,7 @@ def __init__(
# initialize cached data structures (nearest neighbor operations)
self._ball_tree = None
self._kd_tree = None
self._spatialhash = None

# flag to track if coordinates are normalized
self._normalized = None
Expand Down Expand Up @@ -1762,6 +1764,44 @@ def get_kd_tree(

return self._kd_tree

def get_spatial_hash(
self,
reconstruct: bool = False,
):
"""Get the SpatialHash data structure of this Grid that allows for
fast face search queries. Face searches are used to find the faces that
a list of points, in spherical coordinates, are contained within.

Parameters
----------
reconstruct : bool, default=False
If true, reconstructs the spatial hash

Returns
-------
self._spatialhash : grid.Neighbors.SpatialHash
SpatialHash instance

Examples
--------
Open a grid from a file path:

>>> import uxarray as ux
>>> uxgrid = ux.open_grid("grid_filename.nc")

Obtain SpatialHash instance:

>>> spatial_hash = uxgrid.get_spatial_hash()

Query to find the face a point lies within in addition to its barycentric coordinates:

>>> face_ids, bcoords = spatial_hash.query([0.0, 0.0])
"""
if self._spatialhash is None or reconstruct:
self._spatialhash = SpatialHash(self, reconstruct)

return self._spatialhash

def copy(self):
"""Returns a deep copy of this grid."""

Expand Down
Loading
Loading