Перейти к содержанию

Внутренняя схема: сценарий на входе, план на выходе

Эти две схемы — первое, что нужно зафиксировать, раньше любого кода продукта. Причина в 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 — обязательное поле. Если галс не назначен, надо знать почему: нет совместимой нагрузки, не влезает в запас хода даже в одиночку, лежит вне разрешённого ВП. Молчаливое выбрасывание галсов — худшее, что может сделать планировщик, и именно это будут проверять на экспертизе.

warningsviolations. Высота 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 %.