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)
94 lines
3.4 KiB
Python
94 lines
3.4 KiB
Python
"""
|
|
Detailed unit tests for the Okumura-Hata model.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
|
|
from app.propagation.base import PropagationInput
|
|
from app.propagation.okumura_hata import OkumuraHataModel
|
|
|
|
|
|
def make_input(**kwargs) -> PropagationInput:
|
|
defaults = {
|
|
"frequency_mhz": 900,
|
|
"distance_m": 5000,
|
|
"tx_height_m": 30,
|
|
"rx_height_m": 1.5,
|
|
"environment": "urban",
|
|
}
|
|
defaults.update(kwargs)
|
|
return PropagationInput(**defaults)
|
|
|
|
|
|
class TestOkumuraHata:
|
|
def test_urban_typical_range(self):
|
|
model = OkumuraHataModel()
|
|
out = model.calculate(make_input())
|
|
# 900MHz, 5km, urban: expect ~130-155 dB
|
|
assert 120 < out.path_loss_db < 160
|
|
|
|
def test_environment_ordering(self):
|
|
"""Urban > suburban > rural path loss."""
|
|
model = OkumuraHataModel()
|
|
urban = model.calculate(make_input(environment="urban")).path_loss_db
|
|
suburban = model.calculate(make_input(environment="suburban")).path_loss_db
|
|
rural = model.calculate(make_input(environment="rural")).path_loss_db
|
|
assert urban > suburban > rural
|
|
|
|
def test_distance_increases_loss(self):
|
|
model = OkumuraHataModel()
|
|
loss_1 = model.calculate(make_input(distance_m=2000)).path_loss_db
|
|
loss_5 = model.calculate(make_input(distance_m=5000)).path_loss_db
|
|
loss_10 = model.calculate(make_input(distance_m=10000)).path_loss_db
|
|
assert loss_1 < loss_5 < loss_10
|
|
|
|
def test_frequency_increases_loss(self):
|
|
model = OkumuraHataModel()
|
|
loss_450 = model.calculate(make_input(frequency_mhz=450)).path_loss_db
|
|
loss_900 = model.calculate(make_input(frequency_mhz=900)).path_loss_db
|
|
assert loss_900 > loss_450
|
|
|
|
def test_higher_tx_reduces_loss(self):
|
|
model = OkumuraHataModel()
|
|
loss_low = model.calculate(make_input(tx_height_m=10)).path_loss_db
|
|
loss_high = model.calculate(make_input(tx_height_m=50)).path_loss_db
|
|
assert loss_high < loss_low
|
|
|
|
def test_valid_frequency_range(self):
|
|
model = OkumuraHataModel()
|
|
assert model.is_valid_for(make_input(frequency_mhz=150))
|
|
assert model.is_valid_for(make_input(frequency_mhz=1500))
|
|
assert not model.is_valid_for(make_input(frequency_mhz=2000))
|
|
|
|
def test_valid_distance_range(self):
|
|
model = OkumuraHataModel()
|
|
assert model.is_valid_for(make_input(distance_m=500))
|
|
assert model.is_valid_for(make_input(distance_m=20000))
|
|
# Out of range
|
|
assert not model.is_valid_for(make_input(distance_m=50))
|
|
|
|
def test_model_name(self):
|
|
model = OkumuraHataModel()
|
|
assert model.name == "Okumura-Hata"
|
|
|
|
def test_open_environment(self):
|
|
"""Open environment should have even less loss than rural."""
|
|
model = OkumuraHataModel()
|
|
rural = model.calculate(make_input(environment="rural")).path_loss_db
|
|
open_area = model.calculate(make_input(environment="open")).path_loss_db
|
|
assert open_area < rural
|
|
|
|
|
|
if __name__ == "__main__":
|
|
instance = TestOkumuraHata()
|
|
for method_name in [m for m in dir(instance) if m.startswith("test_")]:
|
|
try:
|
|
getattr(instance, method_name)()
|
|
print(f" PASS {method_name}")
|
|
except Exception as e:
|
|
print(f" FAIL {method_name}: {e}")
|
|
print("\nAll tests completed.")
|