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

Check for polygon degeneracy #195

Merged
merged 1 commit into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion src/hipscat/catalog/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
SphericalCoordinates,
filter_pixels_by_polygon,
)
from hipscat.pixel_math.validators import validate_declination_values, validate_radius
from hipscat.pixel_math.validators import validate_declination_values, validate_polygon, validate_radius


class Catalog(HealpixDataset):
Expand Down Expand Up @@ -93,6 +93,7 @@ def filter_by_polygon(self, vertices: List[SphericalCoordinates] | List[Cartesia
# Get the coordinates vector on the unit sphere if we were provided
# with polygon spherical coordinates of ra and dec
vertices = hp.ang2vec(ra, dec, lonlat=True)
validate_polygon(vertices)
return self.filter_from_pixel_list(filter_pixels_by_polygon(self.pixel_tree, vertices))

def filter_from_pixel_list(self, pixels: List[HealpixPixel]) -> Catalog:
Expand Down
47 changes: 47 additions & 0 deletions src/hipscat/pixel_math/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class ValidatorsErrors(str, Enum):
"""Error messages for the coordinate validators"""
INVALID_DEC = "declination must be in the -90.0 to 90.0 degree range"
INVALID_RADIUS = "cone radius must be positive"
INVALID_NUM_VERTICES = "polygon must contain a minimum of 3 vertices"
DUPLICATE_VERTICES = "polygon has duplicated vertices"
DEGENERATE_POLYGON = "polygon is degenerate"


def validate_radius(radius: float):
Expand Down Expand Up @@ -38,3 +41,47 @@ def validate_declination_values(dec: float | List[float]):
lower_bound, upper_bound = -90., 90.
if not np.all((dec_values >= lower_bound) & (dec_values <= upper_bound)):
raise ValueError(ValidatorsErrors.INVALID_DEC.value)


def validate_polygon(vertices: np.ndarray):
"""Checks if the polygon contain a minimum of three vertices, that they are
unique and that the polygon does not fall on a great circle.

Arguments:
vertices (np.ndarray): The polygon vertices, in cartesian coordinates

Raises:
ValueError exception if the polygon is invalid.
"""
if len(vertices) < 3:
raise ValueError(ValidatorsErrors.INVALID_NUM_VERTICES.value)
if len(vertices) != len(np.unique(vertices, axis=0)):
raise ValueError(ValidatorsErrors.DUPLICATE_VERTICES.value)
if is_polygon_degenerate(vertices):
raise ValueError(ValidatorsErrors.DEGENERATE_POLYGON.value)


def is_polygon_degenerate(vertices: np.ndarray) -> bool:
"""Checks if all the vertices of the polygon are contained in a same plane.
If the plane intersects the center of the sphere, the polygon is degenerate.

Arguments:
vertices (np.ndarray): The polygon vertices, in cartesian coordinates

Returns:
A boolean, which is True if the polygon is degenerate, i.e. if it falls
on a great circle, False otherwise.
"""
# Calculate the normal vector of the plane using three of the vertices
normal_vector = np.cross(vertices[1] - vertices[0], vertices[2] - vertices[0])

# Check if the other vertices lie on the same plane
for vertex in vertices[3:]:
dot_product = np.dot(normal_vector, vertex - vertices[0])
if not np.isclose(dot_product, 0):
return False

# Check if the plane intersects the sphere's center. If it does,
# the polygon is degenerate and therefore, invalid.
center_distance = np.dot(normal_vector, vertices[0])
return bool(np.isclose(center_distance, 0))
22 changes: 20 additions & 2 deletions tests/hipscat/catalog/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def test_polygonal_filter(small_sky_order1_catalog):
assert filtered_catalog.catalog_info.total_rows is None


def test_polygon_filter_with_cartesian_coordinates(small_sky_order1_catalog):
def test_polygonal_filter_with_cartesian_coordinates(small_sky_order1_catalog):
sky_vertices = [(282, -58), (282, -55), (272, -55), (272, -58)]
cartesian_vertices = hp.ang2vec(*np.array(sky_vertices).T, lonlat=True)
filtered_catalog_1 = small_sky_order1_catalog.filter_by_polygon(sky_vertices)
Expand Down Expand Up @@ -192,7 +192,7 @@ def test_polygonal_filter_empty(small_sky_order1_catalog):
assert len(filtered_catalog.pixel_tree) == 1


def test_polygonal_filter_invalid_polygon_coordinates(small_sky_order1_catalog):
def test_polygonal_filter_invalid_coordinates(small_sky_order1_catalog):
# Declination is over 90 degrees
polygon_vertices = [(47.1, -100), (64.5, -100), (64.5, 6.27), (47.1, 6.27)]
with pytest.raises(ValueError, match=ValidatorsErrors.INVALID_DEC):
Expand All @@ -202,6 +202,24 @@ def test_polygonal_filter_invalid_polygon_coordinates(small_sky_order1_catalog):
small_sky_order1_catalog.filter_by_polygon(polygon_vertices)


def test_polygonal_filter_invalid_polygon(small_sky_order1_catalog):
# The polygon must have a minimum of 3 vertices
with pytest.raises(ValueError, match=ValidatorsErrors.INVALID_NUM_VERTICES):
vertices = [(100.1, -20.3), (100.1, 40.3)]
small_sky_order1_catalog.filter_by_polygon(vertices[:2])
# The vertices should not have duplicates
with pytest.raises(ValueError, match=ValidatorsErrors.DUPLICATE_VERTICES):
vertices = [(100.1, -20.3), (100.1, -20.3), (280.1, -20.3), (280.1, 40.3)]
small_sky_order1_catalog.filter_by_polygon(vertices)
# The polygons should not be on a great circle
with pytest.raises(ValueError, match=ValidatorsErrors.DEGENERATE_POLYGON):
vertices = [(100.1, 40.3), (100.1, -20.3), (280.1, -20.3), (280.1, 40.3)]
small_sky_order1_catalog.filter_by_polygon(vertices)
with pytest.raises(ValueError, match=ValidatorsErrors.DEGENERATE_POLYGON):
vertices = [(50.1, 0), (100.1, 0), (150.1, 0), (200.1, 0)]
small_sky_order1_catalog.filter_by_polygon(vertices)


def test_empty_directory(tmp_path):
"""Test loading empty or incomplete data"""
## Path doesn't exist
Expand Down