Files
RadioPropagationApi/api/tests/test_vegetation_integration.py
T
2026-06-26 11:55:00 +03:00

263 lines
9.1 KiB
Python

from math import isclose
from app.core.surface import SurfaceProfile, SurfaceSample
from app.models.coverage import LinkRx, LinkTx
from app.models.link import LinkBudgetRequest
from app.services import link as link_service
def test_link_budget_includes_vegetation_loss(monkeypatch) -> None:
monkeypatch.setattr(link_service, "vegetation_loss_along", lambda *args, **kwargs: 5.0)
monkeypatch.setattr(
link_service,
"surface_profile_from_points",
lambda points, *args, **kwargs: SurfaceProfile(
distance_m=points[-1].distance_m,
samples=[
SurfaceSample(
i=index,
lat=point.lat,
lon=point.lon,
distance_m=point.distance_m,
ground_m=0,
building_m=0,
canopy_m=0,
surface_m=0,
)
for index, point in enumerate(points)
],
),
)
request = LinkBudgetRequest(
tx=LinkTx(lat=60.17, lon=24.94, height_agl=30, power_dbm=37, gain_dbi=8),
rx=LinkRx(
lat=60.25,
lon=25.10,
height_agl=2,
gain_dbi=2,
sensitivity_dbm=-110,
),
frequency_mhz=433,
model="manual",
include_vegetation=True,
)
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 + result.vegetation_db + result.atmospheric_db,
)
def test_link_budget_includes_diffraction_loss(monkeypatch) -> None:
monkeypatch.setattr(link_service, "vegetation_loss_along", lambda *args, **kwargs: 0.0)
def fake_profile(points, *args, **kwargs):
midpoint = len(points) // 2
samples = []
for index, point in enumerate(points):
obstacle = 20.0 if index == midpoint else 0.0
samples.append(
SurfaceSample(
i=index,
lat=point.lat,
lon=point.lon,
distance_m=point.distance_m,
ground_m=obstacle,
building_m=0,
canopy_m=0,
surface_m=obstacle,
)
)
return SurfaceProfile(distance_m=points[-1].distance_m, samples=samples)
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=37, gain_dbi=8),
rx=LinkRx(
lat=60.25,
lon=25.10,
height_agl=0,
gain_dbi=2,
sensitivity_dbm=-110,
),
frequency_mhz=433,
model="manual",
include_buildings=False,
include_vegetation=False,
)
result = link_service.link_budget(request)
assert result.diffraction_db > 0
assert result.knife_edge_diffraction_db == result.diffraction_db
assert result.propagation_excess_db == result.diffraction_db
assert isclose(result.propagation_model_loss_db, result.fspl_db + result.diffraction_db)
assert isclose(
result.total_loss_db,
result.fspl_db + result.diffraction_db + result.atmospheric_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
def test_itm_link_budget_does_not_double_count_diffraction(monkeypatch) -> None:
monkeypatch.setattr(link_service, "vegetation_loss_along", lambda *args, **kwargs: 3.0)
monkeypatch.setattr(link_service, "atmospheric_loss_between", lambda *args, **kwargs: 2.0)
monkeypatch.setattr(link_service, "bullington_equivalent_loss", lambda *args, **kwargs: 99.0)
monkeypatch.setattr(
link_service,
"surface_profile_from_points",
lambda points, *args, **kwargs: SurfaceProfile(
distance_m=points[-1].distance_m,
samples=[
SurfaceSample(
i=index,
lat=point.lat,
lon=point.lon,
distance_m=point.distance_m,
ground_m=0,
building_m=0,
canopy_m=0,
surface_m=0,
)
for index, point in enumerate(points)
],
),
)
request = LinkBudgetRequest(
tx=LinkTx(lat=60.17, lon=24.94, height_agl=30, power_dbm=37, gain_dbi=8),
rx=LinkRx(lat=60.25, lon=25.10, height_agl=2, gain_dbi=2, sensitivity_dbm=-110),
frequency_mhz=433,
model="itm",
include_vegetation=True,
)
base = link_service.manual_link_budget(
request.tx,
request.rx,
request.tx.power_dbm,
request.tx.gain_dbi,
request.rx.gain_dbi,
request.rx.sensitivity_dbm,
request.frequency_mhz,
).fspl_db
monkeypatch.setattr(link_service, "itm_loss", lambda *args, **kwargs: base + 12.0)
result = link_service.link_budget(request)
assert result.diffraction_db == 12.0
assert result.knife_edge_diffraction_db == 99.0
assert result.propagation_model_loss_db == result.fspl_db + 12.0
assert result.propagation_excess_db == 12.0
assert result.atmospheric_db == 2.0
assert isclose(result.total_loss_db, result.fspl_db + 12.0 + 3.0 + 2.0)
def test_p452_link_budget_does_not_double_count_diffraction_or_atmosphere(monkeypatch) -> None:
monkeypatch.setattr(link_service, "vegetation_loss_along", lambda *args, **kwargs: 3.0)
monkeypatch.setattr(link_service, "atmospheric_loss_between", lambda *args, **kwargs: 99.0)
monkeypatch.setattr(link_service, "bullington_equivalent_loss", lambda *args, **kwargs: 88.0)
monkeypatch.setattr(
link_service,
"surface_profile_from_points",
lambda points, *args, **kwargs: SurfaceProfile(
distance_m=points[-1].distance_m,
samples=[
SurfaceSample(
i=index,
lat=point.lat,
lon=point.lon,
distance_m=point.distance_m,
ground_m=0,
building_m=0,
canopy_m=0,
surface_m=0,
)
for index, point in enumerate(points)
],
),
)
request = LinkBudgetRequest(
tx=LinkTx(lat=60.17, lon=24.94, height_agl=30, power_dbm=37, gain_dbi=8),
rx=LinkRx(lat=60.25, lon=25.10, height_agl=2, gain_dbi=2, sensitivity_dbm=-110),
frequency_mhz=433,
model="p452",
include_vegetation=True,
)
base = link_service.manual_link_budget(
request.tx,
request.rx,
request.tx.power_dbm,
request.tx.gain_dbi,
request.rx.gain_dbi,
request.rx.sensitivity_dbm,
request.frequency_mhz,
).fspl_db
monkeypatch.setattr(link_service, "p452_loss", lambda *args, **kwargs: base + 14.0)
result = link_service.link_budget(request)
assert result.diffraction_db == 14.0
assert result.knife_edge_diffraction_db == 88.0
assert result.propagation_model_loss_db == result.fspl_db + 14.0
assert result.propagation_excess_db == 14.0
assert result.atmospheric_db == 0.0
assert isclose(result.total_loss_db, result.fspl_db + 14.0 + 3.0)