Перейти к содержимому
k13

k13. Математика дрона: векторы и системы координат — body, NED, ENU

k13. Математика дрона: векторы и системы координат — body, NED, ENU

UAV Python Pro · Конспект K13 · Модуль M2. Математика дрона: векторы и системы координат — body, NED, ENU · Редакция 1.0 · После CHECKPOINT-G1

Модуль M1 закрыт, гейт G1 сдан: вы умеете собрать CLI-программу с конфигурацией, состоянием, логированием и тестами. С этого урока начинается модуль M2 «Математика дрона». Телеметрия автопилота и команды ему — это числа, но каждое число живёт в своей системе координат и без соглашения об осях смысла не имеет. Поле z, равное минус пяти, в одной системе означает «пять метров над стартовой точкой», а в другой — «пять метров под ней». На этом уроке вы освоите векторы и три главные системы координат беспилотника: связанную систему body, земную NED и земную ENU, — а также научитесь переводить векторы между ними и проверять результаты тестами.

Одна и та же физическая величина — например, скорость аппарата — записывается разными наборами чисел в зависимости от того, в какой системе координат ведётся отсчёт. Система координат фиксирует направление осей относительно выбранного объекта: относительно Земли (NED и ENU) или относительно корпуса аппарата (body). Сообщения MAVLink всегда указывают свою систему координат, и задача разработчика — никогда их не путать: перепутанный знак или переставленные оси в команде скорости напрямую превращаются в ошибку траектории полёта.

Содержание

1. Цели урока

После изучения этого урока вы будете уметь выполнять следующие действия.

  • Вы установите библиотеку numpy в виртуальное окружение и выполните базовые операции над векторами: сложение, вычитание, умножение на число, вычисление длины и нормирование.
  • Вы объясните, что такое система координат (фрейм), и проверите тройку осей на «правость» с помощью правила правой руки.
  • Вы назовёте оси систем body, NED и ENU и скажете, какая экосистема какую систему использует.
  • Вы переведёте вектор из NED в ENU и обратно с помощью функций ned_to_enu() и enu_to_ned().
  • Вы разложите скорость на компоненты NED по модулю скорости и курсу с помощью math.cos, math.sin и math.radians.
  • Вы вычислите скалярное произведение вектора ветра и направления полёта и истолкуете его знак как попутный или встречный ветер.
  • Вы напишете тесты математических функций с допусками pytest.approx и np.allclose и объясните, почему точное сравнение дробных чисел ненадёжно.
Связь с разработкой программного обеспечения для БПЛА. Начиная с модуля M5 каждый ваш скрипт для pymavlink будет читать позицию и скорость в NED, угловые скорости в связанной системе body и отправлять команды скорости снова в NED. Математика этого урока — не академическое упражнение, а ежедневный рабочий инструмент: без неё сообщения телеметрии остаются непонятными наборами чисел.
↑ К оглавлению

2. Словарь урока

ТерминПростыми словами
Скаляр Величина, которую полностью задаёт одно число: напряжение батареи, температура, масса.
Вектор Величина, у которой есть и числовое значение, и направление: скорость, перемещение, ветер.
Система координат (фрейм) Договорённость о начале отсчёта и о направлениях трёх осей, относительно которых записываются координаты вектора.
Правая тройка Согласованная ориентация осей: если согнуть пальцы правой руки от X к Y, большой палец укажет вдоль Z.
Body (связанная система) Оси жёстко связаны с корпусом аппарата: X — вперёд, Y — вправо, Z — вниз. Поворачивается вместе с дроном.
NED Земная система: X — север (North), Y — восток (East), Z — вниз (Down). Стандарт ArduPilot и PX4.
ENU Земная система: X — восток (East), Y — север (North), Z — вверх (Up). Часто встречается в ROS.
Норма вектора Длина вектора: корень из суммы квадратов компонент.
Единичный вектор Вектор длины один: задаёт чистое направление без величины.
Курс (heading, yaw) Угол между направлением на север и «носом» аппарата, измеренный по часовой стрелке в градусах.
Скалярное произведение Операция над двумя векторами, результат которой — одно число; показывает, насколько векторы «сонаправлены».
Проекция Составляющая одного вектора вдоль направления другого; скалярное произведение с единичным вектором направления.
numpy Библиотека численных вычислений для Python: массивы и готовые векторные операции, отраслевой стандарт.
pytest.approx Средство pytest для сравнения дробных чисел с допуском вместо точного равенства.
↑ К оглавлению

3. Зачем программисту БПЛА системы координат

Начнём с конкретной ситуации. Бортовой компьютер получил число: вертикальная составляющая позиции равна минус пять метров. Само по себе это число ничего не утверждает. Если оно записано в системе NED, где ось Z направлена вниз, то минус пять означает «пять метров выше точки старта». Если то же число записано в системе ENU, где ось Z направлена вверх, то оно означает «пять метров ниже точки старта». Одно и то же число — два противоположных физических смысла. Различает их только договорённость о системе координат.

Цена такой путаницы в нашей отрасли высока. Если наземная станция отправит автопилоту команду скорости, в которой оси перепутаны местами, аппарат полетит на восток вместо севера; если перепутан знак вертикали — начнёт снижаться вместо набора высоты. В симуляторе SITL ошибка обернётся падением виртуального аппарата и строкой в журнале; на реальном беспилотном летательном аппарате — аварией. Поэтому у разработчиков есть простая дисциплина: каждое имя переменной и каждый комментарий обязаны называть систему координат явно, например v_ned или velocity_enu, а числа без указания системы считаются подозрительными.

Модуль M2 даёт минимальный математический набор, которого достаточно для уверенной работы с телеметрией и командами. Этот урок знакомит с векторами и системами координат. Следующий урок K14 объяснит, как углы ориентации (крен, тангаж, рыскание) связывают связанную систему аппарата с земной системой, и что такое кватернионы. Урок K15 переведёт всё это в географические координаты: широта, долгота, расстояние между точками и направление на точку.

↑ К оглавлению

4. Вектор, скаляр и операции с numpy

Скаляр — это величина, которую полностью задаёт одно число: напряжение батареи 14,8 В, температура 23 °C, масса 1,2 кг. Вектор — величина, у которой кроме значения есть направление: скорость аппарата, перемещение из одной точки в другую, вектор ветра. Вектор записывают упорядоченным набором из трёх чисел — компонент по осям:

\[ \vec{v} = (v_x,\; v_y,\; v_z) \]

Словами: вектор — это тройка чисел, но не любая, а договорённая: первое число относится к оси X, второе к оси Y, третье к оси Z выбранной системы координат. Сами по себе операции над векторами не требуют выбора системы: векторы можно складывать (два последовательных перемещения дают суммарное), вычитать (разность позиций даёт перемещение «из B в A» и наоборот) и умножать на число (растягивать или сжимать длину, не меняя направления).

Норма вектора (его длина, или модуль) вычисляется по теореме Пифагора, обобщённой на три измерения:

\[ \|\vec{v}\| = \sqrt{v_x^2 + v_y^2 + v_z^2} \]

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

Вычислять всё это вручную несложно, но в отрасли принят инструмент numpy — библиотека численных вычислений для Python. Она добавляет тип «массив чисел» и готовые операции: сложение массивов, умножение на число, вычисление нормы, скалярное произведение. numpy устанавливается в виртуальное окружение привычной командой из урока K06:

python -m pip install numpy

Чистый код

# Пример: перемещение между точками, длина и единичный вектор
import numpy as np

start = (0.0, 0.0, -5.0)   # точка A в NED: 5 м над началом координат
end = (30.0, 40.0, -5.0)   # точка B: 30 м север, 40 м восток, та же высота

shift = np.asarray(end, dtype=float) - np.asarray(start, dtype=float)
length = np.linalg.norm(shift)
direction = shift / length
step = direction * 10.0
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import numpy as np
    # → Общепринятое короткое имя библиотеки: np.
    
    start = (0.0, 0.0, -5.0)   # точка A в NED: 5 м над началом координат
    end = (30.0, 40.0, -5.0)   # точка B: 30 м север, 40 м восток, та же высота
    # → Координаты в NED: ось Z направлена вниз,
    # → поэтому «выше старта» записывается отрицательным числом.
    
    shift = np.asarray(end, dtype=float) - np.asarray(start, dtype=float)
    # → np.asarray превращает кортеж в массив numpy с дробными числами.
    # → Вычитание массивов происходит покомпонентно:
    # → получается вектор перемещения из A в B: (30, 40, 0).
    
    length = np.linalg.norm(shift)
    # → Готовая функция нормы: корень из суммы квадратов компонент.
    # → Для (30, 40, 0) длина равна 50 — египетский треугольник 3-4-5,
    # → умноженный на десять.
    
    direction = shift / length
    # → Деление массива на число тоже покомпонентное:
    # → получился единичный вектор (0.6, 0.8, 0.0) — чистое направление.
    
    step = direction * 10.0
    # → Умножение единичного вектора на 10 даёт перемещение
    # → длиной ровно 10 метров вдоль того же направления: (6, 8, 0).
Зачем вам это в курсе. Позиция, скорость, ускорение и ветер в сообщениях MAVLink приходят именно тройками чисел. Все дальнейшие расчёты — расстояние до точки, снос ветром, направление на цель — строятся на трёх операциях этого раздела: разность, норма, единичный вектор.
↑ К оглавлению

5. Системы координат и правая тройка

Система координат (по-английски — frame, «рамка отсчёта») — это договорённость из двух частей: где находится начало отсчёта и куда направлены три оси. Один и тот же физический вектор в разных системах получает разные координаты. Бытовая аналогия: инструкцию «пройти два квартала вперёд и один направо» вы привязываете к себе и своему направлению взгляда, а инструкцию «два квартала на север и один на восток» — к карте города. Оба описания верны, но пересчитать одно в другое можно, только зная, куда вы смотрите. Аналогия заканчивается там, где начинается строгость: в математике правило пересчёта между системами известно точно и выражается формулами.

Оси трёхмерной системы принято ориентировать согласованно — в правую тройку. Проверка выполняется правилом правой руки:

Правило правой руки: согните пальцы правой руки от оси X
к оси Y по короткой дуге — отставленный большой палец
укажет направление оси Z.

NED:  X — север,  Y — восток  ⇒  Z — вниз
ENU:  X — восток, Y — север   ⇒  Z — вверх
body: X — вперёд, Y — вправо  ⇒  Z — вниз

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

Соберём три главные системы курса в одну таблицу.

СистемаОсь XОсь YОсь ZКто использует
body (связанная) вперёд, к «носу» вправо по борту вниз, к «животу» Датчики на борту: акселерометр, гироскоп; угловые скорости в MAVLink
NED север (North) восток (East) вниз (Down) ArduPilot и PX4: позиция, скорость, ускорение локально
ENU восток (East) север (North) вверх (Up) ROS и исследовательские стеки, часть геоинформационных инструментов
Начало отсчёта. У земных систем NED и ENU есть ещё и начало координат. На практике ArduPilot использует так называемую локальную систему с началом в позиции EKF — точки, откуда стартовал аппарат и где расположился его «дом» (home). Географические широту и долготу этот урок не затрагивает: ими занимается K15.
↑ К оглавлению

6. NED и ENU: две «земные» системы

NED и ENU описывают один и тот же мир — окрестность точки старта относительно сторон света, — но оси у них направлены по-разному. Переход между ними выражается простым правилом: первая и вторая компоненты меняются местами, а третья меняет знак.

\[ (n,\; e,\; d)_{\mathrm{NED}} \;\mapsto\; (e,\; n,\; -d)_{\mathrm{ENU}} \]

Словами: вектор «30 метров на север, 40 на восток, 5 вниз» в NED записывается как (30, 40, 5), а в ENU — как (40, 30, −5): восток стал первой компонентой, север второй, а «вниз» превратилось в «минус вверх». Обратите внимание на важное свойство: это преобразование симметрично. Обратный перевод из ENU в NED выполняется той же формулой — обмен местами и смена знака третьей компоненты. Применив правило дважды, вы получите исходный вектор; тесты практики проверяют именно этот «круговой» возврат.

Чистый код

# Пример: перевод векторов между NED и ENU
def ned_to_enu(v_ned):
    n, e, d = float(v_ned[0]), float(v_ned[1]), float(v_ned[2])
    return (e, n, -d)

def enu_to_ned(v_enu):
    x, y, z = float(v_enu[0]), float(v_enu[1]), float(v_enu[2])
    return (y, x, -z)
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    def ned_to_enu(v_ned):
    # → Функция принимает вектор в NED: кортеж, список или массив из трёх чисел.
    
        n, e, d = float(v_ned[0]), float(v_ned[1]), float(v_ned[2])
    # → Распаковываем компоненты в именованные переменные и приводим к float:
    # → имена n, e, d напоминают о смысле каждой компоненты.
    
        return (e, n, -d)
    # → Правила перевода: восток становится первой компонентой ENU,
    # → север — второй, а «вниз» меняет знак на «вверх».
    
    def enu_to_ned(v_enu):
    # → Обратный перевод устроен симметрично.
    
        x, y, z = float(v_enu[0]), float(v_enu[1]), float(v_enu[2])
    # → В ENU первая компонента — восток, вторая — север, третья — вверх.
    
        return (y, x, -z)
    # → Север (y) возвращается на первое место NED,
    # → восток (x) — на второе, «вверх» меняет знак на «вниз».
Классическая авария из-за путаницы систем. Если передать координаты, подготовленные в ENU, напрямую в интерфейс, ожидающий NED, команда «лететь пять метров на восток» превратится в «лететь пять метров на север», а «набраться высоты» — в «снижаться». Правило безопасности простое и жёсткое: не отправлять автопилоту числа, система координат которых неизвестна или не записана явно в имени переменной.
Отрицательный ноль в выводе. Запустив демо практики, вы увидите в строках вида «ENU (0.0, 1.0, -0.0)» значение −0.0. Это «минус ноль» — след смены знака у нулевой компоненты: математически оно равно обычному нулю, сравнение -0.0 == 0.0 даёт True, и тесты проходят. К разделу 10 вы узнаете, почему дробная арифметика компьютера вообще ведёт себя подобным образом.
↑ К оглавлению

7. Связанная система body: оси на корпусе

Связанная система координат (body frame) отличается от земных принципиальным свойством: её оси жёстко привязаны к корпусу аппарата и вращаются вместе с ним. Ось X всегда смотрит вперёд, к «носу» — у мультиротора это направление переднего луча; ось Y — вправо по борту; ось Z — вниз, к «животу». Когда дрон наклоняется, «вперёд» наклоняется вместе с ним: вектор тяги моторов живёт именно в связанной системе, потому что моторы прикручены к раме, а не к планете.

Связанная система — это мир бортовых датчиков. Акселерометр измеряет ускорение вдоль осей корпуса, гироскоп — угловые скорости вращения вокруг этих осей. Оба прибора физически установлены на плате и «не знают», куда аппарат смотрит относительно севера. Поэтому в телеметрии MAVLink угловые скорости приходят в body-системе, а позиция и скорость относительно земли — в NED; подробнее о сообщениях мы поговорим в модуле M5.

Главный вопрос, который отсюда следует: как пересчитать вектор из связанной системы в земную и обратно? Ответ требует знания ориентации аппарата — углов крена (roll), тангажа (pitch) и рыскания (yaw), которые показывают, как именно повёрнут корпус относительно Земли. Полный пересчёт — тема следующего урока K14. Здесь зафиксируем два простых случая, которые понятны уже сейчас.

  • Аппарат стоит на земле без наклона, «нос» смотрит на север. Связанная система совпадает с NED: вперёд — это север, вправо — восток, вниз — вниз.
  • Аппарат без наклона повёрнул «нос» на восток (курс 90°). Теперь X связанной системы смотрит вдоль Y системы NED: «вперёд» стало «на восток». Пересчёт вектора сводится к повороту вокруг вертикальной оси на угол курса — ровно эту операцию выполняет раздел 8.
Почему это важно именно в такой связке. Автопилот непрерывно решает задачу, обратную нашей: знает команды в связанной системе (тяга и моменты от моторов) и состояние в земной (куда летит), и пересчитывает одно в другое десятки и сотни раз в секунду. Программист наземной станции и бортового компьютера-компаньона работает с результатами этих пересчётов и обязан свободно читать обе стороны.
↑ К оглавлению

8. Скорость в NED и курс аппарата

Курс (heading, или рыскание yaw) — это угол между направлением на север и направлением «носа» аппарата, измеренный по часовой стрелке, если смотреть сверху. Курс 0° — аппарат смотрит на север, 90° — на восток, 180° — на юг, 270° — на запад. Именно в таком виде — градусы от севера по часовой стрелке — курс показывает компас и именно так его отдаёт наземная станция Mission Planner.

Пусть аппарат летит горизонтально со скоростью \(v\) метров в секунду по курсу \(\psi\). Разложение вектора скорости на компоненты NED задаётся школьными синусом и косинусом:

\[ v_N = v \cos\psi, \qquad v_E = v \sin\psi, \qquad v_D = 0 \]

Словами: северная составляющая скорости равна модулю скорости, умноженному на косинус курса, а восточная — на синус курса. Косинус даёт «долю движения вдоль севера», синус — «долю движения вдоль востока»; при курсе 0° весь полёт идёт на север, при 90° — на восток. Вертикальная составляющая в горизонтальном полёте равна нулю.

север (v_north = v · cos ψ)
  ↑
  │   ╱ направление полёта
  │  ╱   длиной v
  │ ╱
  │╱ ψ — курс от севера
  │      по часовой стрелке
  └────────→ восток (v_east = v · sin ψ)

Остаётся техническая деталь: тригонометрические функции Python работают не с градусами, а с радианами. Радиан — угловая мера, в которой полный оборот равен не 360°, а \(2\pi\) радиан; прямой угол — это \(\pi/2\) радиан, то есть примерно 1,5708. Переводит градусы в радианы функция math.radians(). Забыть этот перевод — одна из самых частых математических ошибок новичка: math.cos(90) вернёт не ноль, потому что 90 здесь — радианы, а не градусы.

Чистый код

# Пример: скорость в NED по модулю и курсу
import math

def heading_to_velocity_ned(speed_ms: float, heading_deg: float) -> tuple:
    if speed_ms < 0:
        raise ValueError(f"модуль скорости не может быть отрицательным: {speed_ms}")
    yaw_rad = math.radians(heading_deg % 360.0)
    v_north = speed_ms * math.cos(yaw_rad)
    v_east = speed_ms * math.sin(yaw_rad)
    return (v_north, v_east, 0.0)
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import math
    # → Стандартный математический модуль Python: установка не требуется.
    
    def heading_to_velocity_ned(speed_ms: float, heading_deg: float) -> tuple:
    # → Вход: модуль скорости (м/с) и курс (градусы). Выход: кортеж из трёх чисел NED.
    
        if speed_ms < 0:
            raise ValueError(f"модуль скорости не может быть отрицательным: {speed_ms}")
    # → Модуль скорости — длина вектора, а длина не бывает отрицательной.
    # → Невозможный вход отклоняется исключением сразу (приём урока K08).
    
        yaw_rad = math.radians(heading_deg % 360.0)
    # → Остаток от деления на 360 приводит любой курс к диапазону 0–360:
    # → курс 450° означает то же, что 90°. Затем градусы переводятся в радианы.
    
        v_north = speed_ms * math.cos(yaw_rad)
    # → Косинус курса — доля скорости, направленная на север.
    
        v_east = speed_ms * math.sin(yaw_rad)
    # → Синус курса — доля скорости, направленная на восток.
    
        return (v_north, v_east, 0.0)
    # → Третья компонента NED — вертикальная; в горизонтальном полёте она нулевая.

Проверим формулу на опорных направлениях: при скорости 10 м/с должны получаться «круглые» числа.

Курс, °Куда летитv_north, м/сv_east, м/с
0 на север 10.00 0.00
45 на северо-восток 7.07 7.07
90 на восток 0.00 10.00
180 на юг -10.00 0.00
270 на запад 0.00 -10.00
Мост к уроку K14. Функция heading_to_velocity_ned() фактически поворачивает единичный вектор «вперёд» связанной системы вокруг вертикальной оси на угол курса. Это частный случай полного пересчёта body → NED, в котором участвуют ещё крен и тангаж; полный случай — тема следующего урока.
↑ К оглавлению

9. Скалярное произведение и проекция: ветер

Вторая операция, которая нужна в работе постоянно, — скалярное произведение двух векторов. Результат этой операции — одно число (поэтому она и «скалярная»), которое показывает, насколько два вектора сонаправлены. Алгебраически скалярное произведение равно сумме произведений соответствующих компонент:

\[ \vec{a} \cdot \vec{b} = a_x b_x + a_y b_y + a_z b_z \]

Геометрически то же самое число равно произведению длин векторов на косинус угла \(\theta\) между ними:

\[ \vec{a} \cdot \vec{b} = \|\vec{a}\| \, \|\vec{b}\| \cos\theta \]

Словами и по знакам: если векторы смотрят примерно в одну сторону (угол острый), произведение положительно; если в противоположные (угол тупой) — отрицательно; если перпендикулярны — равно нулю, потому что косинус прямого угла равен нулю. Отдельно отметим частный случай: скалярное произведение вектора на единичный вектор направления даёт проекцию — составляющую вектора вдоль этого направления, со знаком.

Прикладная задача этого раздела: летим по известному курсу, известен вектор ветра. Насколько ветер попутный или встречный? Договоримся о соглашении (и запишем его в комментарии к коду): вектор ветра указывает, куда воздух движется. Метеорологи, наоборот, называют ветер по направлению, откуда он дует: «северный ветер» дует с севера на юг, то есть его вектор направлен на юг. Перепутать эти два соглашения — классический способ получить знак «наоборот».

Чистый код

# Пример: проекция ветра на направление полёта
import numpy as np

def wind_along_track_ms(wind_ned, heading_deg: float) -> float:
    track = np.asarray(heading_to_velocity_ned(1.0, heading_deg), dtype=float)
    wind = np.asarray(wind_ned, dtype=float)
    return float(np.dot(wind, track))
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import numpy as np
    # → Скалярное произведение возьмём готовое из numpy.
    
    def wind_along_track_ms(wind_ned, heading_deg: float) -> float:
    # → Вход: вектор ветра в NED (куда дует, м/с) и курс полёта (градусы).
    
        track = np.asarray(heading_to_velocity_ned(1.0, heading_deg), dtype=float)
    # → Направление полёта как единичный вектор: функция из раздела 8
    # → вызывается со скоростью 1.0, поэтому возвращается чистое направление.
    
        wind = np.asarray(wind_ned, dtype=float)
    # → Ветер приводим к массиву numpy, чтобы воспользоваться np.dot.
    
        return float(np.dot(wind, track))
    # → np.dot — скалярное произведение: сумма произведений компонент.
    # → Результат больше нуля — ветер попутный, меньше нуля — встречный,
    # → ноль — чистый боковой ветер. float(...) превращает ответ numpy
    # → в обычный тип Python — так удобнее печатать и тестировать.

Разберём числа из демонстрации практики. Полёт на север (курс 0°), единичный вектор направления равен (1, 0, 0). Ветер (5, 0, 0) — воздух движется на север вместе с нами: произведение +5, попутный ветер. Ветер (−8, 0, 0) — воздух движется на юг, навстречу: произведение −8, встречный ветер. Ветер (0, 6, 0) — дует строго на восток, перпендикулярно курсу: произведение 0, чистая боковина, которая сносит аппарат в сторону, но не меняет путевую скорость.

Зачем вам это в курсе. Проекция ветра на направление полёта — базовый кирпич оценки времени миссии и запаса энергии: встречный ветер съедает путевую скорость и батарею, попутный добавляет. В модулях миссий (M6) и в задачах сопровождения цели (M11) скалярное произведение встретится снова — уже для углов между направлением на цель и направлением взгляда камеры.
↑ К оглавлению

10. Тесты математики: pytest.approx

Математические функции тестируются так же, как любые другие (урок K11), но с одной особенностью: дробные числа в компьютере нельзя сравнивать на точное равенство. Причина в двоичном представлении: многие десятичные дроби, привычные человеку, в двоичной системе становятся бесконечными, и компьютер хранит их округлёнными. Классический пример:

Чистый код

# Пример: почему точное сравнение дробных чисел подводит
print(0.1 + 0.2 == 0.3)              # False
print(0.1 + 0.2)                     # 0.30000000000000004

import math
print(math.cos(math.radians(90.0)))  # 6.123233995736766e-17, а не 0.0
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    print(0.1 + 0.2 == 0.3)              # False
    # → Сумма 0.1 + 0.2 не равна 0.3 точно: обе дроби двоично бесконечны,
    # → и результат округляется с крошечной погрешностью.
    
    print(0.1 + 0.2)                     # 0.30000000000000004
    # → Вот эта погрешность: семнадцатый знак после запятой.
    
    import math
    print(math.cos(math.radians(90.0)))  # 6.123233995736766e-17, а не 0.0
    # → Косинус прямого угла математически равен нулю,
    # → но вычислительный результат — очень маленькое число 6.1e-17.
    # → Запись e-17 означает «умножить на 10 в минус семнадцатой степени».

Отсюда правило для тестов математики: вместо точного равенства использовать сравнение с допуском. В pytest для этого служит pytest.approx — он считает числа равными, если они отличаются в пределах крошечной относительной погрешности (по умолчанию около одной миллионной). Для массивов numpy тот же смысл несёт функция np.allclose(). Важно понимать: допуск не ослабляет тест до бесполезности. Погрешность округления составляет единицы в триллионных долях, тогда как физическая ошибка, которую тест обязан ловить, — это перепутанный знак, обмен осей или лишний множитель; такие ошибки больше допуска на многие порядки.

Чистый код

# Пример: тесты математики с допусками
import numpy as np
import pytest
from k13_task2_ned_enu import enu_to_ned, ned_to_enu
from k13_task3_velocity_ned import heading_to_velocity_ned

def test_heading_east():
    assert heading_to_velocity_ned(10.0, 90.0) == pytest.approx((0.0, 10.0, 0.0))

def test_round_trip_returns_original():
    v = (30.0, 40.0, -5.0)
    assert enu_to_ned(ned_to_enu(v)) == pytest.approx(v)

def test_displacement_and_distance():
    shift = np.array([30.0, 40.0, 0.0])
    assert np.allclose(shift, (30.0, 40.0, 0.0))
    assert np.linalg.norm(shift) == pytest.approx(50.0)
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import numpy as np
    import pytest
    from k13_task2_ned_enu import enu_to_ned, ned_to_enu
    from k13_task3_velocity_ned import heading_to_velocity_ned
    # → Тестируем функции из модулей практики: обычный импорт, как в K11.
    
    def test_heading_east():
        assert heading_to_velocity_ned(10.0, 90.0) == pytest.approx((0.0, 10.0, 0.0))
    # → Косинус 90° не равен нулю точно (6.1e-17), поэтому без approx тест бы упал.
    # → pytest.approx сравнивает кортежи покомпонентно с допуском.
    
    def test_round_trip_returns_original():
        v = (30.0, 40.0, -5.0)
        assert enu_to_ned(ned_to_enu(v)) == pytest.approx(v)
    # → «Круговой» тест: перевод туда и обратно обязан вернуть исходный вектор.
    # → Так проверяют согласованность пары взаимно обратных функций.
    
    def test_displacement_and_distance():
        shift = np.array([30.0, 40.0, 0.0])
        assert np.allclose(shift, (30.0, 40.0, 0.0))
        assert np.linalg.norm(shift) == pytest.approx(50.0)
    # → Для массивов numpy — свой инструмент допуска: np.allclose.
    # → Норма сравнивается как обычное число через pytest.approx.
↑ К оглавлению

11. Типичные ошибки

ОшибкаЧто происходитКак следует рассуждать
Передаёте вектор из ENU в интерфейс, ожидающий NED, без перевода Аппарат летит не туда: север путается с востоком, набор высоты — со снижением Перед каждой командой спрашивайте себя: в какой системе эти числа? Имена переменных обязаны содержать систему: v_ned, v_enu
Привычка «вверх — это плюс» переносится на NED Знак вертикали перевёрнут: минус пять в NED — это пять метров высоты, а не глубины В NED высота над началом отсчёта равна минус z; проверяйте знак на простом примере
Курс в градусах передан в math.cos без перевода Компоненты скорости получаются совершенно неверными Тригонометрия Python работает в радианах: сначала math.radians(), потом cos/sin
Точное сравнение дробных чисел в тестах (== 0.0) Тест падает из-за погрешности в семнадцатом знаке Используйте pytest.approx для чисел и кортежей, np.allclose для массивов
Ветер «откуда дует» подставлен как вектор «куда дует» Знак проекции перевёрнут: встречный ветер выглядит попутным Зафиксируйте соглашение в комментарии и в имени; в нашем коде вектор ветра — куда дует
numpy не установлен в виртуальном окружении ModuleNotFoundError: No module named 'numpy' Выполните python -m pip install numpy в том же venv, где запускаете код
Нормирование нулевого вектора Деление на ноль и «мусорный» результат Перед делением проверяйте норму: функция unit_vector() поднимает ValueError для нулевого вектора
↑ К оглавлению

12. Практика

Практика урока состоит из пяти файлов: четыре демонстрационных модуля с функциями и один файл тестов. Порядок работы следующий: установите numpy, создайте файлы задач 13.1–13.5 в каталоге code/M01_python, выполните прогоны из задачи 13.6 и сохраните исходники в Git (задача 13.7). В карточках в аккордеонах лежит полный текст каждого файла; он совпадает с содержимым репозитория курса.

Задача 13.1. Векторы и numpy

Файл практики: k13_task1_vector_basics.py.

Создайте модуль базовых векторных операций на numpy: перемещение между двумя точками displacement_ned(), длина вектора distance_m() и единичный вектор unit_vector() с защитой от нулевого вектора через ValueError.

Ожидаемый результат. Запуск python k13_task1_vector_basics.py печатает перемещение (30, 40, 0) м, расстояние 50.00 м, единичный вектор (0.6, 0.8, 0) и десятиметровый шаг (6, 8, 0) м.

  • Полный текст файла k13_task1_vector_basics.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k13_task1_vector_basics.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k13_task1_vector_basics.py
    # Цель: базовые операции над векторами с помощью numpy —
    #       перемещение между двумя точками в NED, длина, единичный вектор.
    # Запуск из каталога code/M01_python:
    #   python k13_task1_vector_basics.py
    
    import numpy as np
    
    
    def displacement_ned(start_m, end_m):
        """Вектор перемещения из начальной точки в конечную (обе в NED, метры)."""
        return np.asarray(end_m, dtype=float) - np.asarray(start_m, dtype=float)
    
    
    def distance_m(vector_m):
        """Длина вектора (модуль) в метрах — евклидова норма."""
        return float(np.linalg.norm(vector_m))
    
    
    def unit_vector(vector_m):
        """Единичный вектор: направление то же, длина равна единице.
    
        Для нулевого вектора поднимает ValueError: направление не определено.
        """
        v = np.asarray(vector_m, dtype=float)
        length = np.linalg.norm(v)
        if length == 0.0:
            raise ValueError("нулевой вектор не имеет направления")
        return v / length
    
    
    if __name__ == "__main__":
        start = (0.0, 0.0, -5.0)   # точка A: 5 метров над началом координат
        end = (30.0, 40.0, -5.0)   # точка B: 30 м на север, 40 м на восток, та же высота
    
        shift = displacement_ned(start, end)
        print(f"перемещение NED: {shift} м")
        print(f"расстояние: {distance_m(shift):.2f} м")
    
        direction = unit_vector(shift)
        print(f"направление (единичный вектор): {np.round(direction, 3)}")
    
        step = direction * 10.0
        print(f"шаг 10 метров вдоль направления: {np.round(step, 3)} м")

Задача 13.2. Перевод NED ↔ ENU

Файл практики: k13_task2_ned_enu.py.

Создайте модуль перевода между системами координат: функции ned_to_enu() и enu_to_ned() и демонстрацию на четырёх векторах — три осевых и один произвольный — с обратным переводом туда и обратно.

Ожидаемый результат. Демо печатает для произвольного вектора цепочку NED (30.0, 40.0, -5.0) → ENU (40.0, 30.0, 5.0) → обратно (30.0, 40.0, -5.0); осевые векторы показывают обмен местами и смену знака вертикали.

  • Полный текст файла k13_task2_ned_enu.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k13_task2_ned_enu.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k13_task2_ned_enu.py
    # Цель: перевод векторов между системами координат NED и ENU —
    #       обмен местами двух осей и смена знака вертикали.
    # Запуск из каталога code/M01_python:
    #   python k13_task2_ned_enu.py
    
    
    def ned_to_enu(v_ned):
        """Перевести координаты вектора из системы NED в систему ENU.
    
        NED: (north, east, down); ENU: (east, north, up).
        Правило: первая и вторая компоненты меняются местами,
        третья меняет знак.
        """
        n, e, d = float(v_ned[0]), float(v_ned[1]), float(v_ned[2])
        return (e, n, -d)
    
    
    def enu_to_ned(v_enu):
        """Перевести координаты вектора из системы ENU в систему NED.
    
        Преобразование симметрично: тот же обмен местами и тот же знак,
        поэтому обратный перевод возвращает исходный вектор.
        """
        x, y, z = float(v_enu[0]), float(v_enu[1]), float(v_enu[2])
        return (y, x, -z)
    
    
    if __name__ == "__main__":
        samples = [
            ("1 м на север", (1.0, 0.0, 0.0)),
            ("1 м на восток", (0.0, 1.0, 0.0)),
            ("1 м вниз", (0.0, 0.0, 1.0)),
            ("произвольный вектор", (30.0, 40.0, -5.0)),
        ]
        for title, v_ned in samples:
            v_enu = ned_to_enu(v_ned)
            back = enu_to_ned(v_enu)
            print(f"{title}: NED {v_ned} -> ENU {v_enu} -> обратно {back}")

Задача 13.3. Скорость по курсу

Файл практики: k13_task3_velocity_ned.py.

Создайте модуль разложения скорости: функция heading_to_velocity_ned() с переводом градусов в радианы через math.radians(), компонентами через math.cos() и math.sin() и защитой от отрицательного модуля скорости.

Ожидаемый результат. Демо печатает компоненты для курсов 0°, 45°, 90°, 180° и 270° при скорости 10 м/с; для курса 90° северная компонента равна 0.00, а восточная — 10.00 м/с.

  • Полный текст файла k13_task3_velocity_ned.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k13_task3_velocity_ned.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k13_task3_velocity_ned.py
    # Цель: разложить горизонтальную скорость на компоненты NED
    #       по курсу (heading), измеренному в градусах от севера по часовой стрелке.
    # Запуск из каталога code/M01_python:
    #   python k13_task3_velocity_ned.py
    
    import math
    
    
    def heading_to_velocity_ned(speed_ms: float, heading_deg: float) -> tuple:
        """Вернуть вектор скорости в NED (м/с) по модулю скорости и курсу.
    
        Курс измеряется в градусах от направления на север по часовой стрелке:
        0 — север, 90 — восток, 180 — юг, 270 — запад.
        Вертикальная компонента равна нулю: рассматриваем горизонтальный полёт.
        """
        if speed_ms < 0:
            raise ValueError(f"модуль скорости не может быть отрицательным: {speed_ms}")
        yaw_rad = math.radians(heading_deg % 360.0)
        v_north = speed_ms * math.cos(yaw_rad)
        v_east = speed_ms * math.sin(yaw_rad)
        return (v_north, v_east, 0.0)
    
    
    if __name__ == "__main__":
        speed = 10.0
        for heading in (0.0, 45.0, 90.0, 180.0, 270.0):
            v = heading_to_velocity_ned(speed, heading)
            print(
                f"курс {heading:5.1f} град: "
                f"v_north={v[0]:7.2f} м/с  v_east={v[1]:7.2f} м/с  v_down={v[2]:4.1f} м/с"
            )

Задача 13.4. Проекция ветра

Файл практики: k13_task4_wind_projection.py.

Создайте модуль скалярного произведения: функция wind_along_track_ms() на np.dot() и демонстрация четырёх ветровых ситуаций для полёта на север — попутный, встречный, чистый боковой и ветер под углом.

Ожидаемый результат. Демо печатает проекцию -8.00 м/с со словом «встречный» для южного ветра и +5.00 м/с со словом «попутный» для северного ветра.

  • Полный текст файла k13_task4_wind_projection.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k13_task4_wind_projection.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k13_task4_wind_projection.py
    # Цель: скалярное произведение векторов на прикладном примере —
    #       попутная или встречная составляющая ветра вдоль курса полёта.
    # Запуск из каталога code/M01_python:
    #   python k13_task4_wind_projection.py
    
    import numpy as np
    
    from k13_task3_velocity_ned import heading_to_velocity_ned
    
    
    def wind_along_track_ms(wind_ned, heading_deg: float) -> float:
        """Проекция вектора ветра на направление полёта (м/с).
    
        wind_ned — вектор ветра в NED: куда ветер дует, а не откуда.
        Результат больше нуля — попутный ветер, меньше нуля — встречный,
        равен нулю — чистый боковой ветер.
        """
        track = np.asarray(heading_to_velocity_ned(1.0, heading_deg), dtype=float)
        wind = np.asarray(wind_ned, dtype=float)
        return float(np.dot(wind, track))
    
    
    if __name__ == "__main__":
        heading = 0.0  # летим на север
    
        cases = [
            ("ветер дует на север (+5 м/с вдоль курса)", (5.0, 0.0, 0.0)),
            ("ветер дует на юг (8 м/с навстречу)", (-8.0, 0.0, 0.0)),
            ("ветер дует на восток (чистая боковина)", (0.0, 6.0, 0.0)),
            ("ветер под 45 град к курсу", (3.0, 3.0, 0.0)),
        ]
        for title, wind in cases:
            projection = wind_along_track_ms(wind, heading)
            if projection > 0:
                kind = "попутный"
            elif projection < 0:
                kind = "встречный"
            else:
                kind = "боковой, проекция равна нулю"
            print(f"{title}: проекция {projection:+.2f} м/с ({kind})")

Задача 13.5. Тесты математики

Файл практики: k13_task5_test_frames.py.

Создайте файл тестов: три проверки векторных операций, три проверки перевода NED/ENU, пять проверок разложения скорости (включая ValueError при отрицательной скорости и курс больше 360°) и три проверки проекции ветра — всего четырнадцать тестов с допусками pytest.approx и np.allclose().

Ожидаемый результат. Команда python -m pytest k13_task5_test_frames.py -v показывает четырнадцать строк PASSED и итог «14 passed».

  • Полный текст файла k13_task5_test_frames.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k13_task5_test_frames.py в проекте PyCharm (виртуальное окружение) и запустите тесты командой python -m pytest k13_task5_test_frames.py -v из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k13_task5_test_frames.py
    # Тесты математических модулей K13: векторы, системы NED/ENU, курс, проекция ветра.
    # Запуск из каталога code/M01_python:
    #   python -m pytest k13_task5_test_frames.py -v
    
    import numpy as np
    import pytest
    
    from k13_task1_vector_basics import displacement_ned, distance_m, unit_vector
    from k13_task2_ned_enu import enu_to_ned, ned_to_enu
    from k13_task3_velocity_ned import heading_to_velocity_ned
    from k13_task4_wind_projection import wind_along_track_ms
    
    
    # --- векторы и numpy ------------------------------------------------------
    
    
    def test_displacement_and_distance():
        shift = displacement_ned((0.0, 0.0, -5.0), (30.0, 40.0, -5.0))
        assert np.allclose(shift, (30.0, 40.0, 0.0))
        assert distance_m(shift) == pytest.approx(50.0)
    
    
    def test_unit_vector_length_is_one():
        u = unit_vector((30.0, 40.0, 0.0))
        assert np.linalg.norm(u) == pytest.approx(1.0)
    
    
    def test_unit_vector_rejects_zero():
        with pytest.raises(ValueError):
            unit_vector((0.0, 0.0, 0.0))
    
    
    # --- системы координат NED и ENU ------------------------------------------
    
    
    def test_north_in_enu_points_to_y():
        assert ned_to_enu((1.0, 0.0, 0.0)) == pytest.approx((0.0, 1.0, 0.0))
    
    
    def test_down_becomes_minus_up():
        assert ned_to_enu((0.0, 0.0, 1.0)) == pytest.approx((0.0, 0.0, -1.0))
    
    
    def test_round_trip_returns_original():
        v = (30.0, 40.0, -5.0)
        assert enu_to_ned(ned_to_enu(v)) == pytest.approx(v)
    
    
    # --- курс и скорость ------------------------------------------------------
    
    
    def test_heading_north():
        assert heading_to_velocity_ned(10.0, 0.0) == pytest.approx((10.0, 0.0, 0.0))
    
    
    def test_heading_east():
        assert heading_to_velocity_ned(10.0, 90.0) == pytest.approx((0.0, 10.0, 0.0))
    
    
    def test_heading_45_degrees():
        v = heading_to_velocity_ned(10.0, 45.0)
        assert v[0] == pytest.approx(7.0710678, rel=1e-6)
        assert v[1] == pytest.approx(7.0710678, rel=1e-6)
    
    
    def test_heading_accepts_over_360():
        assert heading_to_velocity_ned(10.0, 450.0) == pytest.approx((0.0, 10.0, 0.0))
    
    
    def test_negative_speed_raises():
        with pytest.raises(ValueError):
            heading_to_velocity_ned(-1.0, 0.0)
    
    
    # --- проекция ветра -------------------------------------------------------
    
    
    def test_headwind_is_negative():
        assert wind_along_track_ms((-8.0, 0.0, 0.0), 0.0) == pytest.approx(-8.0)
    
    
    def test_tailwind_is_positive():
        assert wind_along_track_ms((5.0, 0.0, 0.0), 0.0) == pytest.approx(5.0)
    
    
    def test_crosswind_projection_is_zero():
        assert wind_along_track_ms((0.0, 6.0, 0.0), 0.0) == pytest.approx(0.0, abs=1e-9)

Задача 13.6. Прогоны демо и тестов (обязательно выполнить)

Выполните команды по порядку из каталога code/M01_python в терминале с активным виртуальным окружением.

# Каталог: code/M01_python (venv активен)
python -m pip install numpy
python k13_task1_vector_basics.py
python k13_task2_ned_enu.py
python k13_task3_velocity_ned.py
python k13_task4_wind_projection.py
python -m pytest k13_task5_test_frames.py -v

Ожидаемый результат. Ключевые строки вывода демо (сверьте числа):

task1: перемещение NED: [30. 40.  0.] м
       расстояние: 50.00 м
       направление (единичный вектор): [0.6 0.8 0. ]
       шаг 10 метров вдоль направления: [6. 8. 0.] м

task2: 1 м на север: NED (1.0, 0.0, 0.0) -> ENU (0.0, 1.0, -0.0) -> обратно (1.0, 0.0, 0.0)
       1 м вниз: NED (0.0, 0.0, 1.0) -> ENU (0.0, 0.0, -1.0) -> обратно (0.0, 0.0, 1.0)
       произвольный вектор: NED (30.0, 40.0, -5.0) -> ENU (40.0, 30.0, 5.0) -> обратно (30.0, 40.0, -5.0)

task3: курс   0.0 град: v_north=  10.00 м/с  v_east=   0.00 м/с  v_down= 0.0 м/с
       курс  45.0 град: v_north=   7.07 м/с  v_east=   7.07 м/с  v_down= 0.0 м/с
       курс  90.0 град: v_north=   0.00 м/с  v_east=  10.00 м/с  v_down= 0.0 м/с

task4: ветер дует на север (+5 м/с вдоль курса): проекция +5.00 м/с (попутный)
       ветер дует на юг (8 м/с навстречу): проекция -8.00 м/с (встречный)
       ветер дует на восток (чистая боковина): проекция +0.00 м/с (боковой, проекция равна нулю)

tests: 14 passed

Значение «-0.0» и «+0.00» в выводе — отрицательный ноль, о котором рассказано в разделе 6; математически это ноль, и тесты проходят штатно.

Задача 13.7. Сохранение в Git

Добавьте в репозиторий пять файлов урока и сделайте коммит.

# Из корня репозитория курса
git add code/M01_python/k13_task1_vector_basics.py code/M01_python/k13_task2_ned_enu.py code/M01_python/k13_task3_velocity_ned.py code/M01_python/k13_task4_wind_projection.py code/M01_python/k13_task5_test_frames.py
git commit -m "K13: vectors, NED/ENU frames, heading decomposition, wind projection, 14 tests"

Ожидаемый результат. Команда git status показывает чистое рабочее дерево по файлам k13_*, в истории появился коммит урока K13.

↑ К оглавлению

13. Проверьте себя

  1. Чем вектор отличается от скаляра? Приведите по два примера из телеметрии аппарата.
  2. Что фиксирует система координат и почему одни и те же числа в разных системах означают разное?
  3. Как с помощью правила правой руки проверить, что тройка осей — правая? Проверьте так NED и ENU.
  4. Назовите оси систем body, NED и ENU и скажите, какая экосистема какую систему использует.
  5. Сформулируйте правило перевода вектора из NED в ENU. Почему обратный перевод выполняется той же формулой?
  6. Аппарат снижается со скоростью 2 м/с. Какой знак у вертикальной компоненты в NED и в ENU?
  7. Как измеряется курс? Какие компоненты скорости NED получаются при курсе 0° и при курсе 90°?
  8. Зачем нужен перевод градусов в радианы и какая функция Python его выполняет?
  9. Что означает знак скалярного произведения вектора ветра и единичного вектора направления полёта?
  10. Почему в тестах математических функций используют pytest.approx, а не точное равенство? Ослабляет ли допуск тест?
↑ К оглавлению

14. Чек-лист самопроверки

  • Я установил numpy в виртуальное окружение командой python -m pip install numpy.
  • Я создал все пять файлов практики в каталоге code/M01_python.
  • Четыре демо-скрипта запускаются, и я могу объяснить каждую напечатанную строку.
  • Я могу записать по памяти правило перевода NED → ENU и объяснить, почему оно симметрично.
  • Я вывел компоненты скорости для курсов 0°, 90°, 180° и 270° без подсказки.
  • Я объясняю знак проекции ветра как попутный, встречный или боковой.
  • Команда python -m pytest k13_task5_test_frames.py -v завершается результатом 14 passed.
  • Я могу объяснить, почему точное сравнение дробных чисел ненадёжно и что такое отрицательный ноль.
  • Я сделал git commit с файлами урока K13.
Результат. Следующий урок — K14 «Углы Эйлера, кватернионы и матрицы ориентации»: полный пересчёт между связанной системой body и земной NED.
↑ К оглавлению

15. Что дальше

  • Предыдущий урок: K12 · Мини-проект CLI «Fake GCS» (гейт G1 сдан)
  • Текущий урок: K13 · Векторы и системы координат: body, NED, ENU
  • Следующий урок: K14 · Углы Эйлера, кватернионы и DCM
  • Модуль M2 не имеет собственного гейта; следующий гейт G2 сдаётся после лабораторной работы K22 в модуле M3 «Протоколы и интерфейсы».
↑ К оглавлению

 

 

Воскресенье, 11 октября 2026
k13. Математика дрона: векторы и системы координат — body, NED, ENU