Skip to content

Commit 9997828

Browse files
hombitclaude
andauthored
Use Haversine formula in cone_filter (#624) (#636)
* Use astropy.coordinates.angular_separation in cone_filter (#624) Replace manual spherical cosine formula with astropy's angular_separation, converting all coordinates to radians and comparing the separation in radians. Add an ASV benchmark for the cone_filter angular separation computation. https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 * Rename benchmark method and fix uniform sphere sampling Append 'benchmark' to the cone filter benchmark name, and use arcsin(uniform(-1,1)) for declination to produce a uniform distribution on the sphere instead of biasing toward the poles. https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 * Rename AngularSeparationSuite to ConeFilterSuite https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 * Use haversine formula instead of astropy angular_separation for speed Replace the slow astropy.coordinates.angular_separation call with an inline haversine formula using only numpy operations. All math remains in radians, and the result is compared in radians. https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 * Optimize haversine: compare sin² half-angles, avoid arcsin/sqrt - Compare haversine value a <= sin²(radius/2) directly, eliminating the expensive arcsin and sqrt operations - Use np.square instead of **2 - Multiply by 0.5 instead of dividing by 2 - Precompute cos(dec0) scalar Valid for all separations up to 180 degrees since sin²(sep/2) is monotonically increasing on [0, pi]. https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 * Fix pylint: initialize attributes in ConeFilterSuite.__init__ https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 * Reformat haversine equation for readability https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 * Split haversine terms for readability, add comment Break the haversine equation into named intermediate variables (hav_dec, hav_ra) so the formatter doesn't mangle the layout. https://claude.ai/code/session_01D2WEdPXU3F8cfdmewEYXX8 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ebbd155 commit 9997828

File tree

2 files changed

+41
-7
lines changed

2 files changed

+41
-7
lines changed

benchmarks/benchmarks.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from pathlib import Path
77

8+
import nested_pandas as npd
89
import numpy as np
910

1011
import hats.pixel_math as hist
@@ -14,6 +15,7 @@
1415
from hats.pixel_math import HealpixPixel
1516
from hats.pixel_tree import align_trees
1617
from hats.pixel_tree.pixel_tree import PixelTree
18+
from hats.search.region_search import cone_filter
1719

1820
BENCH_DATA_DIR = Path(__file__).parent / "data"
1921

@@ -80,3 +82,34 @@ def time_small_cone_large_catalog():
8082
original_catalog = read_hats(BENCH_DATA_DIR / "large_catalog")
8183

8284
original_catalog.filter_by_cone(315, -66.443, 1)
85+
86+
87+
class ConeFilterSuite:
88+
"""Benchmark the cone_filter angular separation computation."""
89+
90+
def __init__(self):
91+
self.metadata = None
92+
self.data_frame = None
93+
self.ra = None
94+
self.dec = None
95+
self.radius_arcsec = None
96+
97+
def setup(self):
98+
rng = np.random.default_rng(42)
99+
n = 100_000
100+
ra = rng.uniform(0, 360, n)
101+
dec = np.degrees(np.arcsin(rng.uniform(-1, 1, n)))
102+
self.metadata = TableProperties(
103+
catalog_name="bench",
104+
catalog_type="object",
105+
total_rows=n,
106+
ra_column="ra",
107+
dec_column="dec",
108+
)
109+
self.data_frame = npd.NestedFrame({"ra": ra, "dec": dec})
110+
self.ra = 180.0
111+
self.dec = 0.0
112+
self.radius_arcsec = 10.0 * 3600 # 10 degrees
113+
114+
def time_cone_filter_benchmark(self):
115+
cone_filter(self.data_frame, self.ra, self.dec, self.radius_arcsec, self.metadata)

src/hats/search/region_search.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,14 @@ def cone_filter(data_frame: npd.NestedFrame, ra, dec, radius_arcsec, metadata: T
9292
ra0 = np.radians(ra)
9393
dec0 = np.radians(dec)
9494

95-
cos_angle = np.sin(dec_rad) * np.sin(dec0) + np.cos(dec_rad) * np.cos(dec0) * np.cos(ra_rad - ra0)
96-
97-
# Clamp to valid range to avoid numerical issues
98-
cos_separation = np.clip(cos_angle, -1.0, 1.0)
99-
100-
cos_radius = np.cos(np.radians(radius_arcsec / 3600))
101-
data_frame = data_frame[cos_separation >= cos_radius]
95+
cos_dec0 = np.cos(dec0)
96+
# Haversine formula
97+
hav_dec = np.square(np.sin((dec_rad - dec0) * 0.5))
98+
hav_ra = np.square(np.sin((ra_rad - ra0) * 0.5))
99+
a = hav_dec + cos_dec0 * np.cos(dec_rad) * hav_ra
100+
radius_rad = np.radians(radius_arcsec / 3600)
101+
threshold = np.square(np.sin(radius_rad * 0.5))
102+
data_frame = data_frame[a <= threshold]
102103
return data_frame
103104

104105

0 commit comments

Comments
 (0)