mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 00:58:12 -07:00
style: apply ruff-format to entire codebase
First-time run of ruff-format via pre-commit hook normalises quote style, trailing commas, and whitespace across 188 Python files. No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+61
-80
@@ -14,16 +14,18 @@ import math
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logger = logging.getLogger('intercept.trilateration')
|
||||
logger = logging.getLogger("intercept.trilateration")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Data Classes
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentObservation:
|
||||
"""A single observation of a device by an agent."""
|
||||
|
||||
agent_name: str
|
||||
agent_lat: float
|
||||
agent_lon: float
|
||||
@@ -35,6 +37,7 @@ class AgentObservation:
|
||||
@dataclass
|
||||
class LocationEstimate:
|
||||
"""Estimated location of a device with confidence metrics."""
|
||||
|
||||
latitude: float
|
||||
longitude: float
|
||||
accuracy_meters: float # Estimated accuracy radius
|
||||
@@ -47,14 +50,14 @@ class LocationEstimate:
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to JSON-serializable dictionary."""
|
||||
return {
|
||||
'latitude': self.latitude,
|
||||
'longitude': self.longitude,
|
||||
'accuracy_meters': self.accuracy_meters,
|
||||
'confidence': self.confidence,
|
||||
'num_observations': self.num_observations,
|
||||
'method': self.method,
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
'agents': [obs.agent_name for obs in self.observations]
|
||||
"latitude": self.latitude,
|
||||
"longitude": self.longitude,
|
||||
"accuracy_meters": self.accuracy_meters,
|
||||
"confidence": self.confidence,
|
||||
"num_observations": self.num_observations,
|
||||
"method": self.method,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"agents": [obs.agent_name for obs in self.observations],
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +65,7 @@ class LocationEstimate:
|
||||
# Path Loss Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class PathLossModel:
|
||||
"""
|
||||
Convert RSSI to estimated distance using path loss models.
|
||||
@@ -79,25 +83,22 @@ class PathLossModel:
|
||||
|
||||
# Default parameters for different environments
|
||||
ENVIRONMENTS = {
|
||||
'free_space': {'n': 2.0, 'rssi_ref': -40},
|
||||
'outdoor': {'n': 2.5, 'rssi_ref': -45},
|
||||
'indoor': {'n': 3.0, 'rssi_ref': -50},
|
||||
'indoor_obstructed': {'n': 4.0, 'rssi_ref': -55},
|
||||
"free_space": {"n": 2.0, "rssi_ref": -40},
|
||||
"outdoor": {"n": 2.5, "rssi_ref": -45},
|
||||
"indoor": {"n": 3.0, "rssi_ref": -50},
|
||||
"indoor_obstructed": {"n": 4.0, "rssi_ref": -55},
|
||||
}
|
||||
|
||||
# Frequency-specific reference RSSI adjustments (WiFi vs Bluetooth)
|
||||
FREQUENCY_ADJUSTMENTS = {
|
||||
2400: 0, # 2.4 GHz WiFi/Bluetooth - baseline
|
||||
5000: -3, # 5 GHz WiFi - weaker propagation
|
||||
900: +5, # 900 MHz ISM - better propagation
|
||||
433: +8, # 433 MHz sensors - even better
|
||||
2400: 0, # 2.4 GHz WiFi/Bluetooth - baseline
|
||||
5000: -3, # 5 GHz WiFi - weaker propagation
|
||||
900: +5, # 900 MHz ISM - better propagation
|
||||
433: +8, # 433 MHz sensors - even better
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
environment: str = 'outdoor',
|
||||
path_loss_exponent: float | None = None,
|
||||
reference_rssi: float | None = None
|
||||
self, environment: str = "outdoor", path_loss_exponent: float | None = None, reference_rssi: float | None = None
|
||||
):
|
||||
"""
|
||||
Initialize path loss model.
|
||||
@@ -107,15 +108,11 @@ class PathLossModel:
|
||||
path_loss_exponent: Override the environment's default n value
|
||||
reference_rssi: Override the environment's default RSSI at 1m
|
||||
"""
|
||||
env_params = self.ENVIRONMENTS.get(environment, self.ENVIRONMENTS['outdoor'])
|
||||
self.n = path_loss_exponent if path_loss_exponent is not None else env_params['n']
|
||||
self.rssi_ref = reference_rssi if reference_rssi is not None else env_params['rssi_ref']
|
||||
env_params = self.ENVIRONMENTS.get(environment, self.ENVIRONMENTS["outdoor"])
|
||||
self.n = path_loss_exponent if path_loss_exponent is not None else env_params["n"]
|
||||
self.rssi_ref = reference_rssi if reference_rssi is not None else env_params["rssi_ref"]
|
||||
|
||||
def rssi_to_distance(
|
||||
self,
|
||||
rssi: float,
|
||||
frequency_mhz: float | None = None
|
||||
) -> float:
|
||||
def rssi_to_distance(self, rssi: float, frequency_mhz: float | None = None) -> float:
|
||||
"""
|
||||
Convert RSSI to estimated distance in meters.
|
||||
|
||||
@@ -146,11 +143,7 @@ class PathLossModel:
|
||||
except (ValueError, OverflowError):
|
||||
return 100.0 # Default fallback
|
||||
|
||||
def distance_to_rssi(
|
||||
self,
|
||||
distance: float,
|
||||
frequency_mhz: float | None = None
|
||||
) -> float:
|
||||
def distance_to_rssi(self, distance: float, frequency_mhz: float | None = None) -> float:
|
||||
"""
|
||||
Estimate RSSI at a given distance (inverse of rssi_to_distance).
|
||||
Useful for testing and validation.
|
||||
@@ -174,6 +167,7 @@ class PathLossModel:
|
||||
# Geographic Utilities
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
"""
|
||||
Calculate the great-circle distance between two points in meters.
|
||||
@@ -187,8 +181,7 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
|
||||
delta_phi = math.radians(lat2 - lat1)
|
||||
delta_lambda = math.radians(lon2 - lon1)
|
||||
|
||||
a = math.sin(delta_phi / 2) ** 2 + \
|
||||
math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2) ** 2
|
||||
a = math.sin(delta_phi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2) ** 2
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
return R * c
|
||||
@@ -225,6 +218,7 @@ def offset_position(lat: float, lon: float, north_m: float, east_m: float) -> tu
|
||||
# Trilateration Algorithm
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class Trilateration:
|
||||
"""
|
||||
Estimate device location using multilateration from multiple RSSI observations.
|
||||
@@ -240,7 +234,7 @@ class Trilateration:
|
||||
path_loss_model: PathLossModel | None = None,
|
||||
min_observations: int = 2,
|
||||
max_iterations: int = 100,
|
||||
convergence_threshold: float = 0.1 # meters
|
||||
convergence_threshold: float = 0.1, # meters
|
||||
):
|
||||
"""
|
||||
Initialize trilateration engine.
|
||||
@@ -256,10 +250,7 @@ class Trilateration:
|
||||
self.max_iterations = max_iterations
|
||||
self.convergence_threshold = convergence_threshold
|
||||
|
||||
def estimate_location(
|
||||
self,
|
||||
observations: list[AgentObservation]
|
||||
) -> LocationEstimate | None:
|
||||
def estimate_location(self, observations: list[AgentObservation]) -> LocationEstimate | None:
|
||||
"""
|
||||
Estimate device location from multiple agent observations.
|
||||
|
||||
@@ -275,9 +266,12 @@ class Trilateration:
|
||||
|
||||
# Filter out observations with invalid coordinates
|
||||
valid_obs = [
|
||||
obs for obs in observations
|
||||
if obs.agent_lat is not None and obs.agent_lon is not None
|
||||
and -90 <= obs.agent_lat <= 90 and -180 <= obs.agent_lon <= 180
|
||||
obs
|
||||
for obs in observations
|
||||
if obs.agent_lat is not None
|
||||
and obs.agent_lon is not None
|
||||
and -90 <= obs.agent_lat <= 90
|
||||
and -180 <= obs.agent_lon <= 180
|
||||
]
|
||||
|
||||
if len(valid_obs) < self.min_observations:
|
||||
@@ -307,13 +301,10 @@ class Trilateration:
|
||||
total_error = 0.0
|
||||
|
||||
for obs, expected_dist in zip(valid_obs, distances):
|
||||
actual_dist = haversine_distance(
|
||||
current_lat, current_lon,
|
||||
obs.agent_lat, obs.agent_lon
|
||||
)
|
||||
actual_dist = haversine_distance(current_lat, current_lon, obs.agent_lat, obs.agent_lon)
|
||||
|
||||
error = actual_dist - expected_dist
|
||||
total_error += error ** 2
|
||||
total_error += error**2
|
||||
|
||||
if actual_dist > 0.1: # Avoid division by zero
|
||||
# Gradient components
|
||||
@@ -350,10 +341,7 @@ class Trilateration:
|
||||
# Calculate accuracy estimate (average distance error)
|
||||
total_error = 0.0
|
||||
for obs, expected_dist in zip(valid_obs, distances):
|
||||
actual_dist = haversine_distance(
|
||||
current_lat, current_lon,
|
||||
obs.agent_lat, obs.agent_lon
|
||||
)
|
||||
actual_dist = haversine_distance(current_lat, current_lon, obs.agent_lat, obs.agent_lon)
|
||||
total_error += abs(actual_dist - expected_dist)
|
||||
|
||||
avg_error = total_error / len(valid_obs)
|
||||
@@ -367,7 +355,7 @@ class Trilateration:
|
||||
error_factor = max(0.0, 1.0 - avg_error / 500.0) # Decreases as error increases
|
||||
rssi_factor = min(1.0, max(0.0, (max(obs.rssi for obs in valid_obs) + 90) / 50.0))
|
||||
|
||||
confidence = (obs_factor * 0.3 + error_factor * 0.5 + rssi_factor * 0.2)
|
||||
confidence = obs_factor * 0.3 + error_factor * 0.5 + rssi_factor * 0.2
|
||||
|
||||
return LocationEstimate(
|
||||
latitude=current_lat,
|
||||
@@ -376,7 +364,7 @@ class Trilateration:
|
||||
confidence=confidence,
|
||||
num_observations=len(valid_obs),
|
||||
observations=valid_obs,
|
||||
method="multilateration"
|
||||
method="multilateration",
|
||||
)
|
||||
|
||||
|
||||
@@ -384,6 +372,7 @@ class Trilateration:
|
||||
# Device Location Tracker
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DeviceLocationTracker:
|
||||
"""
|
||||
Track device locations over time using observations from multiple agents.
|
||||
@@ -396,7 +385,7 @@ class DeviceLocationTracker:
|
||||
self,
|
||||
trilateration: Trilateration | None = None,
|
||||
observation_window_seconds: float = 60.0,
|
||||
min_observations: int = 2
|
||||
min_observations: int = 2,
|
||||
):
|
||||
"""
|
||||
Initialize device tracker.
|
||||
@@ -424,7 +413,7 @@ class DeviceLocationTracker:
|
||||
agent_lon: float,
|
||||
rssi: float,
|
||||
frequency_mhz: float | None = None,
|
||||
timestamp: datetime | None = None
|
||||
timestamp: datetime | None = None,
|
||||
) -> LocationEstimate | None:
|
||||
"""
|
||||
Add an observation and potentially update location estimate.
|
||||
@@ -447,7 +436,7 @@ class DeviceLocationTracker:
|
||||
agent_lon=agent_lon,
|
||||
rssi=rssi,
|
||||
frequency_mhz=frequency_mhz,
|
||||
timestamp=timestamp or datetime.now(timezone.utc)
|
||||
timestamp=timestamp or datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
if device_id not in self.observations:
|
||||
@@ -467,8 +456,7 @@ class DeviceLocationTracker:
|
||||
cutoff = now.timestamp() - self.observation_window
|
||||
|
||||
self.observations[device_id] = [
|
||||
obs for obs in self.observations[device_id]
|
||||
if obs.timestamp.timestamp() > cutoff
|
||||
obs for obs in self.observations[device_id] if obs.timestamp.timestamp() > cutoff
|
||||
]
|
||||
|
||||
def _update_location(self, device_id: str) -> LocationEstimate | None:
|
||||
@@ -501,12 +489,7 @@ class DeviceLocationTracker:
|
||||
"""Get all current location estimates."""
|
||||
return dict(self.locations)
|
||||
|
||||
def get_devices_near(
|
||||
self,
|
||||
lat: float,
|
||||
lon: float,
|
||||
radius_meters: float
|
||||
) -> list[tuple[str, LocationEstimate]]:
|
||||
def get_devices_near(self, lat: float, lon: float, radius_meters: float) -> list[tuple[str, LocationEstimate]]:
|
||||
"""Find all tracked devices within radius of a point."""
|
||||
results = []
|
||||
for device_id, estimate in self.locations.items():
|
||||
@@ -525,10 +508,8 @@ class DeviceLocationTracker:
|
||||
# Convenience Functions
|
||||
# =============================================================================
|
||||
|
||||
def estimate_location_from_observations(
|
||||
observations: list[dict],
|
||||
environment: str = 'outdoor'
|
||||
) -> dict | None:
|
||||
|
||||
def estimate_location_from_observations(observations: list[dict], environment: str = "outdoor") -> dict | None:
|
||||
"""
|
||||
Convenience function to estimate location from a list of observation dicts.
|
||||
|
||||
@@ -555,17 +536,17 @@ def estimate_location_from_observations(
|
||||
"""
|
||||
obs_list = []
|
||||
for obs in observations:
|
||||
obs_list.append(AgentObservation(
|
||||
agent_name=obs.get('agent_name', 'unknown'),
|
||||
agent_lat=obs['agent_lat'],
|
||||
agent_lon=obs['agent_lon'],
|
||||
rssi=obs['rssi'],
|
||||
frequency_mhz=obs.get('frequency_mhz')
|
||||
))
|
||||
obs_list.append(
|
||||
AgentObservation(
|
||||
agent_name=obs.get("agent_name", "unknown"),
|
||||
agent_lat=obs["agent_lat"],
|
||||
agent_lon=obs["agent_lon"],
|
||||
rssi=obs["rssi"],
|
||||
frequency_mhz=obs.get("frequency_mhz"),
|
||||
)
|
||||
)
|
||||
|
||||
trilat = Trilateration(
|
||||
path_loss_model=PathLossModel(environment=environment)
|
||||
)
|
||||
trilat = Trilateration(path_loss_model=PathLossModel(environment=environment))
|
||||
|
||||
estimate = trilat.estimate_location(obs_list)
|
||||
return estimate.to_dict() if estimate else None
|
||||
|
||||
Reference in New Issue
Block a user