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)
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
"""
|
|
Detailed unit tests for the Free Space Path Loss model.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import math
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
|
|
from app.propagation.base import PropagationInput
|
|
from app.propagation.free_space import FreeSpaceModel
|
|
|
|
|
|
def make_input(**kwargs) -> PropagationInput:
|
|
defaults = {
|
|
"frequency_mhz": 1800,
|
|
"distance_m": 1000,
|
|
"tx_height_m": 30,
|
|
"rx_height_m": 1.5,
|
|
"environment": "urban",
|
|
}
|
|
defaults.update(kwargs)
|
|
return PropagationInput(**defaults)
|
|
|
|
|
|
class TestFreeSpaceModel:
|
|
def test_formula_accuracy(self):
|
|
"""FSPL = 20*log10(d_km) + 20*log10(f_MHz) + 32.45"""
|
|
model = FreeSpaceModel()
|
|
# At 1km, 1000MHz: 20*0 + 20*60 + 32.45 = 92.45 dB
|
|
out = model.calculate(make_input(distance_m=1000, frequency_mhz=1000))
|
|
expected = 32.45 + 20 * math.log10(1.0) + 20 * math.log10(1000)
|
|
assert abs(out.path_loss_db - expected) < 0.1
|
|
|
|
def test_6db_per_distance_doubling(self):
|
|
model = FreeSpaceModel()
|
|
loss_1 = model.calculate(make_input(distance_m=1000)).path_loss_db
|
|
loss_2 = model.calculate(make_input(distance_m=2000)).path_loss_db
|
|
assert abs((loss_2 - loss_1) - 6.02) < 0.1
|
|
|
|
def test_6db_per_frequency_doubling(self):
|
|
model = FreeSpaceModel()
|
|
loss_1 = model.calculate(make_input(frequency_mhz=900)).path_loss_db
|
|
loss_2 = model.calculate(make_input(frequency_mhz=1800)).path_loss_db
|
|
assert abs((loss_2 - loss_1) - 6.02) < 0.1
|
|
|
|
def test_always_los(self):
|
|
model = FreeSpaceModel()
|
|
out = model.calculate(make_input())
|
|
assert out.is_los is True
|
|
|
|
def test_model_name(self):
|
|
model = FreeSpaceModel()
|
|
assert model.name == "Free-Space"
|
|
|
|
def test_wide_frequency_range(self):
|
|
model = FreeSpaceModel()
|
|
assert model.is_valid_for(make_input(frequency_mhz=1))
|
|
assert model.is_valid_for(make_input(frequency_mhz=100000))
|
|
|
|
def test_very_short_distance(self):
|
|
model = FreeSpaceModel()
|
|
out = model.calculate(make_input(distance_m=10))
|
|
assert out.path_loss_db > 0
|
|
assert out.path_loss_db < 80
|
|
|
|
def test_very_long_distance(self):
|
|
model = FreeSpaceModel()
|
|
out = model.calculate(make_input(distance_m=100000))
|
|
assert out.path_loss_db > 120
|
|
|
|
|
|
if __name__ == "__main__":
|
|
instance = TestFreeSpaceModel()
|
|
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.")
|