added fresnel zone
This commit is contained in:
@@ -337,6 +337,7 @@ curl -s -X POST http://localhost:5603/api/v1/link/budget \
|
||||
- `rx_power_dbm`
|
||||
- `fade_margin_db`
|
||||
- `fresnel_clear`
|
||||
- `fresnel_quality`: `clear`, `partial` или `blocked`
|
||||
- `link_viable`
|
||||
- `los_clear`
|
||||
- `geometric_los`
|
||||
@@ -352,9 +353,18 @@ 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` берётся из этого же анализа. Диагностические
|
||||
поля `geometric_los`, `worst_obstruction` и счётчики obstruction помогают понять,
|
||||
почему `link_viable=false`.
|
||||
в `diffraction_db`. Поле `fresnel_clear` берётся из этого же анализа.
|
||||
|
||||
`link_viable` считается по фактическому `fade_margin_db > 0` после применённых
|
||||
потерь. Состояние зоны Френеля не выключает линк напрямую, а отдаётся отдельным
|
||||
качеством:
|
||||
|
||||
- `clear`: нет нарушений требуемых 60% первой зоны Френеля.
|
||||
- `partial`: геометрическая LOS есть, но 60% зоны Френеля частично перекрыта.
|
||||
- `blocked`: геометрическая линия TX-RX пересекает terrain/buildings/canopy.
|
||||
|
||||
Диагностические поля `geometric_los`, `worst_obstruction` и счётчики obstruction
|
||||
помогают понять, почему качество Френеля ухудшилось.
|
||||
|
||||
Модели `p452` и `itm` пока возвращают `501 Not Implemented`.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -65,5 +65,5 @@ def manual_link_budget(
|
||||
rx_power_dbm=rx_power,
|
||||
fade_margin_db=margin,
|
||||
fresnel_clear=fresnel_clear,
|
||||
link_viable=margin > 0 and fresnel_clear,
|
||||
link_viable=margin > 0,
|
||||
)
|
||||
|
||||
Binary file not shown.
@@ -27,6 +27,7 @@ class LinkBudgetResponse(BaseModel):
|
||||
rx_power_dbm: float
|
||||
fade_margin_db: float
|
||||
fresnel_clear: bool
|
||||
fresnel_quality: Literal["clear", "partial", "blocked"]
|
||||
link_viable: bool
|
||||
los_clear: bool
|
||||
geometric_los: bool
|
||||
|
||||
Binary file not shown.
@@ -68,6 +68,7 @@ def link_budget(request: LinkBudgetRequest, db: Session | None = None) -> LinkBu
|
||||
]
|
||||
return LinkBudgetResponse(
|
||||
**budget.__dict__,
|
||||
fresnel_quality=_fresnel_quality(los_result.los_clear, los_result.geometric_los),
|
||||
los_clear=los_result.los_clear,
|
||||
geometric_los=los_result.geometric_los,
|
||||
first_fresnel_clearance_pct=los_result.first_fresnel_clearance_pct,
|
||||
@@ -85,6 +86,14 @@ def link_budget(request: LinkBudgetRequest, db: Session | None = None) -> LinkBu
|
||||
)
|
||||
|
||||
|
||||
def _fresnel_quality(los_clear: bool, geometric_los: bool) -> str:
|
||||
if los_clear:
|
||||
return "clear"
|
||||
if geometric_los:
|
||||
return "partial"
|
||||
return "blocked"
|
||||
|
||||
|
||||
def _convert_obstruction(sample: LosSample | None) -> Obstruction | None:
|
||||
if sample is None:
|
||||
return None
|
||||
|
||||
Binary file not shown.
@@ -123,4 +123,5 @@ def test_manual_link_budget_endpoint() -> None:
|
||||
data = response.json()
|
||||
assert data["fspl_db"] > 0
|
||||
assert "link_viable" in data
|
||||
assert data["fresnel_quality"] in {"clear", "partial", "blocked"}
|
||||
assert "data_sources" in data
|
||||
|
||||
@@ -97,7 +97,54 @@ def test_link_budget_includes_diffraction_loss(monkeypatch) -> None:
|
||||
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.fresnel_quality == "blocked"
|
||||
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
|
||||
|
||||
|
||||
def test_link_viable_depends_on_margin_not_fresnel_quality(monkeypatch) -> None:
|
||||
monkeypatch.setattr(link_service, "vegetation_loss_along", lambda *args, **kwargs: 0.0)
|
||||
|
||||
def fake_profile(points, *args, **kwargs):
|
||||
midpoint = len(points) // 2
|
||||
return SurfaceProfile(
|
||||
distance_m=points[-1].distance_m,
|
||||
samples=[
|
||||
SurfaceSample(
|
||||
i=index,
|
||||
lat=point.lat,
|
||||
lon=point.lon,
|
||||
distance_m=point.distance_m,
|
||||
ground_m=20.0 if index == midpoint else 0.0,
|
||||
building_m=0,
|
||||
canopy_m=0,
|
||||
surface_m=20.0 if index == midpoint else 0.0,
|
||||
)
|
||||
for index, point in enumerate(points)
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(link_service, "surface_profile_from_points", fake_profile)
|
||||
request = LinkBudgetRequest(
|
||||
tx=LinkTx(lat=60.17, lon=24.94, height_agl=0, power_dbm=70, gain_dbi=8),
|
||||
rx=LinkRx(
|
||||
lat=60.25,
|
||||
lon=25.10,
|
||||
height_agl=0,
|
||||
gain_dbi=2,
|
||||
sensitivity_dbm=-120,
|
||||
),
|
||||
frequency_mhz=433,
|
||||
model="manual",
|
||||
include_buildings=False,
|
||||
include_vegetation=False,
|
||||
)
|
||||
|
||||
result = link_service.link_budget(request)
|
||||
|
||||
assert result.fade_margin_db > 0
|
||||
assert result.fresnel_quality == "blocked"
|
||||
assert result.fresnel_clear is False
|
||||
assert result.link_viable is True
|
||||
|
||||
Reference in New Issue
Block a user