added land
This commit is contained in:
Binary file not shown.
@@ -16,6 +16,8 @@ class Settings(BaseSettings):
|
||||
landcover_path: Path = Field(default=Path("/data/landcover"))
|
||||
canopy_path: Path = Field(default=Path("/data/canopy"))
|
||||
buildings_query_limit: int = Field(default=5000, gt=0)
|
||||
p833_gamma_db_per_m: float = Field(default=0.15, ge=0)
|
||||
p833_max_attenuation_db: float = Field(default=25.0, gt=0)
|
||||
|
||||
|
||||
@lru_cache
|
||||
|
||||
Binary file not shown.
@@ -44,6 +44,7 @@ class LosRequest(BaseModel):
|
||||
fresnel_clearance: float = Field(default=0.6, ge=0)
|
||||
include_buildings: bool = True
|
||||
include_canopy: bool = True
|
||||
include_vegetation: bool = True
|
||||
k_factor: float = Field(default=1.333, gt=0)
|
||||
samples: int = Field(default=512, ge=2, le=10000)
|
||||
|
||||
@@ -67,4 +68,5 @@ class LosResponse(BaseModel):
|
||||
geometric_obstructions_count: int
|
||||
building_obstructions_count: int
|
||||
diffraction_loss_db: float
|
||||
vegetation_loss_db: float
|
||||
profile_ref: str
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,19 +1,29 @@
|
||||
from app.core.geo import GeoPoint
|
||||
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.services.vegetation import vegetation_loss_along
|
||||
|
||||
|
||||
def link_budget(request: LinkBudgetRequest) -> LinkBudgetResponse:
|
||||
if request.model != "manual":
|
||||
raise NotImplementedError(f"{request.model} link model is implemented in a later stage")
|
||||
|
||||
tx = GeoPoint(lat=request.tx.lat, lon=request.tx.lon)
|
||||
rx = GeoPoint(lat=request.rx.lat, lon=request.rx.lon)
|
||||
points = sample_path(tx, rx, 256)
|
||||
vegetation_db = vegetation_loss_along(
|
||||
points,
|
||||
freq_hz=request.frequency_mhz * 1_000_000,
|
||||
include_vegetation=request.include_vegetation,
|
||||
)
|
||||
budget = manual_link_budget(
|
||||
tx=GeoPoint(lat=request.tx.lat, lon=request.tx.lon),
|
||||
rx=GeoPoint(lat=request.rx.lat, lon=request.rx.lon),
|
||||
tx=tx,
|
||||
rx=rx,
|
||||
tx_power_dbm=request.tx.power_dbm,
|
||||
tx_gain_dbi=request.tx.gain_dbi,
|
||||
rx_gain_dbi=request.rx.gain_dbi,
|
||||
sensitivity_dbm=request.rx.sensitivity_dbm,
|
||||
frequency_mhz=request.frequency_mhz,
|
||||
vegetation_db=vegetation_db,
|
||||
)
|
||||
return LinkBudgetResponse(**budget.__dict__)
|
||||
|
||||
@@ -20,6 +20,7 @@ from app.models.terrain import (
|
||||
TerrainProfileResponse,
|
||||
)
|
||||
from app.services import buildings as buildings_service
|
||||
from app.services.vegetation import vegetation_loss_along
|
||||
|
||||
|
||||
def elevation_at(lat: float, lon: float, surface: str) -> ElevationResponse:
|
||||
@@ -101,6 +102,11 @@ def los(
|
||||
request.rx.height_agl,
|
||||
request.frequency_mhz * 1_000_000,
|
||||
)
|
||||
vegetation_loss = vegetation_loss_along(
|
||||
points,
|
||||
freq_hz=request.frequency_mhz * 1_000_000,
|
||||
include_vegetation=request.include_vegetation,
|
||||
)
|
||||
|
||||
def convert(sample: LosSample) -> Obstruction:
|
||||
return Obstruction(
|
||||
@@ -131,6 +137,7 @@ def los(
|
||||
geometric_obstructions_count=len(geometric_obstructions),
|
||||
building_obstructions_count=len(building_obstructions),
|
||||
diffraction_loss_db=diffraction_loss,
|
||||
vegetation_loss_db=vegetation_loss,
|
||||
profile_ref=f"inline:{profile_request.samples}",
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import get_settings
|
||||
from app.core.geo import PathPoint
|
||||
from app.core.landcover import LandcoverNotConfiguredError, landcover_path
|
||||
from app.core.vegetation import P833Coefficients, p833_attenuation
|
||||
|
||||
|
||||
def vegetation_loss_along(
|
||||
points: list[PathPoint],
|
||||
freq_hz: float,
|
||||
include_vegetation: bool,
|
||||
landcover_path_dir: str | Path | None = None,
|
||||
canopy_path_dir: str | Path | None = None,
|
||||
) -> float:
|
||||
if not include_vegetation:
|
||||
return 0.0
|
||||
|
||||
settings = get_settings()
|
||||
landcover_dir = landcover_path_dir or settings.landcover_path
|
||||
canopy_dir = canopy_path_dir or settings.canopy_path
|
||||
|
||||
try:
|
||||
path = landcover_path(points, landcover_dir, canopy_dir)
|
||||
except LandcoverNotConfiguredError:
|
||||
return 0.0
|
||||
|
||||
coefficients = P833Coefficients(
|
||||
gamma_db_per_m=settings.p833_gamma_db_per_m,
|
||||
max_attenuation_db=settings.p833_max_attenuation_db,
|
||||
)
|
||||
return p833_attenuation(
|
||||
depth_m=path.vegetation_depth_m,
|
||||
freq_hz=freq_hz,
|
||||
forest_type="unknown",
|
||||
coefficients=coefficients,
|
||||
)
|
||||
Binary file not shown.
@@ -65,6 +65,7 @@ def test_los_endpoint_returns_obstruction_summary_fields() -> None:
|
||||
assert "geometric_obstructions" in data
|
||||
assert data["fresnel_violations_count"] == len(data["obstructions"])
|
||||
assert data["geometric_obstructions_count"] == len(data["geometric_obstructions"])
|
||||
assert "vegetation_loss_db" in data
|
||||
assert data["building_obstructions_count"] == len(
|
||||
[item for item in data["geometric_obstructions"] if item["type"] == "building"]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
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.total_loss_db == result.fspl_db + 5.0
|
||||
Reference in New Issue
Block a user