@mytec: feat: Phase 3.0 Architecture Refactor ✅
Major refactoring of RFCP backend: - Modular propagation models (8 models) - SharedMemoryManager for terrain data - ProcessPoolExecutor parallel processing - WebSocket progress streaming - Building filtering pipeline (351k → 15k) - 82 unit tests Performance: Standard preset 38s → 5s (7.6x speedup) Known issue: Detailed preset timeout (fix in 3.1.0)
This commit is contained in:
21
backend/app/propagation/__init__.py
Normal file
21
backend/app/propagation/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Propagation models for RF coverage calculation.
|
||||
|
||||
Each model implements the PropagationModel interface and is stateless/thread-safe.
|
||||
"""
|
||||
|
||||
from app.propagation.base import PropagationModel, PropagationInput, PropagationOutput
|
||||
from app.propagation.free_space import FreeSpaceModel
|
||||
from app.propagation.okumura_hata import OkumuraHataModel
|
||||
from app.propagation.cost231_hata import Cost231HataModel
|
||||
from app.propagation.cost231_wi import Cost231WIModel
|
||||
from app.propagation.itu_r_p1546 import ITUR_P1546Model
|
||||
from app.propagation.itu_r_p526 import KnifeEdgeDiffractionModel
|
||||
from app.propagation.longley_rice import LongleyRiceModel
|
||||
|
||||
__all__ = [
|
||||
"PropagationModel", "PropagationInput", "PropagationOutput",
|
||||
"FreeSpaceModel", "OkumuraHataModel", "Cost231HataModel",
|
||||
"Cost231WIModel", "ITUR_P1546Model", "KnifeEdgeDiffractionModel",
|
||||
"LongleyRiceModel",
|
||||
]
|
||||
87
backend/app/propagation/base.py
Normal file
87
backend/app/propagation/base.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
62
backend/app/propagation/cost231_hata.py
Normal file
62
backend/app/propagation/cost231_hata.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
COST-231 Hata model (extension of Okumura-Hata).
|
||||
|
||||
Valid for:
|
||||
- Frequency: 1500-2000 MHz
|
||||
- Distance: 1-20 km
|
||||
|
||||
Better for LTE bands than original Okumura-Hata.
|
||||
"""
|
||||
|
||||
import math
|
||||
from app.propagation.base import PropagationModel, PropagationInput, PropagationOutput
|
||||
|
||||
|
||||
class Cost231HataModel(PropagationModel):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "COST-231-Hata"
|
||||
|
||||
@property
|
||||
def frequency_range(self) -> tuple:
|
||||
return (1500, 2000)
|
||||
|
||||
@property
|
||||
def distance_range(self) -> tuple:
|
||||
return (100, 20000)
|
||||
|
||||
def calculate(self, input: PropagationInput) -> PropagationOutput:
|
||||
f = input.frequency_mhz
|
||||
d = max(input.distance_m / 1000, 0.1)
|
||||
hb = max(input.tx_height_m, 1.0)
|
||||
hm = max(input.rx_height_m, 1.0)
|
||||
|
||||
# Mobile antenna correction (medium city)
|
||||
a_hm = (1.1 * math.log10(f) - 0.7) * hm - (1.56 * math.log10(f) - 0.8)
|
||||
|
||||
# Metropolitan center correction
|
||||
C_m = 3 if input.environment == "urban" else 0
|
||||
|
||||
L = (
|
||||
46.3
|
||||
+ 33.9 * math.log10(f)
|
||||
- 13.82 * math.log10(hb)
|
||||
- a_hm
|
||||
+ (44.9 - 6.55 * math.log10(hb)) * math.log10(d)
|
||||
+ C_m
|
||||
)
|
||||
|
||||
return PropagationOutput(
|
||||
path_loss_db=L,
|
||||
model_name=self.name,
|
||||
is_los=False,
|
||||
breakdown={
|
||||
"base_loss": 46.3,
|
||||
"frequency_term": 33.9 * math.log10(f),
|
||||
"height_gain": -13.82 * math.log10(hb),
|
||||
"mobile_correction": -a_hm,
|
||||
"distance_term": (44.9 - 6.55 * math.log10(hb)) * math.log10(d),
|
||||
"metro_correction": C_m,
|
||||
},
|
||||
)
|
||||
114
backend/app/propagation/cost231_wi.py
Normal file
114
backend/app/propagation/cost231_wi.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
COST-231 Walfisch-Ikegami model.
|
||||
|
||||
Valid for:
|
||||
- Frequency: 800-2000 MHz
|
||||
- Distance: 20m-5km
|
||||
- Urban microcell environments
|
||||
|
||||
Accounts for building heights, street widths, and building separation.
|
||||
Reference: COST 231 Final Report, Chapter 4.
|
||||
"""
|
||||
|
||||
import math
|
||||
from app.propagation.base import PropagationModel, PropagationInput, PropagationOutput
|
||||
|
||||
|
||||
class Cost231WIModel(PropagationModel):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "COST-231-WI"
|
||||
|
||||
@property
|
||||
def frequency_range(self) -> tuple:
|
||||
return (800, 2000)
|
||||
|
||||
@property
|
||||
def distance_range(self) -> tuple:
|
||||
return (20, 5000)
|
||||
|
||||
def calculate(self, input: PropagationInput) -> PropagationOutput:
|
||||
f = input.frequency_mhz
|
||||
d = max(input.distance_m / 1000, 0.02) # km
|
||||
hb = max(input.tx_height_m, 4.0)
|
||||
hm = max(input.rx_height_m, 1.0)
|
||||
|
||||
# Building parameters (defaults for typical urban)
|
||||
h_roof = input.building_height_m or 15.0 # avg building height
|
||||
w = input.street_width_m or 20.0 # street width
|
||||
b = input.building_separation_m or 30.0 # building separation
|
||||
|
||||
delta_hb = hb - h_roof # TX above rooftop
|
||||
delta_hm = h_roof - hm # rooftop above RX
|
||||
|
||||
# Free space loss
|
||||
L_fs = 32.45 + 20 * math.log10(d) + 20 * math.log10(f)
|
||||
|
||||
# LOS case
|
||||
if delta_hb > 0 and d < 0.5:
|
||||
L = L_fs
|
||||
return PropagationOutput(
|
||||
path_loss_db=L,
|
||||
model_name=self.name,
|
||||
is_los=True,
|
||||
breakdown={"free_space": L_fs, "rooftop_diffraction": 0, "multiscreen": 0},
|
||||
)
|
||||
|
||||
# Rooftop-to-street diffraction (L_rts)
|
||||
phi = 90.0 # street orientation angle (worst case)
|
||||
if phi < 35:
|
||||
L_ori = -10 + 0.354 * phi
|
||||
elif phi < 55:
|
||||
L_ori = 2.5 + 0.075 * (phi - 35)
|
||||
else:
|
||||
L_ori = 4.0 - 0.114 * (phi - 55)
|
||||
|
||||
L_rts = (
|
||||
-16.9
|
||||
- 10 * math.log10(w)
|
||||
+ 10 * math.log10(f)
|
||||
+ 20 * math.log10(delta_hm)
|
||||
+ L_ori
|
||||
)
|
||||
|
||||
# Multi-screen diffraction (L_msd)
|
||||
if delta_hb > 0:
|
||||
L_bsh = -18 * math.log10(1 + delta_hb)
|
||||
k_a = 54
|
||||
k_d = 18
|
||||
else:
|
||||
L_bsh = 0
|
||||
k_a = 54 - 0.8 * abs(delta_hb)
|
||||
if d >= 0.5:
|
||||
k_a = max(k_a, 54 - 0.8 * abs(delta_hb) * (d / 0.5))
|
||||
k_d = 18 - 15 * abs(delta_hb) / h_roof
|
||||
|
||||
k_f = -4 + 0.7 * (f / 925 - 1) # medium city
|
||||
if input.environment == "urban":
|
||||
k_f = -4 + 1.5 * (f / 925 - 1)
|
||||
|
||||
L_msd = (
|
||||
L_bsh
|
||||
+ k_a
|
||||
+ k_d * math.log10(d)
|
||||
+ k_f * math.log10(f)
|
||||
- 9 * math.log10(b)
|
||||
)
|
||||
|
||||
# Total NLOS loss
|
||||
if L_rts + L_msd > 0:
|
||||
L = L_fs + L_rts + L_msd
|
||||
else:
|
||||
L = L_fs
|
||||
|
||||
return PropagationOutput(
|
||||
path_loss_db=L,
|
||||
model_name=self.name,
|
||||
is_los=False,
|
||||
breakdown={
|
||||
"free_space": L_fs,
|
||||
"rooftop_diffraction": max(L_rts, 0),
|
||||
"multiscreen": max(L_msd, 0),
|
||||
},
|
||||
)
|
||||
43
backend/app/propagation/free_space.py
Normal file
43
backend/app/propagation/free_space.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Free Space Path Loss (FSPL) model.
|
||||
|
||||
Used as baseline and for LOS conditions.
|
||||
FSPL = 20*log10(d) + 20*log10(f) + 32.45
|
||||
where d in km, f in MHz
|
||||
"""
|
||||
|
||||
import math
|
||||
from app.propagation.base import PropagationModel, PropagationInput, PropagationOutput
|
||||
|
||||
|
||||
class FreeSpaceModel(PropagationModel):
|
||||
"""Free Space Path Loss — theoretical minimum propagation loss."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Free-Space"
|
||||
|
||||
@property
|
||||
def frequency_range(self) -> tuple:
|
||||
return (1, 100000)
|
||||
|
||||
@property
|
||||
def distance_range(self) -> tuple:
|
||||
return (1, 1000000) # 1m to 1000km
|
||||
|
||||
def calculate(self, input: PropagationInput) -> PropagationOutput:
|
||||
d_km = max(input.distance_m / 1000, 0.001)
|
||||
f = input.frequency_mhz
|
||||
|
||||
L = 20 * math.log10(d_km) + 20 * math.log10(f) + 32.45
|
||||
|
||||
return PropagationOutput(
|
||||
path_loss_db=L,
|
||||
model_name=self.name,
|
||||
is_los=True,
|
||||
breakdown={
|
||||
"distance_loss": 20 * math.log10(d_km),
|
||||
"frequency_loss": 20 * math.log10(f),
|
||||
"constant": 32.45,
|
||||
},
|
||||
)
|
||||
74
backend/app/propagation/itu_r_p1546.py
Normal file
74
backend/app/propagation/itu_r_p1546.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
ITU-R P.1546 model for point-to-area predictions.
|
||||
|
||||
Valid for:
|
||||
- Frequency: 30-3000 MHz
|
||||
- Distance: 1-1000 km
|
||||
- Time percentages: 1%, 10%, 50%
|
||||
|
||||
Best for: VHF/UHF broadcasting and land mobile services.
|
||||
Reference: ITU-R P.1546-6 (2019)
|
||||
"""
|
||||
|
||||
import math
|
||||
from app.propagation.base import PropagationModel, PropagationInput, PropagationOutput
|
||||
|
||||
|
||||
class ITUR_P1546Model(PropagationModel):
|
||||
"""
|
||||
Simplified P.1546 implementation.
|
||||
|
||||
Full implementation would include terrain clearance angle,
|
||||
mixed path (land/sea), and time variability.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "ITU-R-P.1546"
|
||||
|
||||
@property
|
||||
def frequency_range(self) -> tuple:
|
||||
return (30, 3000)
|
||||
|
||||
@property
|
||||
def distance_range(self) -> tuple:
|
||||
return (1000, 1000000) # 1-1000 km
|
||||
|
||||
def calculate(self, input: PropagationInput) -> PropagationOutput:
|
||||
f = input.frequency_mhz
|
||||
d = max(input.distance_m / 1000, 1.0) # km
|
||||
h1 = max(input.tx_height_m, 1.0)
|
||||
|
||||
# Nominal frequency bands
|
||||
if f < 100:
|
||||
f_nom = 100
|
||||
elif f < 600:
|
||||
f_nom = 600
|
||||
else:
|
||||
f_nom = 2000
|
||||
|
||||
# Basic field strength at 1 kW ERP (from curves, simplified regression)
|
||||
E_ref = 106.9 - 20 * math.log10(d) # dBuV/m at 1kW
|
||||
|
||||
# Height gain for transmitter
|
||||
delta_h1 = 20 * math.log10(h1 / 10) if h1 > 10 else 0
|
||||
|
||||
# Frequency correction
|
||||
delta_f = 20 * math.log10(f / f_nom)
|
||||
|
||||
# Convert field strength to path loss
|
||||
# L = 139.3 - E + 20*log10(f) (for 50 Ohm)
|
||||
E = E_ref + delta_h1 - delta_f
|
||||
L = 139.3 - E + 20 * math.log10(f)
|
||||
|
||||
return PropagationOutput(
|
||||
path_loss_db=L,
|
||||
model_name=self.name,
|
||||
is_los=d < 5,
|
||||
breakdown={
|
||||
"reference_field": E_ref,
|
||||
"height_gain": delta_h1,
|
||||
"frequency_correction": delta_f,
|
||||
"path_loss": L,
|
||||
},
|
||||
)
|
||||
87
backend/app/propagation/itu_r_p526.py
Normal file
87
backend/app/propagation/itu_r_p526.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Knife-edge diffraction model based on ITU-R P.526.
|
||||
|
||||
Used for calculating additional loss when terrain or obstacles
|
||||
block the line of sight between TX and RX.
|
||||
|
||||
Reference: ITU-R P.526-15
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
|
||||
class KnifeEdgeDiffractionModel:
|
||||
"""
|
||||
Single knife-edge diffraction model.
|
||||
|
||||
Stateless utility — not a full PropagationModel since it calculates
|
||||
additional loss, not total path loss.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_loss(
|
||||
d1_m: float,
|
||||
d2_m: float,
|
||||
h_m: float,
|
||||
wavelength_m: float,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate diffraction loss over single knife edge.
|
||||
|
||||
Args:
|
||||
d1_m: Distance from TX to obstacle
|
||||
d2_m: Distance from obstacle to RX
|
||||
h_m: Obstacle height above LOS line (positive = above)
|
||||
wavelength_m: Signal wavelength
|
||||
|
||||
Returns:
|
||||
Loss in dB (always >= 0)
|
||||
"""
|
||||
if d1_m <= 0 or d2_m <= 0 or wavelength_m <= 0:
|
||||
return 0.0
|
||||
|
||||
# Fresnel-Kirchhoff parameter
|
||||
v = h_m * math.sqrt(2 * (d1_m + d2_m) / (wavelength_m * d1_m * d2_m))
|
||||
|
||||
# Diffraction loss (Lee approximation)
|
||||
if v < -0.78:
|
||||
L = 0.0
|
||||
elif v < 0:
|
||||
L = 6.02 + 9.11 * v - 1.27 * v ** 2
|
||||
elif v < 2.4:
|
||||
L = 6.02 + 9.11 * v + 1.65 * v ** 2
|
||||
else:
|
||||
L = 12.95 + 20 * math.log10(v)
|
||||
|
||||
return max(0.0, L)
|
||||
|
||||
@staticmethod
|
||||
def calculate_clearance_loss(
|
||||
clearance_m: float,
|
||||
frequency_mhz: float,
|
||||
) -> float:
|
||||
"""
|
||||
Simplified diffraction loss from terrain clearance.
|
||||
|
||||
Matches the existing coverage_service._diffraction_loss logic.
|
||||
|
||||
Args:
|
||||
clearance_m: Minimum LOS clearance (negative = blocked)
|
||||
frequency_mhz: Signal frequency
|
||||
|
||||
Returns:
|
||||
Loss in dB (0 if positive clearance)
|
||||
"""
|
||||
if clearance_m >= 0:
|
||||
return 0.0
|
||||
|
||||
v = abs(clearance_m) / 10
|
||||
|
||||
if v <= 0:
|
||||
loss = 0.0
|
||||
elif v < 2.4:
|
||||
loss = 6.02 + 9.11 * v - 1.27 * v ** 2
|
||||
else:
|
||||
loss = 13.0 + 20 * math.log10(v)
|
||||
|
||||
return min(loss, 40.0)
|
||||
75
backend/app/propagation/longley_rice.py
Normal file
75
backend/app/propagation/longley_rice.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Longley-Rice Irregular Terrain Model (ITM).
|
||||
|
||||
Best for:
|
||||
- VHF/UHF over irregular terrain
|
||||
- Point-to-point links
|
||||
- Distances 1-2000 km
|
||||
|
||||
Note: This is a simplified area-mode version.
|
||||
Full implementation requires terrain profile data.
|
||||
|
||||
Reference: NTIA Report 82-100
|
||||
"""
|
||||
|
||||
import math
|
||||
from app.propagation.base import PropagationModel, PropagationInput, PropagationOutput
|
||||
|
||||
|
||||
class LongleyRiceModel(PropagationModel):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Longley-Rice"
|
||||
|
||||
@property
|
||||
def frequency_range(self) -> tuple:
|
||||
return (20, 20000) # 20 MHz to 20 GHz
|
||||
|
||||
@property
|
||||
def distance_range(self) -> tuple:
|
||||
return (1000, 2000000) # 1-2000 km
|
||||
|
||||
def calculate(self, input: PropagationInput) -> PropagationOutput:
|
||||
"""
|
||||
Simplified Longley-Rice (area mode).
|
||||
|
||||
For proper implementation, use splat! or NTIA ITM reference.
|
||||
"""
|
||||
f = input.frequency_mhz
|
||||
d = max(input.distance_m / 1000, 1.0)
|
||||
h1 = max(input.tx_height_m, 1.0)
|
||||
h2 = max(input.rx_height_m, 1.0)
|
||||
|
||||
# Terrain irregularity parameter (simplified)
|
||||
delta_h = input.terrain_roughness_m or 90 # Default: rolling hills
|
||||
|
||||
# Free space loss
|
||||
L_fs = 32.45 + 20 * math.log10(d) + 20 * math.log10(f)
|
||||
|
||||
# Terrain clutter loss (simplified)
|
||||
if delta_h < 10:
|
||||
L_terrain = 0 # Flat
|
||||
elif delta_h < 50:
|
||||
L_terrain = 5 # Gently rolling
|
||||
elif delta_h < 150:
|
||||
L_terrain = 10 # Rolling hills
|
||||
else:
|
||||
L_terrain = 15 # Mountains
|
||||
|
||||
# Height gain
|
||||
h_eff = h1 + h2
|
||||
height_gain = 10 * math.log10(h_eff / 20) if h_eff > 20 else 0
|
||||
|
||||
L = L_fs + L_terrain - height_gain
|
||||
|
||||
return PropagationOutput(
|
||||
path_loss_db=L,
|
||||
model_name=self.name,
|
||||
is_los=delta_h < 10 and d < 10,
|
||||
breakdown={
|
||||
"free_space_loss": L_fs,
|
||||
"terrain_loss": L_terrain,
|
||||
"height_gain": height_gain,
|
||||
},
|
||||
)
|
||||
74
backend/app/propagation/okumura_hata.py
Normal file
74
backend/app/propagation/okumura_hata.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Okumura-Hata empirical propagation model.
|
||||
|
||||
Valid for:
|
||||
- Frequency: 150-1500 MHz
|
||||
- Distance: 1-20 km
|
||||
- TX height: 30-200 m
|
||||
- RX height: 1-10 m
|
||||
|
||||
Reference: Hata (1980), "Empirical Formula for Propagation Loss
|
||||
in Land Mobile Radio Services"
|
||||
"""
|
||||
|
||||
import math
|
||||
from app.propagation.base import PropagationModel, PropagationInput, PropagationOutput
|
||||
|
||||
|
||||
class OkumuraHataModel(PropagationModel):
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Okumura-Hata"
|
||||
|
||||
@property
|
||||
def frequency_range(self) -> tuple:
|
||||
return (150, 1500)
|
||||
|
||||
@property
|
||||
def distance_range(self) -> tuple:
|
||||
return (100, 20000) # Extended to 100m minimum for practical use
|
||||
|
||||
def calculate(self, input: PropagationInput) -> PropagationOutput:
|
||||
f = input.frequency_mhz
|
||||
d = max(input.distance_m / 1000, 0.1) # km, min 100m
|
||||
hb = max(input.tx_height_m, 1.0)
|
||||
hm = max(input.rx_height_m, 1.0)
|
||||
|
||||
# Mobile antenna height correction factor
|
||||
if input.environment == "urban" and f >= 400:
|
||||
# Large city
|
||||
a_hm = 3.2 * (math.log10(11.75 * hm) ** 2) - 4.97
|
||||
else:
|
||||
# Medium/small city
|
||||
a_hm = (1.1 * math.log10(f) - 0.7) * hm - (1.56 * math.log10(f) - 0.8)
|
||||
|
||||
# Basic path loss (urban)
|
||||
L_urban = (
|
||||
69.55
|
||||
+ 26.16 * math.log10(f)
|
||||
- 13.82 * math.log10(hb)
|
||||
- a_hm
|
||||
+ (44.9 - 6.55 * math.log10(hb)) * math.log10(d)
|
||||
)
|
||||
|
||||
# Environment correction
|
||||
if input.environment == "suburban":
|
||||
L = L_urban - 2 * (math.log10(f / 28) ** 2) - 5.4
|
||||
elif input.environment == "rural":
|
||||
L = L_urban - 4.78 * (math.log10(f) ** 2) + 18.33 * math.log10(f) - 35.94
|
||||
elif input.environment == "open":
|
||||
L = L_urban - 4.78 * (math.log10(f) ** 2) + 18.33 * math.log10(f) - 40.94
|
||||
else:
|
||||
L = L_urban
|
||||
|
||||
return PropagationOutput(
|
||||
path_loss_db=L,
|
||||
model_name=self.name,
|
||||
is_los=False,
|
||||
breakdown={
|
||||
"basic_loss": L_urban,
|
||||
"environment_correction": L - L_urban,
|
||||
"antenna_correction": a_hm,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user