""" Abstract base class for all propagation models. Each model implements a single, well-defined propagation algorithm. Models are stateless and can be called concurrently. """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Optional @dataclass class PropagationInput: """Input for propagation calculation.""" frequency_mhz: float distance_m: float tx_height_m: float rx_height_m: float environment: str = "urban" # urban, suburban, rural, open # Optional terrain info terrain_clearance_m: Optional[float] = None terrain_roughness_m: Optional[float] = None # Optional building info building_height_m: Optional[float] = None street_width_m: Optional[float] = None building_separation_m: Optional[float] = None @dataclass class PropagationOutput: """Output from propagation calculation.""" path_loss_db: float model_name: str is_los: bool breakdown: dict = field(default_factory=dict) class PropagationModel(ABC): """ Abstract base class for all propagation models. Each model implements a single, well-defined propagation algorithm. Models are stateless and can be called concurrently. """ @property @abstractmethod def name(self) -> str: """Model name for logging/display.""" pass @property @abstractmethod def frequency_range(self) -> tuple: """Valid frequency range (min_mhz, max_mhz).""" pass @property @abstractmethod def distance_range(self) -> tuple: """Valid distance range (min_m, max_m).""" pass @abstractmethod def calculate(self, input: PropagationInput) -> PropagationOutput: """ Calculate path loss for given input. This method MUST be: - Stateless (no side effects) - Thread-safe (can be called concurrently) - Fast (no I/O, no heavy computation) """ pass def is_valid_for(self, input: PropagationInput) -> bool: """Check if this model is valid for given input.""" freq_min, freq_max = self.frequency_range dist_min, dist_max = self.distance_range return ( freq_min <= input.frequency_mhz <= freq_max and dist_min <= input.distance_m <= dist_max )