55 lines
1.8 KiB
Python
55 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from math import exp
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AntennaPattern:
|
|
pattern: str = "omni"
|
|
azimuth_deg: float = 0.0
|
|
tilt_deg: float = 0.0
|
|
gain_dbi: float = 0.0
|
|
beamwidth_h: float | None = None
|
|
beamwidth_v: float | None = None
|
|
front_to_back_db: float = 25.0
|
|
sidelobe_floor_db: float = -30.0
|
|
pattern_file: str | None = None
|
|
|
|
|
|
def _angular_delta(angle: float, center: float) -> float:
|
|
return (angle - center + 180.0) % 360.0 - 180.0
|
|
|
|
|
|
def _gaussian_drop(delta: float, beamwidth: float) -> float:
|
|
if beamwidth <= 0:
|
|
raise ValueError("beamwidth must be positive")
|
|
sigma = beamwidth / 2.355
|
|
normalized = exp(-0.5 * (delta / sigma) ** 2)
|
|
if normalized <= 0:
|
|
return float("-inf")
|
|
return 10.0 * (normalized - 1.0)
|
|
|
|
|
|
def gain(pattern: AntennaPattern, az_deg: float, el_deg: float) -> float:
|
|
if pattern.pattern == "omni":
|
|
return pattern.gain_dbi
|
|
if pattern.pattern == "file":
|
|
from app.core.antenna_pattern_file import gain_from_file
|
|
|
|
return gain_from_file(pattern, az_deg, el_deg)
|
|
if pattern.pattern != "sector":
|
|
raise ValueError(f"Unknown antenna pattern: {pattern.pattern}")
|
|
|
|
if pattern.beamwidth_h is None or pattern.beamwidth_v is None:
|
|
raise ValueError("sector pattern requires horizontal and vertical beamwidths")
|
|
|
|
az_delta = _angular_delta(az_deg, pattern.azimuth_deg)
|
|
el_delta = el_deg - pattern.tilt_deg
|
|
h_drop = _gaussian_drop(az_delta, pattern.beamwidth_h)
|
|
v_drop = _gaussian_drop(el_delta, pattern.beamwidth_v)
|
|
attenuation = max(pattern.sidelobe_floor_db, h_drop + v_drop)
|
|
if abs(az_delta) > 90:
|
|
attenuation = min(attenuation, -pattern.front_to_back_db)
|
|
return pattern.gain_dbi + attenuation
|