Внутренняя схема: сценарий на входе, план на выходе¶
Эти две схемы — первое, что нужно зафиксировать, раньше любого кода продукта. Причина в 70-plan/01: пока нет формата входа и выхода, нечего валидировать, а без валидатора разработка превращается в поток правдоподобного кода.
Схемы здесь — черновик. Уточняются по факту, но структура меняться не должна.
Вход: сценарий¶
Один JSON (или YAML) описывает задачу целиком — это и вход API, и формат тестовых сценариев.
{
"schema_version": 1,
"scenario_id": "s03-mixed-payloads",
"name": "Смешанные типы съёмки, два ВПП",
"crs": "EPSG:4326", // вход всегда WGS84; проекция для расчётов выбирается внутри
"launch_sites": [
{
"id": "vpp-1",
"position": [37.6173, 55.7558], // [долгота, широта]
"elevation_m": 145,
"suitable_for": ["multirotor", "fixed_wing"],
"takeoff_heading_deg": 270, // сектор взлёта для катапульты (±30°)
"turnaround_time_min": 10,
"spare_batteries": { "geoscan_gemini": 2 }
}
],
"landing_sites": [
{ "id": "alt-1", "position": [37.65, 55.77], "elevation_m": 150, "reserve": true }
],
"fleet": [
{ "id": "gemini-01", "model": "geoscan_gemini", "payload": "geoscan_pf1b",
"home": "vpp-1", "available_from": "2026-09-20T06:00:00Z" },
{ "id": "801-01", "model": "geoscan_801", "payload": "geoscan_801_thermal",
"home": "vpp-1" }
],
"tasks": [
{
"id": "t1",
"survey_type": "rgb",
"quality": { "gsd_cm": 3.0 }, // для lidar — point_density, для геофизики — line_spacing_m
"area": { "type": "Polygon", "coordinates": [[[...]]] },
"time_window": null
},
{
"id": "t2",
"survey_type": "ir",
"quality": { "gsd_cm": 15.0 },
"area": { "type": "Polygon", "coordinates": [[[...]]] },
"time_window": { "from": "2026-09-20T03:00:00Z", "to": "2026-09-20T06:00:00Z" }
}
],
"airspace": {
"allowed": { "type": "Polygon", "coordinates": [[[...]]] }, // inclusion, R-IN-5
"no_fly": [ // exclusion, R-IN-6
{ "geometry": { "type": "Polygon", "coordinates": [[[...]]] },
"altitude_min_m": 0, "altitude_max_m": 500,
"zone_kind": "no_fly",
"valid_from": null, "valid_to": null }
]
},
"weather": {
"wind_speed_ms": 6.0,
"wind_direction_deg": 270, // откуда дует, метеорологическая конвенция
"temperature_c": 12
},
"objective": {
"criterion": "makespan", // makespan | total_flight_time | pareto
"lambda": null, // для pareto: вес makespan, 0…1
"energy_reserve": 0.20,
"turn_mode": "lzp", // lzp (с выходом на ЛЗП) | flyby (пролётом)
"safety_buffer_m": 25 // отступ от границ запретных зон
},
"solver": { "time_limit_s": 30, "seed": 42 }
}
Три решения, зафиксированные в схеме¶
Конвенция направления ветра. wind_direction_deg — откуда дует (метеорологическая
конвенция: 270° = западный ветер, дует с запада на восток). Это стандарт в метеосводках, но
противоположно математическому вектору. Одна строка в схеме, а путаница из-за неё стоит дня отладки.
quality — объект, а не число. Для RGB/мультиспектра/ИК это gsd_cm, для LiDAR —
point_density_per_m2, для геофизики — line_spacing_m. Разные типы съёмки задают качество
принципиально разными величинами (30-domain/02 (../30-domain/02-survey-types.md)); плоское поле
gsd заставит потом ломать схему.
seed в solver. Метаэвристики стохастичны. Без фиксированного seed нельзя воспроизвести
результат, а значит нельзя сравнить две версии алгоритма и нельзя доверять метрикам. Для режима
«агенты пишут, я проверяю» это не роскошь, а условие работоспособности всей схемы.
Выход: план¶
{
"schema_version": 1,
"scenario_id": "s03-mixed-payloads",
"generated_at": "2026-09-20T09:00:00Z",
"generator": "geoscan-planner-service v0.1",
"solver": { "criterion": "makespan", "time_limit_s": 30, "seed": 42,
"solve_time_s": 12.4, "status": "feasible" },
"metrics": {
"makespan_s": 5280,
"total_flight_time_s": 12960,
"coverage_fraction": 0.998,
"transects_total": 87,
"turns_total": 74,
"turn_time_fraction": 0.21,
"photos_total": 3241,
"uavs_used": 3,
"sorties_total": 5,
"violations": { "nfz": 0, "airspace": 0, "endurance": 0, "wind": 0, "payload": 0 }
},
"missions": [
{
"uav_id": "gemini-01",
"uav_model": "geoscan_gemini",
"payload": "geoscan_pf1b",
"sorties": [
{
"seq": 1,
"launch_site": "vpp-1",
"landing_site": "vpp-1",
"start_time": "2026-09-20T06:00:00Z",
"duration_s": 2145,
"energy_used_frac": 0.78,
"altitude_frame": "AGL",
"phases": [
{ "seq": 0, "phase": "takeoff", "duration_s": 30,
"geometry": { "type": "Point", "coordinates": [37.6173, 55.7558, 0] } },
{ "seq": 1, "phase": "transit", "duration_s": 62, "altitude_m": 150, "speed_ms": 15,
"geometry": { "type": "LineString", "coordinates": [[...], [...]] } },
{ "seq": 2, "phase": "survey", "duration_s": 147, "altitude_m": 153, "speed_ms": 10,
"task_id": "t1", "transect_id": 7,
"camera_trigger_interval_s": 3.5, "photos": 42,
"geometry": { "type": "LineString", "coordinates": [[...], [...]] } },
{ "seq": 3, "phase": "turn", "duration_s": 12, "turn_mode": "lzp",
"geometry": { "type": "LineString", "coordinates": [[...]] } }
]
}
]
}
],
"unassigned": [], // галсы, которые не удалось назначить, с причиной
"warnings": [
{ "code": "ALT_ABOVE_150M", "task_id": "t1", "altitude_m": 153,
"message": "Расчётная высота превышает 150 м — потребуется разрешение на ИВП" }
]
}
Что здесь важно¶
metrics — контракт с валидатором. Именно эти поля сравниваются между прогонами и попадают в
таблицу на защите. Они считаются независимым скриптом по геометрии плана, а не берутся из
решателя: решатель может ошибаться, валидатор проверяет.
violations — все нули или план невалиден. Ненулевое значение это не «предупреждение», а
провал: план, заходящий в бесполётную зону, к выполнению не пригоден.
unassigned — обязательное поле. Если галс не назначен, надо знать почему: нет совместимой
нагрузки, не влезает в запас хода даже в одиночку, лежит вне разрешённого ВП. Молчаливое выбрасывание
галсов — худшее, что может сделать планировщик, и именно это будут проверять на экспертизе.
warnings ≠ violations. Высота 153 м не нарушение, а повод оформить разрешение на ИВП
(30-domain/06 (../30-domain/06-airspace-ru.md)). Разделение этих двух категорий само по себе
показывает зрелость модели.
turn как отдельная фаза. Разворот занимает время и энергию
(30-domain/03), значит он должен быть виден в плане, а
не растворяться в галсах. Заодно turn_time_fraction в метриках сразу показывает, насколько наивная
оценка «длина/скорость» врёт.
Соответствие форматам выгрузки¶
| Наша схема | GeoJSON | KML | .plan |
|---|---|---|---|
mission |
FeatureCollection на борт | Document на борт | файл на борт |
sortie |
— (в properties) | Folder | mission |
phase |
Feature | Placemark | SimpleItem / ComplexItem |
airspace.allowed |
Feature | Polygon | geoFence inclusion |
airspace.no_fly[] |
Feature | Polygon | geoFence exclusion |
landing_sites (reserve) |
Feature Point | Placemark | rallyPoints |
metrics |
properties FC | <description> |
— |
Конвертеры пишутся по одному на формат и покрываются round-trip-тестами
(40-formats/01 (01-kml-geojson.md), раздел «приёмочный тест»).
Проекция координат¶
Вход и выход — WGS84 (EPSG:4326). Но считать геометрию в градусах нельзя: длины и площади исказятся, а шаг галсов в метрах вообще не выразится.
Порядок: на входе определить подходящую метрическую проекцию по центроиду области (UTM-зона или
локальная азимутальная равнопромежуточная), все расчёты вести в метрах, на выходе конвертировать
обратно. Библиотека — pyproj.
Это классическая тихая ошибка: неверная проекция даёт сдвиг и искажение масштаба, которые на глаз не видны, но рушат весь расчёт покрытия. Отдельный тест: площадь полигона, посчитанная в проекции, должна совпадать с геодезической площадью с точностью лучше 0,1 %.