Оглавление

Введение

Представьте: летательный аппарат следует по заданному маршруту на постоянной высоте. Курс выдержан, скорость стабильна. Но впереди — следующая точка маршрута, и она в стороне от текущего направления. Нужно повернуть.

Казалось бы, что тут сложного? Повернул — и летишь дальше. Но у летательного аппарата фиксированного типа есть одно жёсткое ограничение: минимальный радиус разворота. Он не может крутануться на месте. Любой манёвр — это дуга с конкретным радиусом, продиктованным физикой: скоростью, аэродинамикой, конструкцией.

Отсюда возникает задача, которую система управления должна решить заранее: как именно проложить траекторию разворота? Где заканчивается прямолинейный полёт и начинается дуга? Где дуга переходит обратно в прямую, ведущую к цели? Какова длина этой дуги — чтобы автопилот знал, сколько лететь по ней?

Именно эту задачу мы и разберём. Для её решения не понадобится ничего сверхъестественного — только геометрия 9–11 класса: касательная к окружности, теорема Пифагора, подобие треугольников. Весь необходимый аппарат вы уже проходили — просто, возможно, не думали, что он управляет реальными летательными аппаратами.

И вот что интересно: задача достаточно простая, чтобы школьник старших классов не только разобрался в математике, но и самостоятельно построил модель в среде динамического моделирования. Именно это мы и сделаем в конце статьи — разберём реализацию в Engee, с которой вполне справится любой, кто знаком с основами программирования.

В статье мы пройдём путь от постановки задачи через математику — к реализации модели и выбору оптимальной траектории манёвра.

Немного истории: задача Дубинса

В 1957 году американский математик Лестер Дубинс (Lester Eli Dubins) опубликовал работу «О кривых минимальной длины при ограничении на кривизну и заданных граничных условиях на положение и направление».

Рисунок 1 - Лестер Дубинс
Рисунок 1 - Лестер Дубинс

Звучит сухо, но суть проста: Какой кратчайший путь существует между двумя точками, если у объекта есть фиксированный минимальный радиус поворота и заданное направление движения в начале и конце пути?

Дубинс доказал, что оптимальный путь всегда состоит не более чем из трёх сегментов, каждый из которых — либо дуга окружности, либо прямая. Отсюда появилась классификация путей Дубинса:

Тип

Расшифровка

RSR

Правый поворот → Прямая → Правый поворот

LSL

Левый поворот → Прямая → Левый поворот

RSL

Правый поворот → Прямая → Левый поворот

LSR

Левый поворот → Прямая → Правый поворот

RLR

Правый поворот → Левый поворот → Правый поворот

LRL

Левый поворот → Правый поворот → Левый поворот

(R — right, L — left, S — straight)

Система управления перебирает все применимые варианты и выбирает кратчайший. Именно поэтому алгоритм Дубинса до сих пор живёт в системах управления летательными аппаратами, в планировщиках маршрутов автономных автомобилей и в морской навигации.

Наша задача — упрощённый частный случай: один разворот, одна дуга, известный радиус. Но геометрия та же самая. И именно с неё удобно начинать, если вы впервые сталкиваетесь с темой планирования манёвров.

Постановка задачи

Итак, у нас есть:

  • Точка A — откуда летим;

  • Точка B — точка начала разворота;

  • Точка C — куда нужно попасть после разворота;

  • Радиус разворота R — задан физическими характеристиками летательного аппарата: скоростью, аэродинамикой, допустимой перегрузкой;

  • Линейная скорость V — постоянная на всей траектории.

Нужно найти:

  • Точку D — где дуга заканчивается и начинается прямолинейный полёт к C;

  • Длину дуги BD — чтобы система управления знала, сколько лететь по дуге;

  • Время манёвра от точки B до точки C — складывается из времен прохождения дуги BD и прямолинейного участка DC.  

Траектория выглядит так: A → B → (дуга) → D → C (рис.2).

Рисунок 2 - Траектория манёвра летательного аппарата
Рисунок 2 - Траектория манёвра летательного аппарата

Обратите внимание на ключевое геометрическое свойство: прямая AB касается окружности разворота в точке B, а прямая DC касается той же окружности в точке D. Касательность обеспечивает плавный вход и выход из разворота без резкого излома курса — аппарат плавно переходит с прямой на дугу и обратно.

Всё остальное — следствие этого факта и школьной геометрии.

Математика: как это решается?

Шаг первый: Находим центр окружности O

Раз AB — касательная, а OB — радиус, то по свойству касательной: OB ⊥ AB. Значит, центр O лежит на перпендикуляре к AB, проведённом через точку B.

Коэффициенты нормали к прямой AB:

A_{AB}=y_{b}-y_{a};B_{AB}=x_a-x_{b}

Масштабный коэффициент:

t=R/√(A^2_{AB}+B^2_{AB})

Два кандидата на центр (рис.3):

O_1=(x_{b}+A_{AB}t;y_{b}+B_{AB}t)O_2=(x_{b}-A_{AB}t;y_{b}-B_{AB}t)
Рисунок 3 - Варианты точки О
Рисунок 3 - Варианты точки О

Выбираем O с меньшим расстоянием до C — он соответствует развороту в нужную сторону.

Шаг 2. Находим точку касания D

Из точки C проводим две касательные к окружности. Точки касания D₁ и D₂ симметричны относительно OC, а отрезок D₁D₂ перпендикулярен OC и пересекает его в точке H.

Находим H — проекцию O на прямую OC:

t_1=R^2/CO^2x_h=x_o+t_1·(x_c-x_o); y_h=y_o+t_1·(y_c-y_o)

Длина отрезка DH из подобия треугольников и теоремы Пифагора:

DH=R√(1-t_1)

Нормаль к прямой OC:

A_{oc}=y_o-y_c; B_{oc}=x_c-x_ot_2=DH/√(A^2_{oc}+B^2_{oc})

Два кандидата на точку D (рис.4):

D_1=(x_h+A_{oc}·t_2;y_h+B_{oc}·t_2); D_2=(x_h-A_{oc}·t_2;y_h-B_{oc}·t_2)
Рисунок 4 - Варианты расположения точки D
Рисунок 4 - Варианты расположения точки D

Шаг 3. Выбираем правильную точку D

Какую из двух точек выбрать — зависит от угла ∠ABC:

Условие

Выбор

∠ABC > 90°

D с меньшим BD

∠ABC < 90°

D с большим BD

∠ABC = 90°

D с большим AD

Угол оцениваем через теорему Пифагора — без тригонометрии:

  • AC2>AB2+BC2 - угол тупой

  • AC2<AB2+BC2 - угол острый

Рисунок 5 - Варианты траекторий
Рисунок 5 - Варианты траекторий

Шаг 4. Считаем длину дуги BD

По теореме косинусов из треугольника OBD:

cosφ=1-(BD^2)/(2·R^2); φ=arccos[1-(BD^2)/(2·R^2)]

Расстояние от C до прямой AB:

L_1=(|A_{AB}·x_c+B_{AB}·y_c-A_{AB}·x_a-B_{AB}·y_a|)/√(A^2_{AB}+B^2_{AB})

Если L1<2R и угол острый — большая дуга:

BD=(2π-φ)·R

Иначе — меньшая:

BD=φ·R

Шаг 5. Считаем характеристики траектории

Зная координаты всех точек и длину дуги, посчитать характеристики траектории несложно. Разобьём путь на два участка:

Дуга B → D — разворот по окружности. Длина дуги уже найдена на шаге 4. В таком случае время её прохождения:

t_{BD}=BD/V

Участок D → C — прямолинейный полёт к цели:

∣DC∣=√(((x_c−x_d)^2+(y_c−y_d)^2)t_{DC}=∣DC∣/V

Итого расстояние и время:

L=∣AB∣+φ⋅R+∣DC∣T=φR/V+∣DC∣/V

Реализация в Engee

Всю описанную математику мы реализовали в среде Engee на языке Julia. Engee — это облачная среда динамического моделирования, доступная бесплатно прямо в браузере. Никакой установки, никаких зависимостей — открыл и считаешь. Именно поэтому с ней вполне справится школьник старших классов: порог входа минимальный, а результат — полноценная инженерная модель.

Структура кода

Скрипт разбит на шесть логических блоков. Разберём каждый подробно.

Блок 1. Структура Waypoint

struct Waypoint
    x::Float64
    y::Float64
end

Первое, что бросается в глаза — мы не используем название Point, хотя логично было бы. Причина проста: в Engee уже есть встроенный тип Point из графических пакетов (MakieGeometryBasics), и попытка объявить свой Point приведёт к ошибке. Поэтому называем нашу структуру Waypoint — навигационная точка маршрута. Заодно название точнее отражает смысл.

Важный момент для Julia: struct обязан быть объявлен до первого использования. Поэтому он стоит самым первым блоком, ещё до входных параметров.

Блок 2. Входные параметры

A = Waypoint(0.0, 0.0)        # Начальная точка маршрута
B = Waypoint(2000.0, 0.0)     # Точка начала разворота
C = Waypoint(2400.0, -1800.0) # Целевая точка
R = 300.0                      # Радиус разворота, м
V = 35.0                       # Скорость, м/с

Все параметры задачи собраны в одном месте — в самом начале файла. Это сделано намеренно: чтобы поменять задачу, не нужно лезть вглубь кода. Изменили координаты точек, радиус или скорость — и запускаете заново.

Параметры подобраны так, чтобы соответствовать реальному лёгкому летательному аппарату:

  • Маршрут порядка 2–3 км — типично для тактической задачи;

  • Скорость 35 м/с (~126 км/ч) — крейсерский режим;

  • Радиус 300 м — соответствует крену ~30° на данной скорости.

Блок 3. Вспомогательная функция distance

distance(p1::Waypoint, p2::Waypoint) =
    sqrt((p1.x - p2.x)^2 + (p1.y - p2.y)^2)

Евклидово расстояние между двумя точками — формула из школьного курса геометрии. Используется повсюду в алгоритме: при выборе центра O, при вычислении хорды BD, при подсчёте длин участков маршрута.

Блок 4. Функция compute_turn — ядро алгоритма

function compute_turn(A::Waypoint, B::Waypoint, C::Waypoint, R::Float64)

    # Шаг 1. Коэффициенты нормали к прямой AB
    A_ab = B.y - A.y
    B_ab = A.x - B.x
    t    = R / sqrt(A_ab^2 + B_ab^2)

    # Шаг 2. Два кандидата на центр окружности O
    O1 = Waypoint(B.x + A_ab * t, B.y + B_ab * t)
    O2 = Waypoint(B.x - A_ab * t, B.y - B_ab * t)
    O  = distance(C, O1) < distance(C, O2) ? O1 : O2

    # Шаг 3. Находим точку H
    CO = distance(C, O)
    t1 = R^2 / CO^2
    H  = Waypoint(O.x + t1 * (C.x - O.x), O.y + t1 * (C.y - O.y))

    # Шаг 4. Длина DH и нормаль к OC
    DH   = R * sqrt(max(0.0, 1 - t1))
    A_oc = O.y - C.y
    B_oc = C.x - O.x
    t2   = DH / sqrt(A_oc^2 + B_oc^2)

    # Шаг 5. Два кандидата на точку D
    D1 = Waypoint(H.x + A_oc * t2, H.y + B_oc * t2)
    D2 = Waypoint(H.x - A_oc * t2, H.y - B_oc * t2)

    # Шаг 6. Выбираем D по углу ABC
    AC² = distance(A, C)^2
    AB² = distance(A, B)^2
    BC² = distance(B, C)^2
    BD1 = distance(B, D1)
    BD2 = distance(B, D2)

    if AC² ≈ AB² + BC²
        D = distance(A, D1) > distance(A, D2) ? D1 : D2
    elseif AC² > AB² + BC²
        D = BD1 < BD2 ? D1 : D2
    else
        D = BD1 > BD2 ? D1 : D2
    end

    # Шаг 7. Центральный угол φ
    BD    = distance(B, D)
    cos_φ = clamp(1 - BD^2 / (2 * R^2), -1.0, 1.0)
    φ     = acos(cos_φ)

    # Шаг 8. Расстояние от C до прямой AB
    L1 = abs(A_ab * C.x + B_ab * C.y - A_ab * A.x - B_ab * A.y) /
         sqrt(A_ab^2 + B_ab^2)

    # Шаг 9. Длина дуги BD
    arc_BD = if L1 < 2R && AC² < AB² + BC²
        (2π - φ) * R
    else
        φ * R
    end

    return D, O, arc_BD, φ
end

Это главная функция скрипта. Принимает три точки и радиус, возвращает четыре значения: точку касания D, центр окружности O, длину дуги и центральный угол φ. Внутри — прямая реализация шагов 1–4 из математического раздела.

Никаких итераций, никакого численного поиска — только последовательные арифметические операции. Каждый шаг снабжён комментарием, поэтому код читается почти так же, как математический вывод выше.

Одна строка заслуживает отдельного внимания:

cos_φ = clamp(1 - BD^2 / (2 * R^2), -1.0, 1.0)

Блок 5. Вывод результатов

После вызова compute_turn считаются все метрики траектории и выводятся в консоль:

=============================================
Входные параметры:
  A = (0.0, 0.0)
  B = (1500.0, 0.0)
  C = (2000.0, -1800.0)
  R = 300.0 м
  V = 35.0 м/с
=============================================
Результаты:
  Центр окружности O = (1500.0, -300.0)
  Точка касания    D = (1797.4, -260.9)
  Центральный угол   = 82.5°
  Угловая скорость   = 0.117 рад/с
---------------------------------------------
Участок A → B:  1500.0 м  |  42.9 с
Дуга  B → D:    432.0 м  |  12.3 с
Участок D → C:  1552.4 м  |  44.4 с
---------------------------------------------
Итого расстояние: 3484.4 м
Итого время:      99.6 с
=============================================

Блок 6. Визуализация

angle_B  = atan(B.y - O.y, B.x - O.x)
angle_D  = atan(D.y - O.y, D.x - O.x)
angles   = range(angle_B, angle_D, length=200)
arc_x    = O.x .+ R .* cos.(angles)
arc_y    = O.y .+ R .* sin.(angles)

θ        = range(0, 2π, length=300)
circle_x = O.x .+ R .* cos.(θ)
circle_y = O.y .+ R .* sin.(θ)

plt = plot(circle_x, circle_y,
    linestyle=:dash, color=:lightgray,
    label="Окружность разворота", aspect_ratio=:equal)

plot!(plt, [A.x, B.x], [A.y, B.y],
    color=:blue, linewidth=2, label="A → B")

plot!(plt, arc_x, arc_y,
    color=:red, linewidth=2, label="Дуга B → D")

plot!(plt, [D.x, C.x], [D.y, C.y],
    color=:green, linewidth=2, label="D → C")

scatter!(plt,
    [A.x, B.x, C.x, D.x, O.x],
    [A.y, B.y, C.y, D.y, O.y],
    color=:black, markersize=5, label="")

off = R * 0.08
annotate!(plt, A.x, A.y + off, text("A", 10, :blue))
annotate!(plt, B.x, B.y + off, text("B", 10, :blue))
annotate!(plt, C.x, C.y + off, text("C", 10, :green))
annotate!(plt, D.x, D.y + off, text("D", 10, :red))
annotate!(plt, O.x, O.y + off, text("O", 10, :gray))

title!(plt, "Траектория A → B → (дуга) → D → C")
xlabel!(plt, "x, м")
ylabel!(plt, "y, м")
xlims!(plt, -100, 2500)
ylims!(plt, -2500, 100)
display(plt)

График траектории

На графике (рис. 6) отображается:

  • Синяя линия — прямой участок A → B, полёт по исходному курсу

  • Красная дуга — разворот B → D, траектория манёвра

  • Зелёная линия — прямой участок D → C, выход на курс к цели

  • Серый пунктир — окружность разворота целиком, радиуса R с центром O

  • Чёрные точки — ключевые точки траектории: A, B, C, D, O

Точка O — центр окружности разворота, находится на перпендикуляре к AB в точке B на расстоянии R. Точка D — место выхода из разворота, где аппарат плавно переходит с дуги на прямую к C.

Рисунок 6 - Рассчитанная траектория манёвра летательного аппарата
Рисунок 6 - Рассчитанная траектория манёвра летательного аппарата

Практическое применение

Задача, которую мы разобрали — не учебный пример. Она встречается везде, где есть объект с фиксированным радиусом поворота и заданным маршрутом.

Летательные аппараты. Самолёт не может мгновенно сменить курс — он обязан выполнить разворот с определённым радиусом, который зависит от скорости и допустимого крена. Именно поэтому в бортовых системах управления расчёт точки начала манёвра выполняется заранее, до того как аппарат её достигнет. Та же логика используется при построении схем захода на посадку и процедурных разворотов в инструментальных полётах.

Наземные роботы. Любой робот с дифференциальным приводом имеет минимальный радиус поворота. Планировщики пути для таких роботов используют ровно ту же геометрию — она лежит в основе алгоритмов семейства путей Дубинса, которые применяются в мобильной робототехнике повсеместно.

Автономные автомобили. Расчёт траектории поворота на перекрёстке, перестроения на трассе, выезда с парковки — везде присутствует ограничение на кривизну пути. Алгоритмы планирования траектории в системах типа Apollo и Autoware опираются на схожий математический аппарат.

Морские суда. Радиус циркуляции крупного танкера или контейнеровоза может достигать нескольких километров. Капитан обязан начать манёвр задолго до нужной точки — иначе судно просто не впишется в фарватер. Здесь цена ошибки в расчёте точки начала поворота измеряется не метрами, а посадкой на мель.

Общее во всех случаях одно: объект с кинематическим ограничением на радиус поворота должен заранее знать, где начать манёвр. Геометрия, которую мы разобрали, даёт на этот вопрос точный ответ — и, как выяснилось, для этого достаточно школьного курса математики.

Заключение

Мы прошли путь от простой инженерной задачи — где начать разворот — до полного геометрического решения с реализацией в среде моделирования Engee.

Главный вывод: за системами управления реальных летательных аппаратов стоит математика, которую проходят в школе. Касательная, теорема Пифагора, подобие треугольников — этого достаточно, чтобы рассчитать траекторию манёвра.

Задача Дубинса, сформулированная в 1957 году, до сих пор остаётся актуальной — и наше решение является её частным случаем: один разворот, одна дуга, фиксированный радиус, известная точка начала манёвра. Если тема заинтересовала, следующий шаг — кривые Ридса-Шеппа. Это обобщение задачи Дубинса для объектов, которые умеют двигаться назад. Казалось бы, небольшое добавление — но оно кардинально меняет пространство решений: задний ход иногда позволяет найти путь значительно короче, чем только вперёд. Особенно актуально для автономных автомобилей и роботов на парковке — там манёвр задним ходом часто единственный способ вписаться в узкое пространство.

И напоследок: если вы читаете это в старших классах школы — у вас уже есть весь необходимый математический аппарат. Осталось только попробовать.

Полезные ссылки

Готовая модель в Engee