diff --git a/api/app/models/__pycache__/terrain.cpython-313.pyc b/api/app/models/__pycache__/terrain.cpython-313.pyc index 903934e..9208ca5 100644 Binary files a/api/app/models/__pycache__/terrain.cpython-313.pyc and b/api/app/models/__pycache__/terrain.cpython-313.pyc differ diff --git a/api/app/models/terrain.py b/api/app/models/terrain.py index 54d3577..7918e7a 100644 --- a/api/app/models/terrain.py +++ b/api/app/models/terrain.py @@ -51,6 +51,8 @@ class LosRequest(BaseModel): class Obstruction(BaseModel): distance_m: float clearance_m: float + fresnel_radius_m: float + required_clearance_m: float type: Literal["terrain", "building", "canopy"] @@ -60,5 +62,9 @@ class LosResponse(BaseModel): first_fresnel_clearance_pct: float worst_obstruction: Obstruction | None obstructions: list[Obstruction] + geometric_obstructions: list[Obstruction] + fresnel_violations_count: int + geometric_obstructions_count: int + building_obstructions_count: int diffraction_loss_db: float profile_ref: str diff --git a/api/app/services/__pycache__/terrain.cpython-313.pyc b/api/app/services/__pycache__/terrain.cpython-313.pyc index 208fab6..a3e2e76 100644 Binary files a/api/app/services/__pycache__/terrain.cpython-313.pyc and b/api/app/services/__pycache__/terrain.cpython-313.pyc differ diff --git a/api/app/services/terrain.py b/api/app/services/terrain.py index 3431d1a..779c53f 100644 --- a/api/app/services/terrain.py +++ b/api/app/services/terrain.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import Session from app.config import get_settings from app.core import dem from app.core.diffraction import deygout -from app.core.fresnel import los_analysis +from app.core.fresnel import LosSample, los_analysis from app.core.geo import GeoPoint, PathPoint, linestring_geojson, sample_path from app.core.surface import build_surface_profile from app.models.terrain import ( @@ -102,19 +102,34 @@ def los( request.frequency_mhz * 1_000_000, ) - def convert(sample: object) -> Obstruction: + def convert(sample: LosSample) -> Obstruction: return Obstruction( distance_m=sample.distance_m, clearance_m=sample.clearance_m, + fresnel_radius_m=sample.fresnel_radius_m, + required_clearance_m=sample.required_clearance_m, type=sample.obstruction_type, ) + geometric_obstructions = [ + sample for sample in result.obstructions if sample.clearance_m < 0 + ] + building_obstructions = [ + sample + for sample in geometric_obstructions + if sample.obstruction_type == "building" + ] + return LosResponse( los_clear=result.los_clear, geometric_los=result.geometric_los, first_fresnel_clearance_pct=result.first_fresnel_clearance_pct, worst_obstruction=convert(result.worst_sample) if result.worst_sample else None, obstructions=[convert(sample) for sample in result.obstructions], + geometric_obstructions=[convert(sample) for sample in geometric_obstructions], + fresnel_violations_count=len(result.obstructions), + geometric_obstructions_count=len(geometric_obstructions), + building_obstructions_count=len(building_obstructions), diffraction_loss_db=diffraction_loss, profile_ref=f"inline:{profile_request.samples}", ) diff --git a/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc b/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc index 141eb89..b5c6efa 100644 Binary files a/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc and b/api/tests/__pycache__/test_api.cpython-313-pytest-9.0.3.pyc differ diff --git a/api/tests/test_api.py b/api/tests/test_api.py index b94fc84..3a1288c 100644 --- a/api/tests/test_api.py +++ b/api/tests/test_api.py @@ -45,6 +45,31 @@ def test_elevation_returns_not_implemented_without_dem() -> None: assert response.status_code == 501 +def test_los_endpoint_returns_obstruction_summary_fields() -> None: + response = client.post( + "/api/v1/terrain/los", + json={ + "tx": {"lat": 59.935, "lon": 30.305, "height_agl": 30}, + "rx": {"lat": 59.945, "lon": 30.325, "height_agl": 2}, + "frequency_mhz": 433, + "fresnel_clearance": 0.6, + "include_buildings": False, + "include_canopy": False, + "k_factor": 1.333, + "samples": 16, + }, + ) + assert response.status_code == 200 + data = response.json() + assert "obstructions" in data + assert "geometric_obstructions" in data + assert data["fresnel_violations_count"] == len(data["obstructions"]) + assert data["geometric_obstructions_count"] == len(data["geometric_obstructions"]) + assert data["building_obstructions_count"] == len( + [item for item in data["geometric_obstructions"] if item["type"] == "building"] + ) + + def test_manual_link_budget_endpoint() -> None: response = client.post( "/api/v1/link/budget",