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.
This commit is contained in:
@@ -22,14 +22,14 @@ class TestCalculateMaxDistance:
|
|||||||
|
|
||||||
Ожидания соответствуют каноничным displacement-скоростям B5 (3e2692f):
|
Ожидания соответствуют каноничным displacement-скоростям B5 (3e2692f):
|
||||||
get_base_speed 0.3–2.5 км/ч по априорам ПСО «Экстремум» (94% в 3 км).
|
get_base_speed 0.3–2.5 км/ч по априорам ПСО «Экстремум» (94% в 3 км).
|
||||||
Контрольная точка B5: bike 8yo 2h лес = 5.4 км.
|
Контрольная точка B1 2026-09-09: bike 8yo 2h лес = 10.8 км (СП(лес)=1.0).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_boy_10_years_4_hours_forest_rain(self):
|
def test_boy_10_years_4_hours_forest_rain(self):
|
||||||
"""
|
"""
|
||||||
Мальчик 10 лет, 4 часа, лес, дождь.
|
Мальчик 10 лет, 4 часа, лес, дождь.
|
||||||
Пересчитано под канон B5: НормС(10)=1.5
|
Пересчитано под канон B5: НормС(10)=1.5
|
||||||
Distance = 4 * 1.5 * 0.5 * 0.8 * 1.0 * 0.8 = 1.92 km
|
B1 2026-09-09: СП(лес)=1.0 (было 0.5): 4 * 1.5 * 1.0 * 0.8 * 1.0 * 0.8 = 3.84 km
|
||||||
"""
|
"""
|
||||||
case_data = {
|
case_data = {
|
||||||
'age': 10,
|
'age': 10,
|
||||||
@@ -41,7 +41,7 @@ class TestCalculateMaxDistance:
|
|||||||
|
|
||||||
distance = calculate_max_distance(case_data)
|
distance = calculate_max_distance(case_data)
|
||||||
|
|
||||||
assert distance == pytest.approx(1.92, rel=0.01)
|
assert distance == pytest.approx(3.84, rel=0.01)
|
||||||
|
|
||||||
def test_young_child_short_time(self):
|
def test_young_child_short_time(self):
|
||||||
"""Test for young child (3 years) with short elapsed time."""
|
"""Test for young child (3 years) with short elapsed time."""
|
||||||
@@ -55,8 +55,8 @@ class TestCalculateMaxDistance:
|
|||||||
|
|
||||||
distance = calculate_max_distance(case_data)
|
distance = calculate_max_distance(case_data)
|
||||||
|
|
||||||
# 1 * 0.7 * 0.5 * 0.95 * 1.0 * 1.0 = 0.33 km
|
# 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.33, rel=0.01)
|
assert distance == pytest.approx(0.665, rel=0.01)
|
||||||
|
|
||||||
def test_teenager_long_time_road(self):
|
def test_teenager_long_time_road(self):
|
||||||
"""Test for teenager on road with longer elapsed time."""
|
"""Test for teenager on road with longer elapsed time."""
|
||||||
@@ -101,11 +101,11 @@ class TestCalculateMaxDistance:
|
|||||||
|
|
||||||
distance = calculate_max_distance(case_data)
|
distance = calculate_max_distance(case_data)
|
||||||
|
|
||||||
# 2 * 2.5 * 0.9 * 0.9 * 1.0 * 0.6 = 2.43 km
|
# 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.43, rel=0.01)
|
assert distance == pytest.approx(2.7, rel=0.01)
|
||||||
|
|
||||||
def test_bike_8yo_canonical_b5(self):
|
def test_bike_8yo_canonical_b5(self):
|
||||||
"""Контрольная точка B5: bike 8yo 2h лес = 5.4 км (СУ bike ×5)."""
|
"""Контрольная точка B1 2026-09-09: bike 8yo 2h лес = 10.8 км (СП(лес)=1.0, СУ bike ×5)."""
|
||||||
case_data = {
|
case_data = {
|
||||||
'age': 8,
|
'age': 8,
|
||||||
'elapsed_hours': 2.0,
|
'elapsed_hours': 2.0,
|
||||||
@@ -118,7 +118,7 @@ class TestCalculateMaxDistance:
|
|||||||
|
|
||||||
distance = calculate_max_distance(case_data)
|
distance = calculate_max_distance(case_data)
|
||||||
|
|
||||||
assert distance == pytest.approx(5.4, rel=0.01)
|
assert distance == pytest.approx(10.8, rel=0.01)
|
||||||
|
|
||||||
|
|
||||||
class TestBaseSpeed:
|
class TestBaseSpeed:
|
||||||
@@ -157,7 +157,8 @@ class TestBaseSpeed:
|
|||||||
|
|
||||||
|
|
||||||
class TestTerrainCoefficient:
|
class TestTerrainCoefficient:
|
||||||
"""Test СП (terrain coefficient)."""
|
"""Test СП (terrain coefficient). Семантика: СП=1.0 — средний лес
|
||||||
|
(НормС — скорость смещения по среднему лесу; B1-ревью 2026-09-09)."""
|
||||||
|
|
||||||
def test_road_terrain(self):
|
def test_road_terrain(self):
|
||||||
assert get_terrain_coefficient('дорога') == 0.8
|
assert get_terrain_coefficient('дорога') == 0.8
|
||||||
@@ -165,26 +166,26 @@ class TestTerrainCoefficient:
|
|||||||
assert get_terrain_coefficient('тропа') == 0.8
|
assert get_terrain_coefficient('тропа') == 0.8
|
||||||
|
|
||||||
def test_forest_terrain(self):
|
def test_forest_terrain(self):
|
||||||
assert get_terrain_coefficient('лес') == 0.5
|
assert get_terrain_coefficient('лес') == 1.0
|
||||||
assert get_terrain_coefficient('простой лес') == 0.5
|
assert get_terrain_coefficient('простой лес') == 1.0
|
||||||
assert get_terrain_coefficient('сложный лес') == 0.25
|
assert get_terrain_coefficient('сложный лес') == 0.5
|
||||||
assert get_terrain_coefficient('густой лес') == 0.25
|
assert get_terrain_coefficient('густой лес') == 0.5
|
||||||
|
|
||||||
def test_open_terrain(self):
|
def test_open_terrain(self):
|
||||||
assert get_terrain_coefficient('поле') == 0.9
|
assert get_terrain_coefficient('поле') == 1.0
|
||||||
assert get_terrain_coefficient('луг') == 0.9
|
assert get_terrain_coefficient('луг') == 1.0
|
||||||
|
|
||||||
def test_difficult_terrain(self):
|
def test_difficult_terrain(self):
|
||||||
assert get_terrain_coefficient('болото') == 0.2
|
assert get_terrain_coefficient('болото') == 0.4
|
||||||
assert get_terrain_coefficient('горы') == 0.3
|
assert get_terrain_coefficient('горы') == 0.6
|
||||||
assert get_terrain_coefficient('овраг') == 0.3
|
assert get_terrain_coefficient('овраг') == 0.6
|
||||||
|
|
||||||
def test_urban_terrain(self):
|
def test_urban_terrain(self):
|
||||||
assert get_terrain_coefficient('город') == 1.0
|
assert get_terrain_coefficient('город') == 1.0
|
||||||
assert get_terrain_coefficient('населённый пункт') == 1.0
|
assert get_terrain_coefficient('населённый пункт') == 1.0
|
||||||
|
|
||||||
def test_unknown_terrain(self):
|
def test_unknown_terrain(self):
|
||||||
assert get_terrain_coefficient('неизвестно') == 0.5
|
assert get_terrain_coefficient('неизвестно') == 1.0
|
||||||
|
|
||||||
|
|
||||||
class TestTimeOfDayCoefficient:
|
class TestTimeOfDayCoefficient:
|
||||||
@@ -310,7 +311,7 @@ class TestDistanceStatistics:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert stats['base_speed_kmh'] == 1.5
|
assert stats['base_speed_kmh'] == 1.5
|
||||||
assert stats['terrain_coefficient'] == 0.5
|
assert stats['terrain_coefficient'] == 1.0
|
||||||
assert stats['max_distance_km'] > 0
|
assert stats['max_distance_km'] > 0
|
||||||
assert 0 <= stats['zone_probability'] <= 1.0
|
assert 0 <= stats['zone_probability'] <= 1.0
|
||||||
|
|
||||||
@@ -352,5 +353,5 @@ class TestFatigueCoefficient:
|
|||||||
|
|
||||||
distance = calculate_max_distance(case_data)
|
distance = calculate_max_distance(case_data)
|
||||||
|
|
||||||
# 20 * 2.5 * 0.9 * 0.3 * 1.0 * 1.0 = 13.5
|
# B1 2026-09-09 СП(поле)=1.0: 20 * 2.5 * 1.0 * 0.3 * 1.0 * 1.0 = 15.0
|
||||||
assert distance == pytest.approx(13.5, rel=0.01)
|
assert distance == pytest.approx(15.0, rel=0.01)
|
||||||
|
|||||||
@@ -53,15 +53,27 @@ def _base_input(**overrides: Any) -> SearchInput:
|
|||||||
|
|
||||||
|
|
||||||
async def test_build_search_model_bike_8yo_matches_old_contract():
|
async def test_build_search_model_bike_8yo_matches_old_contract():
|
||||||
"""Регресс из B14: bike 8yo 2h лес день → max_distance 5.4 км
|
"""bike 8yo 2h лес день → max_distance.
|
||||||
(Time 2 × НормС 1.2 × СП 0.5 × СУТ 0.9 × СУ-bike 5.0)."""
|
B1-ревью 2026-09-09 (Виктор): СП(лес) 0.5→1.0 (НормС — уже скорость
|
||||||
|
смещения по среднему лесу; 0.5 учитывал лес дважды).
|
||||||
|
Было 5.4 (2 × 1.2 × 0.5 × 0.9 × 5.0), стало 10.8 (2 × 1.2 × 1.0 × 0.9 × 5.0)."""
|
||||||
model = await build_search_model(_base_input(), analyzer=_fake_analyzer())
|
model = await build_search_model(_base_input(), analyzer=_fake_analyzer())
|
||||||
assert model.max_distance_km == pytest.approx(5.4, abs=1e-6)
|
assert model.max_distance_km == pytest.approx(10.8, abs=1e-6)
|
||||||
assert model.coefficients['base_speed'] == 1.2
|
assert model.coefficients['base_speed'] == 1.2
|
||||||
assert model.coefficients['terrain'] == 0.5
|
assert model.coefficients['terrain'] == 1.0
|
||||||
assert model.coefficients['urgency'] == 5.0 # bike
|
assert model.coefficients['urgency'] == 5.0 # bike
|
||||||
|
|
||||||
|
|
||||||
|
async def test_teen_3h_forest_realistic_distance():
|
||||||
|
"""Операционная проверка Виктора: здоровый подросток за 3 часа проходит
|
||||||
|
заметно больше 2.5 км. При СП(лес)=1.0: 3 × 2.0 × 1.0 × 0.85 = 5.1 км."""
|
||||||
|
model = await build_search_model(
|
||||||
|
_base_input(age=14, has_transport=None, elapsed_hours=3.0),
|
||||||
|
analyzer=_fake_analyzer(),
|
||||||
|
)
|
||||||
|
assert model.max_distance_km == pytest.approx(5.1, abs=1e-6)
|
||||||
|
|
||||||
|
|
||||||
async def test_profiles_derived_from_diagnosis_and_transport():
|
async def test_profiles_derived_from_diagnosis_and_transport():
|
||||||
analyzer = _fake_analyzer()
|
analyzer = _fake_analyzer()
|
||||||
model = await build_search_model(
|
model = await build_search_model(
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ def get_terrain_coefficient(terrain: str) -> float:
|
|||||||
"""
|
"""
|
||||||
Get terrain movement coefficient (СП).
|
Get terrain movement coefficient (СП).
|
||||||
|
|
||||||
|
Семантика (согласовано с Виктором, B1-ревью 2026-09-09): НормС — скорость
|
||||||
|
СМЕЩЕНИЯ потерявшегося по СРЕДНЕМУ лесу, поэтому СП=1.0 — средний лес.
|
||||||
|
Отклонения от среднего леса режут/поднимают скорость относительно неё.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
terrain: Terrain type
|
terrain: Terrain type
|
||||||
|
|
||||||
@@ -133,32 +137,32 @@ def get_terrain_coefficient(terrain: str) -> float:
|
|||||||
Terrain coefficient (0.0 - 1.0)
|
Terrain coefficient (0.0 - 1.0)
|
||||||
"""
|
"""
|
||||||
if not terrain:
|
if not terrain:
|
||||||
return 0.5
|
return 1.0
|
||||||
terrain_lower = terrain.lower()
|
terrain_lower = terrain.lower()
|
||||||
|
|
||||||
terrain_map = {
|
terrain_map = {
|
||||||
'лесная дорога': 0.8,
|
'лесная дорога': 0.8,
|
||||||
'сложный лес': 0.25,
|
'сложный лес': 0.5,
|
||||||
'густой лес': 0.25,
|
'густой лес': 0.5,
|
||||||
'простой лес': 0.5,
|
'простой лес': 1.0,
|
||||||
'лес': 0.5,
|
'лес': 1.0,
|
||||||
'дорога': 0.8,
|
'дорога': 0.8,
|
||||||
'тропа': 0.8,
|
'тропа': 0.8,
|
||||||
'болото': 0.2,
|
'болото': 0.4,
|
||||||
'поле': 0.9,
|
'поле': 1.0,
|
||||||
'луг': 0.9,
|
'луг': 1.0,
|
||||||
'город': 1.0,
|
'город': 1.0,
|
||||||
'населённый пункт': 1.0,
|
'населённый пункт': 1.0,
|
||||||
'горы': 0.3,
|
'горы': 0.6,
|
||||||
'овраг': 0.3
|
'овраг': 0.6
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value in terrain_map.items():
|
for key, value in terrain_map.items():
|
||||||
if key in terrain_lower:
|
if key in terrain_lower:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# Default for unknown terrain
|
# Default for unknown terrain: средний лес (как 'лес')
|
||||||
return 0.5
|
return 1.0
|
||||||
|
|
||||||
|
|
||||||
def get_diagnosis_coefficient(diagnosis_types) -> float:
|
def get_diagnosis_coefficient(diagnosis_types) -> float:
|
||||||
|
|||||||
Reference in New Issue
Block a user