from sqlalchemy.orm import Session from app.config import get_settings from app.core.atmosphere import atmospheric_loss_between from app.core.diffraction import bullington_equivalent_loss 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, ) knife_edge_diffraction_db = bullington_equivalent_loss( surface_profile, tx_height_agl=request.tx.height_agl, rx_height_agl=request.rx.height_agl, freq_hz=freq_hz, k=request.k_factor, ) vegetation_db = vegetation_loss_along( points, freq_hz=freq_hz, include_vegetation=request.include_vegetation, ) atmospheric_db = atmospheric_loss_between(tx, rx, freq_hz) fspl_db = 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 active_diffraction_db = knife_edge_diffraction_db active_atmospheric_db = atmospheric_db propagation_model_loss_db = fspl_db + knife_edge_diffraction_db propagation_excess_db = knife_edge_diffraction_db if request.model == "manual": pass elif request.model == "itm": propagation_model_loss_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), ) propagation_excess_db = propagation_model_loss_db - fspl_db active_diffraction_db = propagation_excess_db elif request.model == "p452": propagation_model_loss_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), ) propagation_excess_db = propagation_model_loss_db - fspl_db active_diffraction_db = propagation_excess_db active_atmospheric_db = 0.0 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=active_diffraction_db, vegetation_db=vegetation_db, atmospheric_db=active_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), propagation_model_loss_db=propagation_model_loss_db, propagation_excess_db=propagation_excess_db, knife_edge_diffraction_db=knife_edge_diffraction_db, 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, )