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

Point In Face & Get Faces Containing Point #1056

Open
wants to merge 83 commits into
base: main
Choose a base branch
from

Conversation

aaronzedwick
Copy link
Member

@aaronzedwick aaronzedwick commented Nov 5, 2024

Closes #905, #1141

Overview

Expected Usage

from uxarray.grid.geometry import point_in_polygon

# Defined polygon
polygon = [ [-10,  10, -10, 10], [10, 10, -10, -10]]

# Point to check
point = [10, 10]

point_in_polygon(polygon, point, inclusive=True)

PR Checklist

General

  • An issue is linked created and linked
  • Add appropriate labels
  • Filled out Overview and Expected Usage (if applicable) sections

Testing

  • Adequate tests are created if there is new functionality
  • Tests cover all possible logical paths in your function
  • Tests are not too basic (such as simply calling a function and nothing else)

Documentation

  • Docstrings have been added to all new functions
  • Docstrings have updated with any function changes
  • Internal functions have a preceding underscore (_) and have been added to docs/internal_api/index.rst
  • User functions have been added to docs/user_api/index.rst

@aaronzedwick
Copy link
Member Author

@ philipc2 do you think this should be an internal function or exposed to the user?

@aaronzedwick aaronzedwick changed the title DRAFT: Point In Polygon Point In Polygon Nov 27, 2024
@aaronzedwick aaronzedwick marked this pull request as ready for review November 27, 2024 15:13
Copy link
Member

@philipc2 philipc2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please include an ASV benchmark. I'd suggest doing a parameterized benchmark for the 120 and 480 km MPAS grids.

Copy link
Member

@philipc2 philipc2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the optimized functions from #1072 and try to write the function entirely in Numba. This may require us to pass in both the cartesian and spherical versions of point & polygon. Let me know if you have any questions!

uxarray/grid/geometry.py Outdated Show resolved Hide resolved
@aaronzedwick aaronzedwick linked an issue Jan 29, 2025 that may be closed by this pull request
uxarray/grid/grid.py Outdated Show resolved Hide resolved
uxarray/grid/grid.py Outdated Show resolved Hide resolved
@philipc2 philipc2 removed the run-benchmark Run ASV benchmark workflow label Jan 31, 2025
Copy link
Contributor

@rajeeja rajeeja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

point_in_face(faces_edge_cartesian[0], point, inclusive=True)

this always requires calculation of edges, can we have another version of this -

  1. point_in_face(face_id, point or
  2. point_in_face(face_conn, point)

For the notebook example, we have 4 faces with id's say 0 to 4 and connectivity as shown below:


xarray.DataArray'face_node_connectivity'n_face: 4n_max_face_nodes: 6

array([[ 0,  1,  2,  3,  4,  5],
       [15,  7,  6, 12,  0,  5],
       [ 0, 12,  8,  9, 13,  1],
       [ 4, 14, 11, 10, 15,  5]])

getting edges adds another step that might not always be necessary and can be done internally for these other signatures, the two functions above would be more user friendly IMO.

@aaronzedwick
Copy link
Member Author

aaronzedwick commented Feb 4, 2025

point_in_face(faces_edge_cartesian[0], point, inclusive=True)

this always requires calculation of edges, can we have another version of this -

  1. point_in_face(face_id, point or
  2. point_in_face(face_conn, point)

For the notebook example, we have 4 faces with id's say 0 to 4 and connectivity as shown below:


xarray.DataArray'face_node_connectivity'n_face: 4n_max_face_nodes: 6

array([[ 0,  1,  2,  3,  4,  5],
       [15,  7,  6, 12,  0,  5],
       [ 0, 12,  8,  9, 13,  1],
       [ 4, 14, 11, 10, 15,  5]])

getting edges adds another step that might not always be necessary and can be done internally for these other signatures, the two functions above would be more user friendly IMO.

This function is not a grid function, so passing just the face connectivity isn’t enough. You have to pass the node coordinates of the face. Either way, you have to parse the coordinates of the face you want to use.

We can pass the node coordinates as nodes into it if you like, but I originally went with this implementation for two reasons. First, the conversion will in theory impact performance. And second, if the user has to construct the node coordinates and send them in anyway, it would require less effort to use the edge coordinates Cartesian function to create the array for the user. It is easier and ensures the correct array shape is being passed to the function. Also, that is the shape of array the ‘faces_containing_point’ use, as well as all of Hongyu’s code.

I definitely understand wanting to make them more user friendly, but I see ‘get_faces_containing_point’ as the more common user function. And as the point in face function is meant to be called without being attached to the grid, you will always have to construct some kind of coordinate information and pass it in, whether it is the nodes that make up faces, or the nodes that make up edges. Otherwise the function won’t have access to that information.

@rajeeja rajeeja requested review from rajeeja and philipc2 February 4, 2025 19:16
Copy link
Contributor

@rajeeja rajeeja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug fix for conversion _lonlat_rad_to_xyz in coordinates.py, max_face_radius and get_faces_containing_points are critical changes. good work

Copy link
Member

@philipc2 philipc2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix the ASV benchmark, it is still failing on our CI.

benchmarks/mpas_ocean.py Outdated Show resolved Hide resolved
uxarray/grid/geometry.py Outdated Show resolved Hide resolved
@philipc2
Copy link
Member

philipc2 commented Feb 4, 2025

Some perfromance issues, but they appear to be due to our Grid.isel(), which I am already working on optimizations for.

This is 10,000 queries on a 30km MPAS grid

image

@rajeeja
Copy link
Contributor

rajeeja commented Feb 4, 2025

Some perfromance issues, but they appear to be due to our Grid.isel(), which I am already working on optimizations for.

This is 10,000 queries on a 30km MPAS grid

image

do you generate this locally or asv generates it in some automated fashion?

@philipc2
Copy link
Member

philipc2 commented Feb 4, 2025

Some perfromance issues, but they appear to be due to our Grid.isel(), which I am already working on optimizations for.
This is 10,000 queries on a 30km MPAS grid
image

do you generate this locally or asv generates it in some automated fashion?

import uxarray as ux
import numpy as np
import numba as nb

print(nb.__version__)
import cProfile

profiler = cProfile.Profile()


grid_path = "/Users/philipc/PycharmProjects/uxarray/unstructured-grid-viz-cookbook/meshfiles/x1.655362.grid.nc"

uxgrid=ux.open_grid(grid_path)

face_lon = uxgrid.face_lon.values.astype(np.float64)
face_lat = uxgrid.face_lat.values.astype(np.float64)



profiler.enable()
# Profile something here
for i in range(10000):
    point = np.array([face_lon[i], face_lat[i]])
    res = uxgrid.get_faces_containing_point(point)
profiler.disable()

# Save stats
profiler.dump_stats('point_in_polygon_10000.prof')

Then I ran snakeviz point_in_polygon_10000.prof

Comment on lines +2496 to +2505
if len(point) == 2:
point_xyz = np.array(_lonlat_rad_to_xyz(*np.deg2rad(point)))
point_lonlat = point
elif len(point) == 3:
point_xyz = point
point_lonlat = np.array(_xyz_to_lonlat_deg(*point_xyz), dtype=np.float64)
else:
raise ValueError(
"Point must either be in spherical or cartesian coordinates."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into an issue where if my points were float32 the Numba functionality would fail, since it would be trying to call Numba functions between float32 and float64 variables, which isn't possible.

Maybe doing a np.asaray(point, dtype=np.float64) could fix it. May you add a test for this to replicate the functionality?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some of my tests I have found that using lonlat with a dtype=np.float64 can cause issues on the nodes, it will not find all the faces nearby. I think this is possible due to rounding precission issues maybe?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting to a higher precision shouldn't usually lead to issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, doing dtype=np.float32 ensures that Hongyus algorithm recognizes that it is on the great circle arc. But when it is float64 it doesn't seem to realize it is on the arc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a minimal example? That's interesting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I query with Cartesian coordinates in f64, I get the correct indices.

point = np.array([grid.node_x[0].values, grid.node_y[0].values, grid.node_z[0].values], dtype=np.float64)

Though, with f32 I get a Numba error.

point = np.array([grid.node_x[0].values, grid.node_y[0].values, grid.node_z[0].values], dtype=np.float32)
No implementation of function Function(<built-in function dot>) found for signature:
 
 >>> dot(array(float64, 1d, C), array(float32, 1d, C))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does appear to be a precision issue when using spherical coordinates, since we internally convert them to cartesian. We should try to avoid as many of these conversions as possible. @hongyuchen1030 and I have encountered this a lot in our previous implementations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error you are getting happens within Hongyus code. I have had it error out sometimes when using cartesian coordinates that aren't in float64

Copy link
Member Author

@aaronzedwick aaronzedwick Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be fixed by ensuring that the only xyz coordinates are f64. But that still won't fix the precision issue of the spherical coordinates when they are changed to f64.

Copy link
Member

@philipc2 philipc2 Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if adding a small eps to increase the radius of each bounding circle might help?

EDIT: I see you did that below.

Copy link
Member

@philipc2 philipc2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include a test for points along a line of constant latitude?

    def test_point_along_arc():
        node_lon = np.array([-40, -40, 40, 40])
        node_lat = np.array([-20, 20, 20, -20])
        face_node_connectivity = np.array([[0, 1, 2, 3]], dtype=np.int64)

        uxgrid = ux.Grid.from_topology(node_lon, node_lat, face_node_connectivity)

        # point at exactly 20 degrees latitude
        out1 = uxgrid.get_faces_containing_point((0, 20))

        # point at 25.41 degrees latitude (max along the great circle arc)
        out2 = uxgrid.get_faces_containing_point((0, 25.41))

        nt.assert_array_equal(out1, out2)

uxarray/grid/grid.py Outdated Show resolved Hide resolved
uxarray/grid/grid.py Outdated Show resolved Hide resolved
uxarray/grid/grid.py Outdated Show resolved Hide resolved
uxarray/grid/geometry.py Outdated Show resolved Hide resolved
Comment on lines +2496 to +2505
if len(point) == 2:
point_xyz = np.array(_lonlat_rad_to_xyz(*np.deg2rad(point)))
point_lonlat = point
elif len(point) == 3:
point_xyz = point
point_lonlat = np.array(_xyz_to_lonlat_deg(*point_xyz), dtype=np.float64)
else:
raise ValueError(
"Point must either be in spherical or cartesian coordinates."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I query with Cartesian coordinates in f64, I get the correct indices.

point = np.array([grid.node_x[0].values, grid.node_y[0].values, grid.node_z[0].values], dtype=np.float64)

Though, with f32 I get a Numba error.

point = np.array([grid.node_x[0].values, grid.node_y[0].values, grid.node_z[0].values], dtype=np.float32)
No implementation of function Function(<built-in function dot>) found for signature:
 
 >>> dot(array(float64, 1d, C), array(float32, 1d, C))

Comment on lines +2496 to +2505
if len(point) == 2:
point_xyz = np.array(_lonlat_rad_to_xyz(*np.deg2rad(point)))
point_lonlat = point
elif len(point) == 3:
point_xyz = point
point_lonlat = np.array(_xyz_to_lonlat_deg(*point_xyz), dtype=np.float64)
else:
raise ValueError(
"Point must either be in spherical or cartesian coordinates."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does appear to be a precision issue when using spherical coordinates, since we internally convert them to cartesian. We should try to avoid as many of these conversions as possible. @hongyuchen1030 and I have encountered this a lot in our previous implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement Grid.get_faces_containing_point() Point in Face
4 participants