diff --git a/API.md b/API.md index 7f4880c..61d9694 100644 --- a/API.md +++ b/API.md @@ -268,6 +268,13 @@ curl -s -X POST http://localhost:5603/api/v1/link/budget \ - `fade_margin_db` - `fresnel_clear` - `link_viable` +- `los_clear` +- `geometric_los` +- `first_fresnel_clearance_pct` +- `fresnel_violations_count` +- `geometric_obstructions_count` +- `building_obstructions_count` +- `worst_obstruction` При `include_vegetation=true` API сэмплит WorldCover вдоль трассы и добавляет P.833 vegetation attenuation в `vegetation_db`. Если landcover raster отсутствует, @@ -275,7 +282,9 @@ P.833 vegetation attenuation в `vegetation_db`. Если landcover raster от При `include_buildings=true` API также строит terrain surface profile с DEM и OSM buildings, считает Fresnel/LOS и добавляет Bullington-style diffraction loss -в `diffraction_db`. Поле `fresnel_clear` берётся из этого же анализа. +в `diffraction_db`. Поле `fresnel_clear` берётся из этого же анализа. Диагностические +поля `geometric_los`, `worst_obstruction` и счётчики obstruction помогают понять, +почему `link_viable=false`. Модели `p452` и `itm` пока возвращают `501 Not Implemented`. diff --git a/api/app/models/__pycache__/link.cpython-313.pyc b/api/app/models/__pycache__/link.cpython-313.pyc index d81f050..ae9ad9f 100644 Binary files a/api/app/models/__pycache__/link.cpython-313.pyc and b/api/app/models/__pycache__/link.cpython-313.pyc differ diff --git a/api/app/models/link.py b/api/app/models/link.py index 83a4f4d..7b26428 100644 --- a/api/app/models/link.py +++ b/api/app/models/link.py @@ -3,6 +3,7 @@ from typing import Literal from pydantic import BaseModel, Field from app.models.coverage import LinkRx, LinkTx +from app.models.terrain import Obstruction class LinkBudgetRequest(BaseModel): @@ -26,3 +27,10 @@ class LinkBudgetResponse(BaseModel): fade_margin_db: float fresnel_clear: bool link_viable: bool + los_clear: bool + geometric_los: bool + first_fresnel_clearance_pct: float + fresnel_violations_count: int + geometric_obstructions_count: int + building_obstructions_count: int + worst_obstruction: Obstruction | None diff --git a/api/app/services/__pycache__/link.cpython-313.pyc b/api/app/services/__pycache__/link.cpython-313.pyc index 4236a87..7e193b4 100644 Binary files a/api/app/services/__pycache__/link.cpython-313.pyc and b/api/app/services/__pycache__/link.cpython-313.pyc differ diff --git a/api/app/services/link.py b/api/app/services/link.py index c89b617..b7edc89 100644 --- a/api/app/services/link.py +++ b/api/app/services/link.py @@ -1,10 +1,11 @@ from sqlalchemy.orm import Session 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, sample_path from app.core.propagation import manual_link_budget from app.models.link import LinkBudgetRequest, LinkBudgetResponse +from app.models.terrain import Obstruction from app.services.terrain import surface_profile_from_points from app.services.vegetation import vegetation_loss_along @@ -53,4 +54,33 @@ def link_budget(request: LinkBudgetRequest, db: Session | None = None) -> LinkBu vegetation_db=vegetation_db, fresnel_clear=los_result.los_clear, ) - return LinkBudgetResponse(**budget.__dict__) + geometric_obstructions = [ + sample for sample in los_result.obstructions if sample.clearance_m < 0 + ] + building_obstructions = [ + sample + for sample in geometric_obstructions + if sample.obstruction_type == "building" + ] + return LinkBudgetResponse( + **budget.__dict__, + los_clear=los_result.los_clear, + geometric_los=los_result.geometric_los, + first_fresnel_clearance_pct=los_result.first_fresnel_clearance_pct, + fresnel_violations_count=len(los_result.obstructions), + geometric_obstructions_count=len(geometric_obstructions), + building_obstructions_count=len(building_obstructions), + worst_obstruction=_convert_obstruction(los_result.worst_sample), + ) + + +def _convert_obstruction(sample: LosSample | None) -> Obstruction | None: + if sample is None: + return None + 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, + ) diff --git a/api/tests/test_vegetation_integration.py b/api/tests/test_vegetation_integration.py index 9ac7729..f326843 100644 --- a/api/tests/test_vegetation_integration.py +++ b/api/tests/test_vegetation_integration.py @@ -45,6 +45,8 @@ def test_link_budget_includes_vegetation_loss(monkeypatch) -> None: result = link_service.link_budget(request) assert result.vegetation_db == 5.0 + assert result.los_clear is result.fresnel_clear + assert result.fresnel_violations_count >= 0 assert isclose( result.total_loss_db, result.fspl_db + result.diffraction_db + 5.0, @@ -94,3 +96,8 @@ def test_link_budget_includes_diffraction_loss(monkeypatch) -> None: assert result.diffraction_db > 0 assert isclose(result.total_loss_db, result.fspl_db + result.diffraction_db) assert result.fresnel_clear is False + assert result.los_clear is False + assert result.geometric_los is False + assert result.fresnel_violations_count > 0 + assert result.geometric_obstructions_count > 0 + assert result.worst_obstruction is not None