Files
vector/backend/tests/test_distance_service.py
Sadmin 2aa45e8932 B1: СП(лес)=1.0 — НормС трактуется как скорость смещения по среднему лесу
Решение Виктора: СП(лес) 0.5→1.0. Прежняя таблица учитывала лес дважды
(в НормС и в СП) — подросток 14 лет за 3 часа получал 2.55 км вместо
реалистичных 5.1. Согласуется с априором ПСО (94% находок в 3 км):
радиусы стали верхне-перцентилями, не медианой.

- terrain_map пересчитан относительно среднего леса: лес/простой лес/поле/
  луг/город = 1.0; сложный/густой лес 0.5; болото 0.4; горы/овраг 0.6;
  дорога/тропа 0.8; дефолт 1.0.
- Контрольные точки тестов пересчитаны (6 шт: 1.92→3.84, 0.33→0.665,
  2.43→2.7, 5.4→10.8, 13.5→15.0, stats 0.5→1.0) + операционный тест
  подростка 5.1 км.
- settlement_score пока НЕ тронут — на обдумывание (Виктор).

193 passed.
2026-09-09 19:51:05 +03:00

358 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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 км).
Контрольная точка B1 2026-09-09: bike 8yo 2h лес = 10.8 км (СП(лес)=1.0).
"""
def test_boy_10_years_4_hours_forest_rain(self):
"""
Мальчик 10 лет, 4 часа, лес, дождь.
Пересчитано под канон B5: НормС(10)=1.5
B1 2026-09-09: СП(лес)=1.0 (было 0.5): 4 * 1.5 * 1.0 * 0.8 * 1.0 * 0.8 = 3.84 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(3.84, 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)
# B1 2026-09-09 СП(лес)=1.0: 1 * 0.7 * 1.0 * 0.95 * 1.0 * 1.0 = 0.665 km
assert distance == pytest.approx(0.665, 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)
# B1 2026-09-09 СП(поле)=1.0: 2 * 2.5 * 1.0 * 0.9 * 1.0 * 0.6 = 2.7 km
assert distance == pytest.approx(2.7, rel=0.01)
def test_bike_8yo_canonical_b5(self):
"""Контрольная точка B1 2026-09-09: bike 8yo 2h лес = 10.8 км (СП(лес)=1.0, СУ 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(10.8, 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). Семантика: СП=1.0 — средний лес
(НормС — скорость смещения по среднему лесу; B1-ревью 2026-09-09)."""
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('лес') == 1.0
assert get_terrain_coefficient('простой лес') == 1.0
assert get_terrain_coefficient('сложный лес') == 0.5
assert get_terrain_coefficient('густой лес') == 0.5
def test_open_terrain(self):
assert get_terrain_coefficient('поле') == 1.0
assert get_terrain_coefficient('луг') == 1.0
def test_difficult_terrain(self):
assert get_terrain_coefficient('болото') == 0.4
assert get_terrain_coefficient('горы') == 0.6
assert get_terrain_coefficient('овраг') == 0.6
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('неизвестно') == 1.0
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'] == 1.0
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)
# B1 2026-09-09 СП(поле)=1.0: 20 * 2.5 * 1.0 * 0.3 * 1.0 * 1.0 = 15.0
assert distance == pytest.approx(15.0, rel=0.01)