156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
from sqlalchemy.orm import Session
|
|
|
|
from app.config import get_settings
|
|
from app.core.atmosphere import atmospheric_loss_between
|
|
from app.core.diffraction import deygout
|
|
from app.core.fresnel import LosSample, los_analysis
|
|
from app.core.geo import GeoPoint, sample_path
|
|
from app.core.propagation import itm_loss, manual_link_budget, p452_loss
|
|
from app.core.raster_sampling import raster_files
|
|
from app.models.common import DataSources
|
|
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
|
|
|
|
|
|
def link_budget(request: LinkBudgetRequest, db: Session | None = None) -> LinkBudgetResponse:
|
|
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)
|
|
freq_hz = request.frequency_mhz * 1_000_000
|
|
settings = get_settings()
|
|
surface_profile = surface_profile_from_points(
|
|
points,
|
|
include_buildings=request.include_buildings,
|
|
include_canopy=request.include_canopy,
|
|
db=db,
|
|
)
|
|
elevation_profile = [sample.ground_m for sample in surface_profile.samples]
|
|
los_result = los_analysis(
|
|
surface_profile,
|
|
tx_height_agl=request.tx.height_agl,
|
|
rx_height_agl=request.rx.height_agl,
|
|
freq_hz=freq_hz,
|
|
k=request.k_factor,
|
|
)
|
|
diffraction_db = deygout(
|
|
surface_profile,
|
|
tx_height_agl=request.tx.height_agl,
|
|
rx_height_agl=request.rx.height_agl,
|
|
freq_hz=freq_hz,
|
|
)
|
|
vegetation_db = vegetation_loss_along(
|
|
points,
|
|
freq_hz=freq_hz,
|
|
include_vegetation=request.include_vegetation,
|
|
)
|
|
atmospheric_db = atmospheric_loss_between(tx, rx, freq_hz)
|
|
|
|
if request.model == "manual":
|
|
propagation_db = 0.0
|
|
elif request.model == "itm":
|
|
propagation_db = (
|
|
itm_loss(
|
|
tx,
|
|
rx,
|
|
tx_height_agl=request.tx.height_agl,
|
|
rx_height_agl=request.rx.height_agl,
|
|
freq_mhz=request.frequency_mhz,
|
|
elevation_profile_m=elevation_profile,
|
|
dem_path=str(settings.dem_path),
|
|
)
|
|
- manual_link_budget(
|
|
tx,
|
|
rx,
|
|
request.tx.power_dbm,
|
|
request.tx.gain_dbi,
|
|
request.rx.gain_dbi,
|
|
request.rx.sensitivity_dbm,
|
|
request.frequency_mhz,
|
|
).fspl_db
|
|
)
|
|
elif request.model == "p452":
|
|
propagation_db = (
|
|
p452_loss(
|
|
tx,
|
|
rx,
|
|
tx_height_agl=request.tx.height_agl,
|
|
rx_height_agl=request.rx.height_agl,
|
|
freq_mhz=request.frequency_mhz,
|
|
elevation_profile_m=elevation_profile,
|
|
dem_path=str(settings.dem_path),
|
|
)
|
|
- manual_link_budget(
|
|
tx,
|
|
rx,
|
|
request.tx.power_dbm,
|
|
request.tx.gain_dbi,
|
|
request.rx.gain_dbi,
|
|
request.rx.sensitivity_dbm,
|
|
request.frequency_mhz,
|
|
).fspl_db
|
|
)
|
|
else:
|
|
raise NotImplementedError(f"{request.model} link model is not supported")
|
|
|
|
budget = manual_link_budget(
|
|
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,
|
|
diffraction_db=diffraction_db + propagation_db,
|
|
vegetation_db=vegetation_db,
|
|
atmospheric_db=atmospheric_db,
|
|
fresnel_clear=los_result.los_clear,
|
|
)
|
|
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__,
|
|
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,
|
|
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),
|
|
data_sources=DataSources(
|
|
dem=bool(raster_files(settings.dem_path)),
|
|
buildings=request.include_buildings and db is not None,
|
|
landcover=request.include_vegetation
|
|
and bool(raster_files(settings.landcover_path)),
|
|
canopy=request.include_canopy and bool(raster_files(settings.canopy_path)),
|
|
),
|
|
)
|
|
|
|
|
|
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
|
|
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,
|
|
)
|