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

Rewrite check for point inside tesseroid with Numba #419

Merged
merged 9 commits into from
Jul 19, 2023
113 changes: 83 additions & 30 deletions harmonica/_forward/_tesseroid_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def _check_tesseroids(tesseroids):
return tesseroids


def _check_points_outside_tesseroids(coordinates, tesseroids):
def check_points_outside_tesseroids(coordinates, tesseroids):
"""
Check if computation points are not inside the tesseroids

Expand All @@ -423,42 +423,95 @@ def _check_points_outside_tesseroids(coordinates, tesseroids):
This array of tesseroids must have longitude continuity and valid
boundaries.
Run ``_check_tesseroids`` before.

Raises
------
ValueError
If any computation point falls inside any tesseroid.
"""
longitude, latitude, radius = coordinates
conflicting = _check_points_outside_tesseroids(coordinates, tesseroids)
if conflicting:
err_msg = (
"Found computation point(s) inside tesseroid(s). "
"Computation points must be outside of tesseroids.\n"
)
for i, j in conflicting:
west, east, south, north, bottom, top = tesseroids[j, :]
err_msg += (
f" - Computation point '({longitude[i]}, {latitude[i]}, {radius[i]})' "
"inside tesseroid "
f"'({west}, {east}, {south}, {north}, {bottom}, {top})'.\n"
)
raise ValueError(err_msg)


@jit(nopython=True)
def _check_points_outside_tesseroids(coordinates, tesseroids):
longitude, latitude, radius = coordinates[:]
west, east, south, north, bottom, top = tuple(tesseroids[:, i] for i in range(6))
conflicting = []
for i in range(longitude.size):
for j in range(tesseroids.shape[0]):
if _check_point_inside_tesseroid(
Copy link
Member

Choose a reason for hiding this comment

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

@santisoler is it worth having a function here? The function itself is a single if and is called inside another if.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not at all. I coded this fairly quickly and I was prototyping a little bit with parallelization of the checks. I ultimately decided to keep them in serial. I've already merged the two functions into a single one.

longitude[i],
latitude[i],
radius[i],
tesseroids[j, 0],
tesseroids[j, 1],
tesseroids[j, 2],
tesseroids[j, 3],
tesseroids[j, 4],
tesseroids[j, 5],
):
conflicting.append((i, j))
return conflicting


@jit(nopython=True)
def _check_point_inside_tesseroid(
longitude, latitude, radius, west, east, south, north, bottom, top
):
"""
Check if observation point falls inside tesseroid.

Parameters
----------
longitude : float
Longitude coordinate of the observation point in degrees.
latitude : float
Spherical latitude coordinate of the observation point in degrees.
radius : float
Geoscentric spherical radius of the observation point in meters.
west : float
West longitude bound of the tesseroid in degrees.
east : float
East longitude bound of the tesseroid in degrees.
south : float
South latitude bound of the tesseroid in degrees.
north : float
North latitude bound of the tesseroid in degrees.
bottom : float
Bottom radius bound of the tesseroid in meters.
top : float
Top radius bound of the tesseroid in meters.

Returns
-------
inside : bool
True if the observation point falls inside the tesseroid.
"""
# Longitudinal boundaries of the tesseroid must be compared with
# longitudinal coordinates of computation points when moved to
# [0, 360) and [-180, 180).
longitude_360 = longitude % 360
longitude_180 = ((longitude + 180) % 360) - 180
inside_longitude = np.logical_or(
np.logical_and(
west < longitude_360[:, np.newaxis], longitude_360[:, np.newaxis] < east
),
np.logical_and(
west < longitude_180[:, np.newaxis], longitude_180[:, np.newaxis] < east
),
)
inside_latitude = np.logical_and(
south < latitude[:, np.newaxis], latitude[:, np.newaxis] < north
)
inside_radius = np.logical_and(
bottom < radius[:, np.newaxis], radius[:, np.newaxis] < top
)
# Build array of booleans.
# The (i, j) element is True if the computation point i is inside the
# tesseroid j.
inside = inside_longitude * inside_latitude * inside_radius
if inside.any():
err_msg = (
"Found computation point inside tesseroid. "
+ "Computation points must be outside of tesseroids.\n"
)
for point_i, tess_i in np.argwhere(inside):
err_msg += "\tComputation point '{}' found inside tesseroid '{}'\n".format(
coordinates[:, point_i], tesseroids[tess_i, :]
)
raise ValueError(err_msg)
if (
(west < longitude_180 < east or west < longitude_360 < east)
and south < latitude < north
and bottom < radius < top
):
return True
return False


def _longitude_continuity(tesseroids):
Expand Down
4 changes: 2 additions & 2 deletions harmonica/_forward/tesseroid.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
from ..constants import GRAVITATIONAL_CONST
from ._tesseroid_utils import (
_adaptive_discretization,
_check_points_outside_tesseroids,
_check_tesseroids,
_discard_null_tesseroids,
check_points_outside_tesseroids,
gauss_legendre_quadrature,
glq_nodes_weights,
)
Expand Down Expand Up @@ -155,7 +155,7 @@ def tesseroid_gravity(
# Sanity checks for tesseroids and computation points
if not disable_checks:
tesseroids = _check_tesseroids(tesseroids)
_check_points_outside_tesseroids(coordinates, tesseroids)
check_points_outside_tesseroids(coordinates, tesseroids)
# Check if density is a function or constant values
if callable(density):
# Run density-based discretization on each tesseroid
Expand Down
98 changes: 67 additions & 31 deletions harmonica/tests/test_tesseroid.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
MAX_DISCRETIZATIONS,
STACK_SIZE,
_adaptive_discretization,
_check_points_outside_tesseroids,
_check_tesseroids,
check_points_outside_tesseroids,
tesseroid_gravity,
)
from ..constants import GRAVITATIONAL_CONST
Expand Down Expand Up @@ -129,7 +129,7 @@ def test_valid_tesseroid():
_check_tesseroids(np.atleast_2d([w, e, s, n, bottom, bottom]))
# Check if valid tesseroid with west > east is not caught
_check_tesseroids(np.atleast_2d([350, 10, s, n, bottom, top]))
# Check if tesseroids on [-180, 180) are not caught
# Check if tesseroids on -180, 180 are not caught
_check_tesseroids(np.atleast_2d([-70, -60, s, n, bottom, top]))
_check_tesseroids(np.atleast_2d([-10, 10, s, n, bottom, top]))
_check_tesseroids(np.atleast_2d([-150, 150, s, n, bottom, top]))
Expand Down Expand Up @@ -207,35 +207,71 @@ def test_disable_checks():
npt.assert_allclose(invalid_result, -valid_result)


def test_point_inside_tesseroid():
"Check if a computation point inside the tesseroid is caught"
tesseroids = np.atleast_2d([-10, 10, -10, 10, 100, 200])
# Test if outside point is not caught
points = [
np.atleast_2d([0, 0, 250]).T, # outside point on radius
np.atleast_2d([20, 0, 150]).T, # outside point on longitude
np.atleast_2d([0, 20, 150]).T, # outside point on latitude
np.atleast_2d([0, 0, 200]).T, # point on top surface
np.atleast_2d([0, 0, 100]).T, # point on bottom surface
np.atleast_2d([-10, 0, 150]).T, # point on western surface
np.atleast_2d([10, 0, 150]).T, # point on eastern surface
np.atleast_2d([0, -10, 150]).T, # point on southern surface
np.atleast_2d([0, 10, 150]).T, # point on northern surface
]
for coordinates in points:
_check_points_outside_tesseroids(coordinates, tesseroids)
# Test if computation point is inside the tesseroid
coordinates = np.atleast_2d([0, 0, 150]).T
with pytest.raises(ValueError):
_check_points_outside_tesseroids(coordinates, tesseroids)
# Test if computation point with phased longitude is inside the tesseroid
coordinates = np.atleast_2d([360, 0, 150]).T
with pytest.raises(ValueError):
_check_points_outside_tesseroids(coordinates, tesseroids)
tesseroids = np.atleast_2d([260, 280, -10, 10, 100, 200])
coordinates = np.atleast_2d([-90, 0, 150]).T
with pytest.raises(ValueError):
_check_points_outside_tesseroids(coordinates, tesseroids)
class TestPointInsideTesseroid:
"""Test checks for points inside tesseroids."""

@pytest.mark.use_numba
def test_points_outside_tesseroid(self):
"""Check if error is not raised when no point is inside a tesseroid."""
points = np.array(
[
[0, 0, 250], # outside point on radius
[20, 0, 150], # outside point on longitude
[0, 20, 150], # outside point on latitude
[0, 0, 200], # point on top surface
[0, 0, 100], # point on bottom surface
[-10, 0, 150], # point on western surface
[10, 0, 150], # point on eastern surface
[0, -10, 150], # point on southern surface
[0, 10, 150], # point on northern surface
]
).T
tesseroid = np.atleast_2d([-10, 10, -10, 10, 100, 200])
check_points_outside_tesseroids(points, tesseroid)

@pytest.mark.use_numba
@pytest.mark.parametrize("points", [[0, 0, 150], [360, 0, 150]])
def test_point_inside_tesseroid(self, points):
"""Check if error is raised when point fall inside the tesseroid."""
points = np.atleast_2d(points).T
tesseroid = np.atleast_2d([-10, 10, -10, 10, 100, 200])
with pytest.raises(ValueError):
check_points_outside_tesseroids(points, tesseroid)

@pytest.mark.use_numba
@pytest.mark.parametrize(
"point, tesseroid",
[
([360, 0, 150], [-10, 10, -10, 10, 100, 200]),
([-90, 0, 150], [260, 280, -10, 10, 100, 200]),
],
)
def test_point_phased_longitude(self, point, tesseroid):
"""Test if error is raised when the longitude coords are phased."""
point = np.atleast_2d(point).T
tesseroid = np.atleast_2d(tesseroid)
with pytest.raises(ValueError):
check_points_outside_tesseroids(point, tesseroid)

@pytest.mark.use_numba
def test_multiple_points_and_tesseroids(self):
"""Check if error is raised with multiple points and tesseroids."""
tesseroids = np.atleast_2d(
[
[-10, 10, -10, 10, 100, 200],
[20, 30, 20, 30, 400, 500],
[-50, -40, -30, -20, 100, 500],
]
)
points = np.array(
[
[0, 0, 150],
[80, 82, 4000],
[10, 10, 450],
]
).T
with pytest.raises(ValueError):
check_points_outside_tesseroids(points, tesseroids)


@pytest.mark.use_numba
Expand Down
Loading