-
Notifications
You must be signed in to change notification settings - Fork 35
⚡ Bolt: Optimize spatial filtering with equirectangular approximation #365
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
base: main
Are you sure you want to change the base?
Changes from all commits
978a397
1b1dcea
5aa12c4
c07afa7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,8 +3,6 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import List, Tuple, Optional | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| from sklearn.cluster import DBSCAN | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| from backend.models import Issue | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -56,6 +54,20 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return R * c | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def equirectangular_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Calculate the distance between two points using the Equirectangular approximation. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| This is much faster than Haversine and accurate enough for small distances (< 10km). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns distance in meters. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| R = 6371000.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Convert difference to radians directly | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| x = math.radians(lon2 - lon1) * math.cos(math.radians((lat1 + lat2) / 2)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| y = math.radians(lat2 - lat1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return R * math.sqrt(x*x + y*y) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Antimeridian (±180° longitude) wrapping not handled — correctness regression from Haversine. If two points straddle the antimeridian (e.g., lon1=179.999°, lon2=−179.999°), For your stated use case (civic issues, small radii) this is unlikely, but if the app ever serves locations near the antimeridian (Fiji, Tonga, far-east Russia), nearby duplicates would be missed silently. A minimal fix is to normalize the longitude delta: Proposed fix R = 6371000.0
- x = math.radians(lon2 - lon1) * math.cos(math.radians((lat1 + lat2) / 2))
+ dlon = (lon2 - lon1 + 180) % 360 - 180 # normalize to [-180, 180]
+ x = math.radians(dlon) * math.cos(math.radians((lat1 + lat2) / 2))
y = math.radians(lat2 - lat1)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def find_nearby_issues( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issues: List[Issue], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| target_lat: float, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -80,7 +92,8 @@ def find_nearby_issues( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| if issue.latitude is None or issue.longitude is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| distance = haversine_distance( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Use Equirectangular approximation for faster filtering | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| distance = equirectangular_distance( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| target_lat, target_lon, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issue.latitude, issue.longitude | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -94,53 +107,6 @@ def find_nearby_issues( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return nearby_issues | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def cluster_issues_dbscan(issues: List[Issue], eps_meters: float = 30.0) -> List[List[Issue]]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Cluster issues using DBSCAN algorithm based on spatial proximity. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issues: List of Issue objects with latitude/longitude | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| eps_meters: Maximum distance between two samples for one to be considered | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| as in the neighborhood of the other (default 30m) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| List of clusters, where each cluster is a list of Issue objects | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Filter issues with valid coordinates | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| valid_issues = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| issue for issue in issues | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if issue.latitude is not None and issue.longitude is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not valid_issues: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Convert to numpy array for DBSCAN | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| coordinates = np.array([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [issue.latitude, issue.longitude] for issue in valid_issues | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Convert eps from meters to degrees (approximate) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 1 degree latitude ≈ 111,000 meters | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 1 degree longitude ≈ 111,000 * cos(latitude) meters | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| eps_degrees = eps_meters / 111000 # Rough approximation | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Perform DBSCAN clustering | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| db = DBSCAN(eps=eps_degrees, min_samples=1, metric='haversine').fit( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| np.radians(coordinates) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Group issues by cluster | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| clusters = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i, label in enumerate(db.labels_): | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if label not in clusters: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| clusters[label] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| clusters[label].append(valid_issues[i]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Return clusters as list of lists (exclude noise points labeled as -1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [cluster for label, cluster in clusters.items() if label != -1] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_cluster_representative(cluster: List[Issue]) -> Issue: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Get the representative issue from a cluster. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,63 @@ | ||||||
|
|
||||||
| import pytest | ||||||
| import math | ||||||
|
Comment on lines
+2
to
+3
|
||||||
| import pytest | |
| import math |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments/docstrings describing the test distances are misleading: changing both lat and lon by 0.001° yields a diagonal distance (~150m at this latitude), not “roughly 100 meters”, and 0.1°/0.1° is closer to ~15km than 10km. This can confuse future readers about the intent/coverage of these accuracy tests—either adjust the coordinate deltas or update the comments/docstrings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
equirectangular_distancehard-codes Earth radius again (R = 6371000.0) and uses manualsqrt(x*x + y*y). Consider defining a module-level constant for Earth radius and reusing it acrosshaversine_distance/equirectangular_distance(and other helpers) to avoid inconsistencies, and usingmath.hypot(x, y)for clearer, numerically stable distance computation.