2d264a5779
Сверка с методикой ПСР ред. 12.17 (Евдокимов, Лейтес): - ночь: 0.5 → 0.0 — методичка §12.3.5 прямо даёт «ночь = 0 км»: без фонаря в лесу ночью пострадавший не перемещается. Ранее 0.5 ЗАВЫШАЛО радиус ночного поиска. - сумерки/вечер: 0.7 → 0.5 — «за час до заката скорость 50% от обычной». B5 переносил 0.7 из старого фронта вопреки методичке. - get_base_speed: документирована семантика «скорость смещения» (не асфальтовая НормС из §12.3.2) — оператору не нужно вводить асфальтовую скорость, она уже свёрнута; СП = поправка конкретной местности поверх базового среднего леса. Тесты обновлены под методичку, добавлен тест ночи = 0. 201 passed, 0 failed.
357 lines
12 KiB
Python
357 lines
12 KiB
Python
"""
|
||
Tests for distance_service.py
|
||
|
||
Tests the distance calculation formulas and prior probabilities
|
||
based on §9 ВЕКТОР-контекст.md specifications.
|
||
"""
|
||
import pytest
|
||
from services.distance_service import (
|
||
calculate_max_distance,
|
||
get_base_speed,
|
||
get_terrain_coefficient,
|
||
get_time_of_day_coefficient,
|
||
get_weather_coefficient,
|
||
get_distance_priors,
|
||
get_distance_zone,
|
||
get_distance_statistics
|
||
)
|
||
|
||
|
||
class TestCalculateMaxDistance:
|
||
"""Test the main distance calculation function.
|
||
|
||
Ожидания соответствуют каноничным displacement-скоростям B5 (3e2692f):
|
||
get_base_speed 0.3–2.5 км/ч по априорам ПСО «Экстремум» (94% в 3 км).
|
||
Контрольная точка B5: bike 8yo 2h лес = 5.4 км.
|
||
"""
|
||
|
||
def test_boy_10_years_4_hours_forest_rain(self):
|
||
"""
|
||
Мальчик 10 лет, 4 часа, лес, дождь.
|
||
Пересчитано под канон B5: НормС(10)=1.5
|
||
Distance = 4 * 1.5 * 0.5 * 0.8 * 1.0 * 0.8 = 1.92 km
|
||
"""
|
||
case_data = {
|
||
'age': 10,
|
||
'elapsed_hours': 4.0,
|
||
'terrain_primary': 'лес',
|
||
'time_of_day': 'день',
|
||
'weather': 'дождь'
|
||
}
|
||
|
||
distance = calculate_max_distance(case_data)
|
||
|
||
assert distance == pytest.approx(1.92, rel=0.01)
|
||
|
||
def test_young_child_short_time(self):
|
||
"""Test for young child (3 years) with short elapsed time."""
|
||
case_data = {
|
||
'age': 3,
|
||
'elapsed_hours': 1.0,
|
||
'terrain_primary': 'лес',
|
||
'time_of_day': 'день',
|
||
'weather': 'нет'
|
||
}
|
||
|
||
distance = calculate_max_distance(case_data)
|
||
|
||
# 1 * 0.7 * 0.5 * 0.95 * 1.0 * 1.0 = 0.33 km
|
||
assert distance == pytest.approx(0.33, rel=0.01)
|
||
|
||
def test_teenager_long_time_road(self):
|
||
"""Test for teenager on road with longer elapsed time."""
|
||
case_data = {
|
||
'age': 15,
|
||
'elapsed_hours': 6.0,
|
||
'terrain_primary': 'дорога',
|
||
'time_of_day': 'день',
|
||
'weather': 'нет'
|
||
}
|
||
|
||
distance = calculate_max_distance(case_data)
|
||
|
||
# 6 * 2.0 * 0.8 * 0.7 * 1.0 * 1.0 = 6.72 km
|
||
assert distance == pytest.approx(6.72, rel=0.01)
|
||
|
||
def test_elderly_night_swamp(self):
|
||
"""Test for elderly person at night in swamp."""
|
||
case_data = {
|
||
'age': 70,
|
||
'elapsed_hours': 3.0,
|
||
'terrain_primary': 'болото',
|
||
'time_of_day': 'ночь',
|
||
'weather': 'туман'
|
||
}
|
||
|
||
distance = calculate_max_distance(case_data)
|
||
|
||
# 3 * 1.5 * 0.2 * 0.85 * 0.0(ночь) * 0.7 = 0.0 км —
|
||
# методичка §12.3.5: без фонаря ночью движение = 0
|
||
assert distance == pytest.approx(0.0, rel=0.01)
|
||
|
||
def test_adult_heavy_rain_field(self):
|
||
"""Test for adult in heavy rain on field."""
|
||
case_data = {
|
||
'age': 35,
|
||
'elapsed_hours': 2.0,
|
||
'terrain_primary': 'поле',
|
||
'time_of_day': 'день',
|
||
'weather': 'ливень'
|
||
}
|
||
|
||
distance = calculate_max_distance(case_data)
|
||
|
||
# 2 * 2.5 * 0.9 * 0.9 * 1.0 * 0.6 = 2.43 km
|
||
assert distance == pytest.approx(2.43, rel=0.01)
|
||
|
||
def test_bike_8yo_canonical_b5(self):
|
||
"""Контрольная точка B5: bike 8yo 2h лес = 5.4 км (СУ bike ×5)."""
|
||
case_data = {
|
||
'age': 8,
|
||
'elapsed_hours': 2.0,
|
||
'terrain_primary': 'лес',
|
||
'time_of_day': 'день',
|
||
'weather': 'нет',
|
||
'has_transport': 'bike',
|
||
'diagnosis_type': [],
|
||
}
|
||
|
||
distance = calculate_max_distance(case_data)
|
||
|
||
assert distance == pytest.approx(5.4, rel=0.01)
|
||
|
||
|
||
class TestBaseSpeed:
|
||
"""Test НормС (base speed) by age — каноничные displacement-скорости B5."""
|
||
|
||
def test_infant(self):
|
||
assert get_base_speed(1) == 0.3
|
||
assert get_base_speed(2) == 0.3
|
||
|
||
def test_preschool(self):
|
||
assert get_base_speed(3) == 0.7
|
||
assert get_base_speed(5) == 0.7
|
||
|
||
def test_young_child(self):
|
||
assert get_base_speed(6) == 1.2
|
||
assert get_base_speed(8) == 1.2
|
||
|
||
def test_preteen(self):
|
||
assert get_base_speed(10) == 1.5
|
||
assert get_base_speed(12) == 1.5
|
||
|
||
def test_teenager(self):
|
||
assert get_base_speed(13) == 2.0
|
||
assert get_base_speed(15) == 2.0
|
||
assert get_base_speed(16) == 2.5
|
||
assert get_base_speed(17) == 2.5
|
||
|
||
def test_adult(self):
|
||
assert get_base_speed(25) == 2.5
|
||
assert get_base_speed(50) == 2.5
|
||
assert get_base_speed(64) == 2.5
|
||
|
||
def test_elderly(self):
|
||
assert get_base_speed(65) == 1.5
|
||
assert get_base_speed(80) == 1.5
|
||
|
||
|
||
class TestTerrainCoefficient:
|
||
"""Test СП (terrain coefficient)."""
|
||
|
||
def test_road_terrain(self):
|
||
assert get_terrain_coefficient('дорога') == 0.8
|
||
assert get_terrain_coefficient('лесная дорога') == 0.8
|
||
assert get_terrain_coefficient('тропа') == 0.8
|
||
|
||
def test_forest_terrain(self):
|
||
assert get_terrain_coefficient('лес') == 0.5
|
||
assert get_terrain_coefficient('простой лес') == 0.5
|
||
assert get_terrain_coefficient('сложный лес') == 0.25
|
||
assert get_terrain_coefficient('густой лес') == 0.25
|
||
|
||
def test_open_terrain(self):
|
||
assert get_terrain_coefficient('поле') == 0.9
|
||
assert get_terrain_coefficient('луг') == 0.9
|
||
|
||
def test_difficult_terrain(self):
|
||
assert get_terrain_coefficient('болото') == 0.2
|
||
assert get_terrain_coefficient('горы') == 0.3
|
||
assert get_terrain_coefficient('овраг') == 0.3
|
||
|
||
def test_urban_terrain(self):
|
||
assert get_terrain_coefficient('город') == 1.0
|
||
assert get_terrain_coefficient('населённый пункт') == 1.0
|
||
|
||
def test_unknown_terrain(self):
|
||
assert get_terrain_coefficient('неизвестно') == 0.5
|
||
|
||
|
||
class TestTimeOfDayCoefficient:
|
||
"""Test ВВС (time of day coefficient)."""
|
||
|
||
def test_day(self):
|
||
assert get_time_of_day_coefficient('день') == 1.0
|
||
|
||
def test_night(self):
|
||
# Методичка §12.3.5: ночь = 0 (без фонаря движение прекращается)
|
||
assert get_time_of_day_coefficient('ночь') == 0.0
|
||
|
||
def test_twilight(self):
|
||
# Методичка §12.3.3: за час до заката скорость 50% от обычной
|
||
assert get_time_of_day_coefficient('сумерки') == 0.5
|
||
assert get_time_of_day_coefficient('вечер') == 0.5
|
||
|
||
|
||
class TestWeatherCoefficient:
|
||
"""Test ВП (weather coefficient)."""
|
||
|
||
def test_clear_weather(self):
|
||
assert get_weather_coefficient('нет') == 1.0
|
||
assert get_weather_coefficient('ясно') == 1.0
|
||
|
||
def test_rain(self):
|
||
assert get_weather_coefficient('дождь') == 0.8
|
||
assert get_weather_coefficient('ливень') == 0.6
|
||
assert get_weather_coefficient('сильный дождь') == 0.6
|
||
|
||
def test_fog(self):
|
||
assert get_weather_coefficient('туман') == 0.7
|
||
|
||
def test_snow(self):
|
||
assert get_weather_coefficient('снег') == 0.6
|
||
assert get_weather_coefficient('метель') == 0.6
|
||
|
||
def test_heat(self):
|
||
assert get_weather_coefficient('жара') == 0.8
|
||
|
||
|
||
class TestDistancePriors:
|
||
"""Test get_distance_priors() - априорные вероятности зон."""
|
||
|
||
def test_young_child_priors(self):
|
||
"""Children under 8 stay close."""
|
||
priors = get_distance_priors(5)
|
||
|
||
assert priors['0_500m'] == 0.45
|
||
assert priors['500_1500m'] == 0.35
|
||
assert priors['1500_2500m'] == 0.15
|
||
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
||
|
||
def test_preteen_priors(self):
|
||
"""Children 8-12 have more even distribution."""
|
||
priors = get_distance_priors(10)
|
||
|
||
assert priors['0_500m'] == 0.28
|
||
assert priors['500_1500m'] == 0.25
|
||
assert priors['1500_2500m'] == 0.22
|
||
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
||
|
||
def test_teenager_priors(self):
|
||
"""Teenagers can go farther."""
|
||
priors = get_distance_priors(15)
|
||
|
||
assert priors['1500_2500m'] == 0.25
|
||
assert priors['5000_plus'] == 0.08
|
||
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
||
|
||
def test_adult_priors(self):
|
||
"""Adults have highest far-distance probability."""
|
||
priors = get_distance_priors(35)
|
||
|
||
assert priors['0_500m'] == 0.12
|
||
assert priors['5000_plus'] == 0.13
|
||
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
||
|
||
def test_elderly_priors(self):
|
||
"""Elderly stay closer like young children."""
|
||
priors = get_distance_priors(70)
|
||
|
||
assert priors['0_500m'] == 0.35
|
||
assert priors['5000_plus'] == 0.02
|
||
assert sum(priors.values()) == pytest.approx(1.0, rel=0.01)
|
||
|
||
|
||
class TestDistanceZone:
|
||
"""Test get_distance_zone() classification."""
|
||
|
||
def test_zone_classification(self):
|
||
assert get_distance_zone(0.3) == '0_500m'
|
||
assert get_distance_zone(0.5) == '500_1500m'
|
||
assert get_distance_zone(1.0) == '500_1500m'
|
||
assert get_distance_zone(1.5) == '1500_2500m'
|
||
assert get_distance_zone(2.5) == '2500_3500m'
|
||
assert get_distance_zone(4.0) == '3500_5000m'
|
||
assert get_distance_zone(6.0) == '5000_plus'
|
||
|
||
|
||
class TestDistanceStatistics:
|
||
"""Test get_distance_statistics() comprehensive output."""
|
||
|
||
def test_statistics_structure(self):
|
||
stats = get_distance_statistics(
|
||
age_years=10,
|
||
elapsed_hours=4.0,
|
||
terrain='лес'
|
||
)
|
||
|
||
assert 'max_distance_km' in stats
|
||
assert 'current_zone' in stats
|
||
assert 'zone_probability' in stats
|
||
assert 'all_priors' in stats
|
||
assert 'base_speed_kmh' in stats
|
||
assert 'terrain_coefficient' in stats
|
||
|
||
def test_statistics_values(self):
|
||
stats = get_distance_statistics(
|
||
age_years=10,
|
||
elapsed_hours=4.0,
|
||
terrain='лес'
|
||
)
|
||
|
||
assert stats['base_speed_kmh'] == 1.5
|
||
assert stats['terrain_coefficient'] == 0.5
|
||
assert stats['max_distance_km'] > 0
|
||
assert 0 <= stats['zone_probability'] <= 1.0
|
||
|
||
|
||
class TestFatigueCoefficient:
|
||
"""Test СУТ (fatigue coefficient) behavior."""
|
||
|
||
def test_fatigue_progression(self):
|
||
"""Fatigue increases with time (5% per hour)."""
|
||
case_1h = {
|
||
'age': 30,
|
||
'elapsed_hours': 1.0,
|
||
'terrain_primary': 'поле',
|
||
'time_of_day': 'день',
|
||
'weather': 'нет'
|
||
}
|
||
dist_1h = calculate_max_distance(case_1h)
|
||
|
||
case_5h = case_1h.copy()
|
||
case_5h['elapsed_hours'] = 5.0
|
||
dist_5h = calculate_max_distance(case_5h)
|
||
|
||
case_10h = case_1h.copy()
|
||
case_10h['elapsed_hours'] = 10.0
|
||
dist_10h = calculate_max_distance(case_10h)
|
||
|
||
assert dist_5h < dist_1h * 5
|
||
assert dist_10h < dist_5h * 2
|
||
|
||
def test_fatigue_minimum(self):
|
||
"""Fatigue coefficient has minimum of 0.3."""
|
||
case_data = {
|
||
'age': 30,
|
||
'elapsed_hours': 20.0,
|
||
'terrain_primary': 'поле',
|
||
'time_of_day': 'день',
|
||
'weather': 'нет'
|
||
}
|
||
|
||
distance = calculate_max_distance(case_data)
|
||
|
||
# 20 * 2.5 * 0.9 * 0.3 * 1.0 * 1.0 = 13.5
|
||
assert distance == pytest.approx(13.5, rel=0.01)
|