k14. Математика дрона: углы Эйлера, кватернионы и DCM
UAV Python Pro · Конспект K14 · Модуль M2. Математика дрона: углы Эйлера, кватернионы и DCM · Редакция 1.0 · После CHECKPOINT-K13
На прошлом уроке мы остановились на границе: векторы и земные системы координат освоены, но полный пересчёт между связанной системой body и земной NED требует знания ориентации аппарата — того, как именно повёрнут его корпус. Этот урок посвящён трём способам записать ориентацию: углам Эйлера (крен, тангаж, рыскание), матрице поворота (DCM) и кватерниону. Вы увидите, что все три способа описывают один и тот же поворот, научитесь переводить один в другой каноническими формулами, обнаружите слабое место углов Эйлера — gimbal lock — и поймёте, почему автопилоты хранят ориентацию в кватернионах, а людям показывают углы.
Ориентация аппарата — это ответ на вопрос «как повёрнут корпус относительно Земли». Записать этот ответ числами можно тремя способами: тремя углами Эйлера, матрицей поворота из девяти чисел или кватернионом из четырёх чисел. Все три записи описывают один и тот же поворот, и между ними существуют точные формулы перехода. Углы понятны человеку, матрица удобна для пересчёта векторов, а кватернион не теряет степень свободы при вертикальном тангаже и дёшево складывает повороты — поэтому именно его используют внутри автопилотов и передают в сообщении ATTITUDE_QUATERNION.
Содержание
- 1. Цели урока
- 2. Словарь урока
- 3. Зачем нужна ориентация: мост между body и NED
- 4. Углы Эйлера: крен, тангаж, рыскание
- 5. Матрица поворота и DCM
- 6. Gimbal lock: потеря степени свободы
- 7. Кватернион: четыре числа вместо трёх углов
- 8. Когда не следует писать свою математику
- 9. Угловая скорость: как быстро меняется ориентация
- 10. Тесты математики ориентации
- 11. Типичные ошибки
- 12. Практика
- 13. Проверьте себя
- 14. Чек-лист самопроверки
- 15. Что дальше
1. Цели урока
После изучения этого урока вы будете уметь выполнять следующие действия.
- Вы объясните, что такое ориентация аппарата, и назовёте три способа её записать: углы Эйлера, матрица поворота, кватернион.
- Вы определите крен, тангаж и рыскание по осям связанной системы и проверите положительное направление по правилу правой руки из урока K13.
- Вы построите матрицу поворота по рысканию и полную матрицу
body → NEDпо трём углам с помощью функцииeuler_to_dcm(). - Вы проверите свойства матрицы поворота: ортонормированность через
np.dot(R, R.T), единичный определитель черезnp.linalg.det(), обратный поворот через транспонированиеR.T. - Вы продемонстрируете gimbal lock численно: две разные тройки углов при тангаже 90° дают одинаковую ориентацию.
- Вы переведёте углы Эйлера в кватернион и обратно каноническими формулами и объясните, зачем нужен
math.atan2()и защемление аргументаmath.asin(). - Вы напишете тесты математики ориентации с допусками
np.allclose()иpytest.approx.
ATTITUDE приносит углы Эйлера в радианах вместе с угловыми скоростями, а сообщение ATTITUDE_QUATERNION — ориентацию в кватернионе. Чтобы наклонить камеру на цель, рассчитать снос или просто нарисовать горизонт в своей наземной станции, вы будете читать эти поля и переводить их из одной формы в другую. Подробный разбор сообщений — в модуле M5, а математика для них готовится сейчас.2. Словарь урока
| Термин | Простыми словами |
|---|---|
| Ориентация (attitude) | Ответ на вопрос «как повёрнут корпус аппарата относительно Земли». |
| Крен (roll) | Наклон вбок вокруг продольной оси X: правое крыло вниз — положительный крен. |
| Тангаж (pitch) | Наклон носом вверх или вниз вокруг поперечной оси Y: нос вверх — положительный тангаж. |
| Рыскание (yaw) | Поворот носа вправо или влево вокруг вертикальной оси Z: нос вправо — положительное рыскание. |
| Углы Эйлера | Способ записать ориентацию тремя последовательными поворотами вокруг осей; в авиации — схема ZYX: рыскание, тангаж, крен. |
| Матрица поворота | Таблица из девяти чисел три на три, которая превращает координаты вектора в одной системе в координаты в другой. |
| DCM | Direction cosine matrix, матрица направляющих косинусов: то же, что матрица поворота; элементы — косинусы углов между осями двух систем. |
| Единичная матрица | Матрица с единицами по диагонали и нулями в остальных клетках: поворот «ничего не делать»; в numpy — np.eye(3). |
| Транспонирование | Отражение матрицы относительно диагонали: строки становятся столбцами. Для матрицы поворота это обратный поворот. |
| Определитель (детерминант) | Число-характеристика матрицы: у чистого поворота оно равно единице; минус единица означала бы зеркальное отражение. |
| Gimbal lock | Потеря одной степени свободы у углов Эйлера при тангаже ±90°: крен и рыскание начинают делать одно и то же. |
| Кватернион | Четыре числа (w, x, y, z) единичной длины, которые кодируют поворот «ось плюс угол» без потери степеней свободы. |
| atan2 | Арктангенс двух аргументов: возвращает угол в правильном квадранте, от −π до π; в Python — math.atan2(y, x). |
| Угловая скорость | Как быстро меняется ориентация: число радианов в секунду вокруг каждой оси связанной системы. |
3. Зачем нужна ориентация: мост между body и NED
Вспомним вывод урока K13. Датчики аппарата — акселерометр и гироскоп — живут в связанной системе body: они измеряют ускорения и скорости вращения вдоль осей корпуса и «не знают», куда корпус смотрит относительно Земли. Позиция, скорость и маршрут живут в земной системе NED. Между этими двумя мирами нужен мост, и строится он из ориентации: зная, как повёрнут корпус, можно пересчитать любой вектор из связанной системы в земнюю и обратно.
Пример из практики: акселерометр в ровном висении показывает ускорение 9,81 м/с² вдоль своей оси Z — «вниз по корпусу». Если аппарат наклонён, часть этого числа «проецируется» на продольную ось, и наивный код решит, что дрон разгоняется вперёд. Автопилот не ошибается именно потому, что знает свою ориентацию и поворачивает вектор из body в NED до того, как делать выводы. Тот же пересчёт понадобится вам, например, чтобы наклонить подвес камеры на цель: цель известна в земных координатах, а моторы подвеса работают в осях корпуса.
Ориентацию можно записать тремя способами, и этот урок разбирает все три: углы Эйлера (понятны человеку), матрица поворота (удобна для пересчёта векторов) и кватернион (надёжен для хранения и сложения поворотов). Профессионал свободно выбирает нужный способ и не путается между ними.
↑ К оглавлению4. Углы Эйлера: крен, тангаж, рыскание
Ориентация аппарата в авиации традиционно описывается тремя углами Эйлера — тремя углами последовательного поворота, которые переводят корпус из «ровного» положения в текущее. Каждый угол вращает аппарат вокруг своей оси связанной системы: крен (roll) наклоняет аппарат вбок вокруг продольной оси X, тангаж (pitch) поднимает или опускает нос вокруг поперечной оси Y, а рыскание (yaw) поворачивает нос вправо или влево вокруг вертикальной оси Z.
Крен (roll, φ) — вокруг оси X «вперёд»:
правое крыло вниз — положительный крен.
Тангаж (pitch, θ) — вокруг оси Y «вправо»:
нос вверх — положительный тангаж.
Рыскание (yaw, ψ) — вокруг оси Z «вниз»:
нос вправо, то есть по часовой стрелке при взгляде сверху, —
положительное рыскание.Знаки направлений следуют прямо из правила правой руки, которое вы освоили на уроке K13: сожмите правую руку так, чтобы отставленный большой палец смотрел вдоль положительного направления оси, — согнутые пальцы покажут положительное направление вращения вокруг этой оси. Для оси X «вперёд» это наклон правого борта вниз, для оси Y «вправо» — подъём носа, для оси Z «вниз» — поворот носа вправо.
Вторая важная договорённость — порядок поворотов. В авиационной схеме, которую используют ArduPilot и PX4, повороты применяются в порядке ZYX: сначала рыскание вокруг вертикальной оси, затем тангаж вокруг новой поперечной оси, затем крен вокруг новой продольной оси. Порядок принципиален, потому что повороты вокруг разных осей не переставляемы: «поднять нос на 90°, затем повернуть направо 90°» и «повернуть направо 90°, затем поднять нос на 90°» приводят аппарат в разные положения. Схема записи углов без указания порядка — неполная схема, и это частая причина расхождений между библиотеками.
Диапазоны углов: крен и рыскание обычно живут в пределах от −180° до +180°, а тангаж ограничен диапазоном от −90° до +90°. Именно эта ограниченность тангажа станет источником проблемы, которую разбирает раздел 6. Сообщение MAVLink ATTITUDE отдаёт поля roll, pitch и yaw в радианах; для человека их переводят в градусы функцией math.degrees(). Справочное описание сообщения доступно по адресу https://mavlink.io/en/messages/common.html (раздел ATTITUDE).
5. Матрица поворота и DCM
Матрица поворота — это таблица из девяти чисел, расположенных три на три, которая превращает координаты вектора в одной системе координат в координаты того же вектора в другой системе. В numpy матрица создаётся функцией np.array() из списка списков, а умножение матрицы на вектор выполняет уже знакомая функция np.dot():
\[ \vec{v}_{\mathrm{NED}} = R \cdot \vec{v}_{\mathrm{body}} \]
Словами: чтобы получить земные координаты вектора, нужно умножить матрицу поворота на столбец его связанных координат. Начнём с самого простого поворота — только рыскание, аппарат летит «плоско», без крена и тангажа. Такую матрицу вы фактически уже видели на уроке K13, когда раскладывали скорость по курсу:
\[ R_z(\psi) = \begin{pmatrix} \cos\psi & -\sin\psi & 0 \\ \sin\psi & \cos\psi & 0 \\ 0 & 0 & 1 \end{pmatrix} \]
Чистый код
# Пример: поворот только по рысканию (фрагмент k14_task1_yaw_rotation.py)
import math
import numpy as np
def yaw_rotation_matrix(yaw_rad: float):
c = math.cos(yaw_rad)
s = math.sin(yaw_rad)
return np.array([
[c, -s, 0.0],
[s, c, 0.0],
[0.0, 0.0, 1.0],
])
def rotate_body_to_ned(v_body, yaw_rad: float):
matrix = yaw_rotation_matrix(yaw_rad)
return np.dot(matrix, np.asarray(v_body, dtype=float))- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
import math import numpy as np # → math даёт синус и косинус, numpy — массивы и умножение матриц. def yaw_rotation_matrix(yaw_rad: float): # → Вход — угол рыскания в радианах; выход — матрица три на три. c = math.cos(yaw_rad) s = math.sin(yaw_rad) # → Короткие имена c и s: каждая пара значений используется в матрице трижды. return np.array([ [c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0], ]) # → Список списков становится двумерным массивом numpy — матрицей. # → Третья строка (0, 0, 1) означает: вертикальная компонента не меняется — # → поворот идёт строго вокруг вертикальной оси. def rotate_body_to_ned(v_body, yaw_rad: float): # → Поворот произвольного вектора из body в NED. matrix = yaw_rotation_matrix(yaw_rad) return np.dot(matrix, np.asarray(v_body, dtype=float)) # → np.dot(матрица, вектор) выполняет умножение матрицы на столбец: # → каждая строка матрицы умножается на вектор и даёт одну новую компоненту.
Проверьте результат мысленно: при курсе 90° «нос» аппарата — вектор (1, 0, 0) в связанной системе — должен смотреть на восток, то есть превратиться в (0, 1, 0) в NED. Демо практики печатает ровно это.
Полная матрица ориентации по трём углам называется DCM — direction cosine matrix, «матрица направляющих косинусов». Имя объясняет устройство таблицы: каждый её элемент равен косинусу угла между соответствующей осью связанной системы и осью земной. Каноническая формула для схемы ZYX (источник: https://en.wikipedia.org/wiki/Rotation_matrix) с обозначениями \(s_\phi = \sin(\text{крен})\), \(c_\phi = \cos(\text{крен})\), \(s_\theta = \sin(\text{тангаж})\), \(c_\theta = \cos(\text{тангаж})\), \(s_\psi = \sin(\text{рыскание})\), \(c_\psi = \cos(\text{рыскание})\):
\[ R_{\mathrm{body} \to \mathrm{NED}} = \begin{pmatrix} c_\theta c_\psi & s_\phi s_\theta c_\psi - c_\phi s_\psi & c_\phi s_\theta c_\psi + s_\phi s_\psi \\ c_\theta s_\psi & s_\phi s_\theta s_\psi + c_\phi c_\psi & c_\phi s_\theta s_\psi - s_\phi c_\psi \\ -s_\theta & s_\phi c_\theta & c_\phi c_\theta \end{pmatrix} \]
Выводить эту формулу вручную мы не будем — это занятие учебника по математике полёта, а не курса интеграции. Наша задача другая: взять каноническую формулу из надёжного источника, вставить в код и проверить её свойствами, о которых сказано ниже. Именно так поступают на практике; к этому принципу мы вернёмся в разделе 8.
Чистый код
# Пример: полная матрица DCM по трём углам (фрагмент k14_task2_euler_dcm.py)
import math
import numpy as np
def euler_to_dcm(roll_rad: float, pitch_rad: float, yaw_rad: float):
cr, sr = math.cos(roll_rad), math.sin(roll_rad)
cp, sp = math.cos(pitch_rad), math.sin(pitch_rad)
cy, sy = math.cos(yaw_rad), math.sin(yaw_rad)
return np.array([
[cp * cy, sr * sp * cy - cr * sy, cr * sp * cy + sr * sy],
[cp * sy, sr * sp * sy + cr * cy, cr * sp * sy - sr * cy],
[-sp, sr * cp, cr * cp],
])
def ned_to_body_matrix(roll_rad: float, pitch_rad: float, yaw_rad: float):
return euler_to_dcm(roll_rad, pitch_rad, yaw_rad).T- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
def euler_to_dcm(roll_rad: float, pitch_rad: float, yaw_rad: float): # → Все три угла подаются в радианах: функция не угадывает единицы за вызывающего. cr, sr = math.cos(roll_rad), math.sin(roll_rad) cp, sp = math.cos(pitch_rad), math.sin(pitch_rad) cy, sy = math.cos(yaw_rad), math.sin(yaw_rad) # → Шесть коротких имён: c — косинус, s — синус; # → r — roll (крен), p — pitch (тангаж), y — yaw (рыскание). # → Каждое значение входит в формулу несколько раз, поэтому # → их вычисляют один раз и переиспользуют. return np.array([ [cp * cy, sr * sp * cy - cr * sy, cr * sp * cy + sr * sy], [cp * sy, sr * sp * sy + cr * cy, cr * sp * sy - sr * cy], [-sp, sr * cp, cr * cp], ]) # → Таблица три на три — дословная запись канонической формулы ZYX. # → Сверьте её с формулой выше элемент за элементом: это и есть # → «перенос формулы из источника в код без изобретательства». def ned_to_body_matrix(roll_rad: float, pitch_rad: float, yaw_rad: float): return euler_to_dcm(roll_rad, pitch_rad, yaw_rad).T # → Обратный поворот NED -> body не нужно выводить заново: # → атрибут .T транспонирует матрицу (строки становятся столбцами), # → а для матрицы поворота транспонированная матрица и есть обратная.
У матрицы поворота есть три свойства, которые превращают её в самопроверяемую структуру. Первое: она ортонормирована — произведение матрицы на её транспонированную версию даёт единичную матрицу (в numpy — np.eye(3), единицы по диагонали, нули в остальных клетках). Второе: её определитель (функция np.linalg.det()) равен единице; определитель минус единица означал бы, что в формулу закралось зеркальное отражение. Третье: обратный поворот — это транспонирование: столбцы и строки меняются местами, и обратная матрица достаётся бесплатно, без сложного обращения. Тесты практики проверяют все три свойства на «живой» матрице — это гораздо надёжнее, чем однажды сверить числа глазами.
6. Gimbal lock: потеря степени свободы
У углов Эйлера есть врождённый дефект, и понять его помогает механическая картинка. Представьте карданный подвес — конструкцию из трёх вложенных колец на осях, как в гироскопе или в подвесе съёмочной камеры: внешнее кольцо вращается вокруг вертикальной оси (рыскание), среднее — вокруг поперечной (тангаж), внутреннее — вокруг продольной (крен). Пока кольца разведены, три двигателя по одному на кольцо дают аппарату внутри полную свободу ориентации.
Карданный подвес из трёх рамок:
внешняя рамка — рыскание (ось Z, вертикаль)
среднее кольцо — тангаж (ось Y, поперечная)
внутреннее кольцо — крен (ось X, продольная)
Если поднять среднее кольцо ровно на 90 град,
оси внешнего и внутреннего колец совпадают:
два разных двигателя крутят аппарат вокруг одной
и той же оси — одна степень свободы потеряна.Этот эффект и называется gimbal lock (дословно — «блокировка подвеса»). На языке чисел: при тангаже ровно ±90° ориентация перестаёт зависеть от крена и рыскания по отдельности и начинает зависеть только от их разности. Два разных набора углов описывают одно и то же положение корпуса — представление перестаёт быть однозначным. Описание эффекта с иллюстрациями доступно по адресу https://en.wikipedia.org/wiki/Gimbal_lock.
Проверим это численно — именно так, как инженер проверяет подозрение: не рассуждением, а экспериментом.
Чистый код
# Пример: численная демонстрация gimbal lock (фрагмент k14_task4_gimbal_lock_demo.py)
import math
import numpy as np
from k14_task2_euler_dcm import euler_to_dcm
dcm_a = euler_to_dcm(math.radians(30.0), math.radians(20.0), math.radians(10.0))
dcm_b = euler_to_dcm(math.radians(50.0), math.radians(20.0), math.radians(30.0))
print(np.allclose(dcm_a, dcm_b)) # False: тангаж 20 град, всё различимо
dcm_c = euler_to_dcm(math.radians(30.0), math.radians(90.0), math.radians(10.0))
dcm_d = euler_to_dcm(math.radians(50.0), math.radians(90.0), math.radians(30.0))
print(np.allclose(dcm_c, dcm_d)) # True: тангаж 90 град, степень свободы потеряна- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
import math import numpy as np from k14_task2_euler_dcm import euler_to_dcm # → Берём готовую функцию DCM из задачи 14.2 — модули работают вместе. dcm_a = euler_to_dcm(math.radians(30.0), math.radians(20.0), math.radians(10.0)) dcm_b = euler_to_dcm(math.radians(50.0), math.radians(20.0), math.radians(30.0)) # → Две ориентации при обычном тангаже 20 град. # → Крен и рыскание увеличились одинаково на 20 град. print(np.allclose(dcm_a, dcm_b)) # False: тангаж 20 град, всё различимо # → np.allclose сравнивает две матрицы с допуском (урок K13): # → при нормальном тангаже ориентации разные — False. dcm_c = euler_to_dcm(math.radians(30.0), math.radians(90.0), math.radians(10.0)) dcm_d = euler_to_dcm(math.radians(50.0), math.radians(90.0), math.radians(30.0)) # → Тот же сдвиг «крен +20, рыскание +20», но тангаж ровно 90 град: # → нос смотрит вертикально вверх. print(np.allclose(dcm_c, dcm_d)) # True: степень свободы потеряна # → Матрицы совпали: две разные тройки углов описывают одно положение. # → Различить крен 30 с рысканием 10 и крен 50 с рысканием 30 # → по ориентации корпуса невозможно — обе дают разность 20 град.
Почему это важно для беспилотника, а не только для учебника? Во-первых, около вертикального положения перестают быть однозначными сами числа углов: алгоритм, который по матрице восстанавливает крен и рыскание, начнёт выдавать скачущие значения при почти неподвижном аппарате. Во-вторых, плавное изменение ориентации (интерполяция) вблизи блокировки ломается. Для аппаратов вертикального взлёта, включая наш целевой Matek F405 VTOL в квадопланной конфигурации, большие углы тангажа — рабочий режим переходных фаз, поэтому проблема касается нас напрямую.
7. Кватернион: четыре числа вместо трёх углов
Кватернион ориентации — это четыре числа, которые записываются как (w, x, y, z) и образуют вектор единичной длины. Полная математика кватернионов выходит за рамки курса и не нужна для работы: достаточно твёрдо усвоить, что кватернион кодирует поворот как «ось вращения плюс угол вокруг неё», что его норма (длина) у корректной ориентации равна единице и что между кватернионом, углами Эйлера и матрицей DCM существуют точные формулы перехода.
Два главных преимущества кватерниона вытекают из его устройства. Первое: у него нет gimbal lock — все ориентации, включая вертикальные, представляются одинаково хорошо. Второе: два последовательных поворота складываются простым перемножением кватернионов, а численная погрешность легко чинится нормированием — делением четырёх компонент на их общую длину. Именно поэтому автопилот ведёт ориентацию в кватернионах, а MAVLink, помимо ATTITUDE, предлагает сообщение ATTITUDE_QUATERNION.
Формула перехода от углов Эйлера (схема ZYX) к кватерниону — каноническая (источник: https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles). В ней участвуют синусы и косинусы половин углов; для краткости обозначим \(c_r = \cos(\phi/2)\), \(s_r = \sin(\phi/2)\) и так же для тангажа (p) и рыскания (y):
\[ \begin{aligned} w &= c_r c_p c_y + s_r s_p s_y \\ x &= s_r c_p c_y - c_r s_p s_y \\ y &= c_r s_p c_y + s_r c_p s_y \\ z &= c_r c_p s_y - s_r s_p c_y \end{aligned} \]
Чистый код
# Пример: углы Эйлера -> кватернион (фрагмент k14_task3_quaternion.py)
import math
def euler_to_quaternion(roll_rad: float, pitch_rad: float, yaw_rad: float):
cr, sr = math.cos(roll_rad / 2.0), math.sin(roll_rad / 2.0)
cp, sp = math.cos(pitch_rad / 2.0), math.sin(pitch_rad / 2.0)
cy, sy = math.cos(yaw_rad / 2.0), math.sin(yaw_rad / 2.0)
w = cr * cp * cy + sr * sp * sy
x = sr * cp * cy - cr * sp * sy
y = cr * sp * cy + sr * cp * sy
z = cr * cp * sy - sr * sp * cy
return (w, x, y, z)- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
import math def euler_to_quaternion(roll_rad: float, pitch_rad: float, yaw_rad: float): # → Вход — три угла в радианах, выход — кортеж из четырёх чисел (w, x, y, z). cr, sr = math.cos(roll_rad / 2.0), math.sin(roll_rad / 2.0) cp, sp = math.cos(pitch_rad / 2.0), math.sin(pitch_rad / 2.0) cy, sy = math.cos(yaw_rad / 2.0), math.sin(yaw_rad / 2.0) # → Ключевая деталь формулы: берутся половинные углы. # → Кватернион «живёт» в половине угла — это факт, а не приём упрощения. w = cr * cp * cy + sr * sp * sy x = sr * cp * cy - cr * sp * sy y = cr * sp * cy + sr * cp * sy z = cr * cp * sy - sr * sp * cy # → Четыре строки — дословная каноническая формула из источника. # → Компонента w близка к единице при малых углах и убывает # → по мере роста поворота: это «косинус половины угла». return (w, x, y, z) # → Порядок компонент фиксируем явно: сначала w, затем x, y, z. # → Другие библиотеки иногда хранят порядок (x, y, z, w) — # → перепутанный порядок является классическим источником ошибок.
Обратный переход — от кватерниона к углам — использует две новые функции модуля math. Первая: atan2 — арктангенс двух аргументов, math.atan2(y, x). Обычный арктангенс видит только отношение двух чисел и потому путает противоположные квадранты; atan2 получает оба числа по отдельности, учитывает знаки и возвращает угол в полном диапазоне от −π до π. Вторая: math.asin() — арксинус, определённый только для аргументов от −1 до 1. Из-за погрешности округления значение, математически равное единице, может прийти как 1.0000000000000002, и тогда asin поднимет ValueError. Поэтому аргумент предварительно «защемляют» в допустимых границах.
\[ \phi = \operatorname{atan2}\bigl(2(wx + yz),\; 1 - 2(x^2 + y^2)\bigr) \]
\[ \theta = \arcsin\bigl(\operatorname{clip}(2(wy - zx),\; -1,\; 1)\bigr) \]
\[ \psi = \operatorname{atan2}\bigl(2(wz + xy),\; 1 - 2(y^2 + z^2)\bigr) \]
Чистый код
# Пример: кватернион -> углы Эйлера (фрагмент k14_task3_quaternion.py)
import math
def quaternion_to_euler(q):
w, x, y, z = (float(c) for c in q)
roll = math.atan2(2.0 * (w * x + y * z), 1.0 - 2.0 * (x * x + y * y))
sin_pitch = 2.0 * (w * y - z * x)
sin_pitch = max(-1.0, min(1.0, sin_pitch))
pitch = math.asin(sin_pitch)
yaw = math.atan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z))
return (roll, pitch, yaw)- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
import math def quaternion_to_euler(q): # → Вход — кватернион (w, x, y, z), выход — три угла в радианах. w, x, y, z = (float(c) for c in q) # → Распаковка четырёх компонент с приведением к float: # → генераторное выражение применяет float() к каждому элементу. roll = math.atan2(2.0 * (w * x + y * z), 1.0 - 2.0 * (x * x + y * y)) # → Крен: atan2 от двух выражений канонической формулы. # → Два аргумента вместо одного отношения — гарантия правильного квадранта. sin_pitch = 2.0 * (w * y - z * x) # → Синус тангажа из той же канонической формулы. sin_pitch = max(-1.0, min(1.0, sin_pitch)) # → Защемление (clip): min не пускает значение выше 1, # → max не пускает ниже −1. Без этой строки asin однажды упадёт # → с ValueError: math domain error из-за погрешности в 17-м знаке. pitch = math.asin(sin_pitch) # → Арксинус возвращает угол в диапазоне от −90 до +90 град — # → ровно законный диапазон тангажа. yaw = math.atan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z)) # → Рыскание: снова atan2, полный круг без разрывов. return (roll, pitch, yaw) # → Результат в радианах; для печати в градусах вызывающий код # → применяет math.degrees().
quaternion_to_euler() по-прежнему корректна как математика, но сами углы перестают быть устойчивыми: крен и рыскание могут прыгать, оставаясь в рамках своей разности. Это не ошибка формулы, а свойство углового представления — тот самый gimbal lock. Кватернион при этом остаётся плавным и однозначным.8. Когда не следует писать свою математику
Этот раздел короткий, но профессионально важный. Математика ориентации выведена, перепроверена и опубликована десятилетия назад; десятки библиотек реализуют её с тестами и исправленными краевыми случаями. Самостоятельно «улучшать» формулы — значит гарантированно получить трудноуловимую ошибку знака или порядка осей, которая проявится не на столе, а в полёте. Поэтому в отрасли принята схема работы, которую мы воспроизвели в этом уроке и которую вам стоит сделать своей привычкой.
- Берите формулу из надёжного источника и сохраняйте ссылку в комментарии к коду — так сделано в файлах
k14_task2_euler_dcm.pyиk14_task3_quaternion.py. - Явно фиксируйте соглашения: схема углов (ZYX), порядок компонент кватерниона (w, x, y, z), единицы (радианы), системы координат (body, NED).
- Покрывайте реализацию тестами свойств: ортонормированность матрицы, единичный определитель, единичная норма кватерниона, круговые переводы «туда и обратно».
- Когда проект вырастет из учебного, переходите на проверенные реализации: в экосистеме MAVLink помощники живут в модуле
mavextraбиблиотекиpymavlink(модуль M5), в научных проектах — в классеscipy.spatial.transform.Rotation. Ставить их сейчас не нужно: урок сознательно проходит по формулам один раз, чтобы вы понимали, что именно делают библиотеки.
9. Угловая скорость: как быстро меняется ориентация
Рядом с углами ориентации в телеметрии всегда живёт вторая величина — угловая скорость: как быстро аппарат вращается вокруг каждой из осей связанной системы. Её измеряет гироскоп, и MAVLink отдаёт её в полях rollspeed, pitchspeed и yawspeed сообщения ATTITUDE — в радианах в секунду. Не путайте две величины: ориентация отвечает на вопрос «куда смотрит корпус», угловая скорость — «как быстро он поворачивается». В ровном висении углы почти постоянны, а угловые скорости дрожат вокруг нуля; в резком развороте скорость рыскания велика, а сам угол рыскания меняется плавно.
Связь между величинами прямая: ориентация — это, грубо говоря, накопленный итог угловой скорости (математически — её интеграл по времени). Автопилот вычисляет ориентацию сотни раз в секунду, смешивая показания гироскопа, акселерометра, барометра и спутников в фильтре, который называется EKF (Extended Kalman Filter). Внутреннее устройство фильтра — большая отдельная тема за рамками курса; нам достаточно знать, что на выходе EKF лежат те самые ориентации в углах и кватернионах, которые мы научились читать и переводить, а угловая скорость используется в фильтре как главный источник «быстрой» информации о вращении.
↑ К оглавлению10. Тесты математики ориентации
Тесты этого урока продолжают идею K13: математические функции проверяются с допусками (np.allclose() для матриц и массивов, pytest.approx для чисел и кортежей), а вместо «сверил числа глазами» проверяются свойства и круговые переводы. Перечислим четыре класса проверок, которые используются в файле k14_task5_test_attitude.py.
- Опорные случаи: при рыскании 90° вектор «нос» обязан превратиться из северного в восточный; при нулевых углах матрица обязана быть единичной.
- Свойства структуры: произведение матрицы на транспонированную равно единичной матрице, определитель равен единице — независимо от конкретных углов.
- Круговые переводы: углы → кватернион → углы обязаны вернуть исходные значения; поворот вперёд и обратно через транспонированную матрицу обязан вернуть исходный вектор.
- Вырожденный случай: при тангаже 90° две разные тройки углов обязаны дать одинаковую матрицу — тест фиксирует gimbal lock как ожидаемое поведение формулы, а не как сюрприз.
Чистый код
# Пример: тесты свойств DCM и кругового перевода (фрагмент k14_task5_test_attitude.py)
import math
import numpy as np
import pytest
from k14_task2_euler_dcm import euler_to_dcm
from k14_task3_quaternion import euler_to_quaternion, quaternion_to_euler
def test_dcm_is_orthonormal():
matrix = euler_to_dcm(math.radians(10.0), math.radians(-5.0), math.radians(30.0))
assert np.allclose(np.dot(matrix, matrix.T), np.eye(3))
def test_euler_quaternion_round_trip():
angles = (math.radians(10.0), math.radians(-5.0), math.radians(30.0))
q = euler_to_quaternion(*angles)
back = quaternion_to_euler(q)
assert back == pytest.approx(angles)- Построчный разбор
Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.
import math import numpy as np import pytest from k14_task2_euler_dcm import euler_to_dcm from k14_task3_quaternion import euler_to_quaternion, quaternion_to_euler # → Тесты импортируют функции из модулей практики — # → стандартная схема урока K11: логика отдельно, проверки отдельно. def test_dcm_is_orthonormal(): matrix = euler_to_dcm(math.radians(10.0), math.radians(-5.0), math.radians(30.0)) assert np.allclose(np.dot(matrix, matrix.T), np.eye(3)) # → Свойство, а не конкретные числа: для любых углов # → R · Rᵀ обязана давать единичную матрицу np.eye(3). # → np.allclose сравнивает матрицы поэлементно с допуском. def test_euler_quaternion_round_trip(): angles = (math.radians(10.0), math.radians(-5.0), math.radians(30.0)) q = euler_to_quaternion(*angles) back = quaternion_to_euler(q) assert back == pytest.approx(angles) # → Круговой перевод: звёздочка в вызове распаковывает кортеж # → в три отдельных аргумента. Исходные и возвращённые углы # → обязаны совпасть с точностью до допуска pytest.approx.
11. Типичные ошибки
| Ошибка | Что происходит | Как следует рассуждать |
|---|---|---|
Градусы поданы в тригонометрию без перевода или результат atan2 напечатан как градусы | Числа выглядят совершенно правдоподобно, но неверны по величине | Внутри математики — всегда радианы; на границе с человеком — math.radians() туда и math.degrees() обратно |
| Порядок компонент кватерниона перепутан: (w, x, y, z) вместо (x, y, z, w) | Ориентация «шумит», углы скачут, аппарат в симуляторе ведёт себя неадекватно | Порядок фиксируйте в имени и комментарии; при стыковке библиотек сверяйте документацию каждой |
| Хранение и интегрирование ориентации в углах Эйлера | Вблизи тангажа ±90° значения скачут, плавность теряется — gimbal lock | Внутри — кватернион или матрица; углы — только для отображения и пороговой логики |
math.asin() вызван без защемления аргумента | Рано или поздно ValueError: math domain error из-за погрешности в 17-м знаке | Перед asin зажимайте значение в границы от −1 до 1 через max/min |
| Своя «улучшенная» формула поворота вместо канонической | Тонкая ошибка знака или порядка осей, которая всплывает только в полёте | Формула — из источника со ссылкой в комментарии; корректность — через тесты свойств |
Проверка матриц на точное равенство (==) | Тест падает из-за округления в последних знаках | Для массивов — np.allclose(), для чисел и кортежей — pytest.approx |
| Схема углов не указана: ZYX перепутана с XYZ | Ориентация расходится с автопилотом на больших углах | Схему записывайте в docstring функции; ArduPilot и PX4 используют ZYX |
12. Практика
Практика урока состоит из пяти файлов: четыре модуля математики ориентации и один файл тестов. Порядок работы: создайте файлы задач 14.1–14.5 в каталоге code/M01_python, выполните прогоны из задачи 14.6 и сохраните исходники в Git (задача 14.7). Библиотека numpy уже установлена в уроке K13 — новых пакетов этот урок не требует. В карточках в аккордеонах лежит полный текст каждого файла; он совпадает с содержимым репозитория курса.
Задача 14.1. Поворот по рысканию
Файл практики: k14_task1_yaw_rotation.py.
Создайте модуль простейшего поворота: функция yaw_rotation_matrix() строит матрицу поворота вокруг вертикальной оси, а функция rotate_body_to_ned() умножает её на вектор через np.dot(). Демо поворачивает «нос» и «правое крыло» аппарата на курсы 0°, 90° и 180°.
Ожидаемый результат. Демо печатает для курса 90° нос в NED [0. 1. 0.] (восток) и правый борт в NED [-1. 0. 0.] (юг).
- Полный текст файла k14_task1_yaw_rotation.py
Скопируйте приведённое ниже содержимое в файл
code/M01_python/k14_task1_yaw_rotation.pyв проекте PyCharm (виртуальное окружение) и запустите его из каталогаcode/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.# k14_task1_yaw_rotation.py # Цель: поворот вокруг вертикальной оси (только рыскание) — # первый шаг от векторов K13 к полной ориентации K14. # Запуск из каталога code/M01_python: # python k14_task1_yaw_rotation.py import math import numpy as np def yaw_rotation_matrix(yaw_rad: float): """Матрица поворота body -> NED вокруг вертикальной оси на угол yaw. Крен и тангаж предполагаются нулевыми: аппарат летит «плоско». """ c = math.cos(yaw_rad) s = math.sin(yaw_rad) return np.array([ [c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0], ]) def rotate_body_to_ned(v_body, yaw_rad: float): """Повернуть вектор из связанной системы в NED (только рыскание).""" matrix = yaw_rotation_matrix(yaw_rad) return np.dot(matrix, np.asarray(v_body, dtype=float)) if __name__ == "__main__": forward_body = (1.0, 0.0, 0.0) # «нос» аппарата в связанной системе right_body = (0.0, 1.0, 0.0) # «правое крыло» в связанной системе for yaw_deg in (0.0, 90.0, 180.0): yaw_rad = math.radians(yaw_deg) nose = rotate_body_to_ned(forward_body, yaw_rad) wing = rotate_body_to_ned(right_body, yaw_rad) print( f"курс {yaw_deg:5.1f} град: нос в NED {np.round(nose, 3)}, " f"правый борт в NED {np.round(wing, 3)}" )
Задача 14.2. Полная матрица DCM
Файл практики: k14_task2_euler_dcm.py.
Создайте модуль полной ориентации: функция euler_to_dcm() реализует каноническую формулу ZYX со ссылкой на источник в комментарии, а ned_to_body_matrix() возвращает обратный поворот через транспонирование .T. Демо проверяет свойства: ортонормированность и единичный определитель.
Ожидаемый результат. Демо печатает определитель 1.000000, а произведение R @ R.T — единичную матрицу с единицами по диагонали.
- Полный текст файла k14_task2_euler_dcm.py
Скопируйте приведённое ниже содержимое в файл
code/M01_python/k14_task2_euler_dcm.pyв проекте PyCharm (виртуальное окружение) и запустите его из каталогаcode/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.# k14_task2_euler_dcm.py # Цель: полная матрица ориентации body -> NED по углам Эйлера (схема ZYX). # Формула каноническая, источник: # https://en.wikipedia.org/wiki/Rotation_matrix # Запуск из каталога code/M01_python: # python k14_task2_euler_dcm.py import math import numpy as np def euler_to_dcm(roll_rad: float, pitch_rad: float, yaw_rad: float): """Матрица поворота из связанной системы body в земную NED (схема ZYX). Углы в радианах: крен вокруг оси X, тангаж вокруг оси Y, рыскание вокруг оси Z. """ cr, sr = math.cos(roll_rad), math.sin(roll_rad) cp, sp = math.cos(pitch_rad), math.sin(pitch_rad) cy, sy = math.cos(yaw_rad), math.sin(yaw_rad) return np.array([ [cp * cy, sr * sp * cy - cr * sy, cr * sp * cy + sr * sy], [cp * sy, sr * sp * sy + cr * cy, cr * sp * sy - sr * cy], [-sp, sr * cp, cr * cp], ]) def ned_to_body_matrix(roll_rad: float, pitch_rad: float, yaw_rad: float): """Обратный поворот NED -> body: для матрицы поворота это транспонирование.""" return euler_to_dcm(roll_rad, pitch_rad, yaw_rad).T if __name__ == "__main__": roll, pitch, yaw = math.radians(10.0), math.radians(-5.0), math.radians(30.0) matrix = euler_to_dcm(roll, pitch, yaw) print("матрица body -> NED (крен 10, тангаж -5, рыскание 30 град):") print(np.round(matrix, 4)) print("произведение R @ R.T (для чистого поворота это единичная матрица):") print(np.round(np.dot(matrix, matrix.T), 4)) print(f"определитель: {np.linalg.det(matrix):.6f} (ожидается 1)") v_body = np.array([9.81, 0.0, 0.0]) print(f"вектор {v_body} в body -> NED: {np.round(np.dot(matrix, v_body), 4)}")
Задача 14.3. Кватернион
Файл практики: k14_task3_quaternion.py.
Создайте модуль кватерниона: канонический перевод euler_to_quaternion(), норма quaternion_norm() и обратный перевод quaternion_to_euler() с math.atan2() и защемлением аргумента math.asin(). Демо выполняет круговой перевод углов через кватернион и обратно.
Ожидаемый результат. Демо печатает норму 1.000000 и возвращает исходные углы (10.0, -5.0, 30.0) после кругового перевода.
- Полный текст файла k14_task3_quaternion.py
Скопируйте приведённое ниже содержимое в файл
code/M01_python/k14_task3_quaternion.pyв проекте PyCharm (виртуальное окружение) и запустите его из каталогаcode/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.# k14_task3_quaternion.py # Цель: кватернион ориентации — хранение без потери степени свободы (gimbal lock). # Канонические формулы (схема ZYX, порядок компонент w, x, y, z): # https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles # Запуск из каталога code/M01_python: # python k14_task3_quaternion.py import math import numpy as np def euler_to_quaternion(roll_rad: float, pitch_rad: float, yaw_rad: float): """Углы Эйлера (схема ZYX) -> единичный кватернион (w, x, y, z).""" cr, sr = math.cos(roll_rad / 2.0), math.sin(roll_rad / 2.0) cp, sp = math.cos(pitch_rad / 2.0), math.sin(pitch_rad / 2.0) cy, sy = math.cos(yaw_rad / 2.0), math.sin(yaw_rad / 2.0) w = cr * cp * cy + sr * sp * sy x = sr * cp * cy - cr * sp * sy y = cr * sp * cy + sr * cp * sy z = cr * cp * sy - sr * sp * cy return (w, x, y, z) def quaternion_norm(q) -> float: """Норма кватерниона: у кватерниона ориентации она равна единице.""" return float(np.linalg.norm(np.asarray(q, dtype=float))) def quaternion_to_euler(q): """Кватернион (w, x, y, z) -> углы Эйлера (roll, pitch, yaw) в радианах.""" w, x, y, z = (float(c) for c in q) roll = math.atan2(2.0 * (w * x + y * z), 1.0 - 2.0 * (x * x + y * y)) sin_pitch = 2.0 * (w * y - z * x) sin_pitch = max(-1.0, min(1.0, sin_pitch)) # защита от выхода за границы asin pitch = math.asin(sin_pitch) yaw = math.atan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z)) return (roll, pitch, yaw) if __name__ == "__main__": angles_deg = (10.0, -5.0, 30.0) angles_rad = tuple(math.radians(a) for a in angles_deg) q = euler_to_quaternion(*angles_rad) print(f"углы (град): {angles_deg}") print(f"кватернион (w, x, y, z): {tuple(round(c, 4) for c in q)}") print(f"норма кватерниона: {quaternion_norm(q):.6f} (ожидается 1)") back_rad = quaternion_to_euler(q) back_deg = tuple(round(math.degrees(a), 4) for a in back_rad) print(f"обратно в углы (град): {back_deg}")
Задача 14.4. Демо gimbal lock
Файл практики: k14_task4_gimbal_lock_demo.py.
Создайте модуль численной демонстрации gimbal lock: функция orientation_case() возвращает матрицу и кватернион для тройки углов, а демо сравнивает две разные тройки при тангаже 20° и при тангаже 90° с помощью np.allclose().
Ожидаемый результат. Демо печатает False при тангаже 20° и True для матриц и кватернионов при тангаже 90°: две разные тройки углов описывают одну ориентацию.
- Полный текст файла k14_task4_gimbal_lock_demo.py
Скопируйте приведённое ниже содержимое в файл
code/M01_python/k14_task4_gimbal_lock_demo.pyв проекте PyCharm (виртуальное окружение) и запустите его из каталогаcode/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.# k14_task4_gimbal_lock_demo.py # Цель: численно показать gimbal lock — потерю одной степени свободы # при тангаже +90 градусов (нос аппарата смотрит вертикально вверх). # Запуск из каталога code/M01_python: # python k14_task4_gimbal_lock_demo.py import math import numpy as np from k14_task2_euler_dcm import euler_to_dcm from k14_task3_quaternion import euler_to_quaternion def orientation_case(roll_deg: float, pitch_deg: float, yaw_deg: float): """Одна ориентация: углы в градусах -> матрица DCM и кватернион.""" roll, pitch, yaw = (math.radians(a) for a in (roll_deg, pitch_deg, yaw_deg)) return euler_to_dcm(roll, pitch, yaw), euler_to_quaternion(roll, pitch, yaw) if __name__ == "__main__": # Случай 1: тангаж далёк от 90 град — разные углы дают разные ориентации. dcm_a, _ = orientation_case(30.0, 20.0, 10.0) dcm_b, _ = orientation_case(50.0, 20.0, 30.0) print("тангаж 20 град: матрицы одинаковы?", np.allclose(dcm_a, dcm_b)) # Случай 2: тангаж ровно +90 град — крен и рыскание «схлопываются»: # ориентация зависит только от разности (крен - рыскание), здесь 20 град. dcm_c, quat_c = orientation_case(30.0, 90.0, 10.0) dcm_d, quat_d = orientation_case(50.0, 90.0, 30.0) print("тангаж 90 град: матрицы одинаковы?", np.allclose(dcm_c, dcm_d)) print("тангаж 90 град: кватернионы одинаковы?", np.allclose(quat_c, quat_d)) print("округлённая матрица вертикальной ориентации:") print(np.round(dcm_c, 4))
Задача 14.5. Тесты ориентации
Файл практики: k14_task5_test_attitude.py.
Создайте файл тестов: два опорных случая поворота, четыре проверки свойств DCM (согласование с матрицей рыскания, ортонормированность, определитель, обратное транспонирование), три проверки кватерниона (норма, круговой перевод, единичный кватернион) и один тест gimbal lock — всего десять тестов с допусками np.allclose() и pytest.approx.
Ожидаемый результат. Команда python -m pytest k14_task5_test_attitude.py -v показывает десять строк PASSED и итог «10 passed».
- Полный текст файла k14_task5_test_attitude.py
Скопируйте приведённое ниже содержимое в файл
code/M01_python/k14_task5_test_attitude.pyв проекте PyCharm (виртуальное окружение) и запустите тесты командойpython -m pytest k14_task5_test_attitude.py -vиз каталогаcode/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.# k14_task5_test_attitude.py # Тесты математики ориентации K14: повороты, DCM, кватернионы, gimbal lock. # Запуск из каталога code/M01_python: # python -m pytest k14_task5_test_attitude.py -v import math import numpy as np import pytest from k14_task1_yaw_rotation import rotate_body_to_ned, yaw_rotation_matrix from k14_task2_euler_dcm import euler_to_dcm, ned_to_body_matrix from k14_task3_quaternion import ( euler_to_quaternion, quaternion_norm, quaternion_to_euler, ) # --- поворот только по рысканию -------------------------------------------- def test_yaw_90_turns_north_to_east(): v = rotate_body_to_ned((1.0, 0.0, 0.0), math.radians(90.0)) assert np.allclose(v, (0.0, 1.0, 0.0)) def test_yaw_identity_at_zero(): assert np.allclose(yaw_rotation_matrix(0.0), np.eye(3)) # --- полная матрица ориентации ---------------------------------------------- def test_dcm_matches_yaw_matrix_when_level(): yaw = math.radians(40.0) assert np.allclose(euler_to_dcm(0.0, 0.0, yaw), yaw_rotation_matrix(yaw)) def test_dcm_is_orthonormal(): matrix = euler_to_dcm(math.radians(10.0), math.radians(-5.0), math.radians(30.0)) assert np.allclose(np.dot(matrix, matrix.T), np.eye(3)) def test_dcm_determinant_is_one(): matrix = euler_to_dcm(math.radians(10.0), math.radians(-5.0), math.radians(30.0)) assert np.linalg.det(matrix) == pytest.approx(1.0) def test_transpose_is_inverse_rotation(): roll, pitch, yaw = math.radians(10.0), math.radians(-5.0), math.radians(30.0) v_body = (1.0, 2.0, -3.0) forward = np.dot(euler_to_dcm(roll, pitch, yaw), v_body) back = np.dot(ned_to_body_matrix(roll, pitch, yaw), forward) assert np.allclose(back, v_body) # --- кватернионы ------------------------------------------------------------- def test_quaternion_norm_is_one(): q = euler_to_quaternion(math.radians(10.0), math.radians(-5.0), math.radians(30.0)) assert quaternion_norm(q) == pytest.approx(1.0) def test_euler_quaternion_round_trip(): angles = (math.radians(10.0), math.radians(-5.0), math.radians(30.0)) q = euler_to_quaternion(*angles) back = quaternion_to_euler(q) assert back == pytest.approx(angles) def test_identity_quaternion_gives_zero_angles(): assert quaternion_to_euler((1.0, 0.0, 0.0, 0.0)) == pytest.approx((0.0, 0.0, 0.0)) # --- gimbal lock ------------------------------------------------------------- def test_gimbal_lock_collapses_roll_and_yaw(): first = euler_to_dcm(math.radians(30.0), math.radians(90.0), math.radians(10.0)) second = euler_to_dcm(math.radians(50.0), math.radians(90.0), math.radians(30.0)) assert np.allclose(first, second)
Задача 14.6. Прогоны демо и тестов (обязательно выполнить)
Выполните команды по порядку из каталога code/M01_python в терминале с активным виртуальным окружением.
# Каталог: code/M01_python (venv активен, numpy установлен на K13)
python k14_task1_yaw_rotation.py
python k14_task2_euler_dcm.py
python k14_task3_quaternion.py
python k14_task4_gimbal_lock_demo.py
python -m pytest k14_task5_test_attitude.py -vОжидаемый результат. Ключевые строки вывода демо (сверьте значения):
task1: курс 90.0 град: нос в NED [0. 1. 0.], правый борт в NED [-1. 0. 0.]
task2: определитель: 1.000000 (ожидается 1)
произведение R @ R.T равно единичной матрице [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
task3: углы (град): (10.0, -5.0, 30.0)
кватернион (w, x, y, z): (0.9604, 0.0954, -0.0194, 0.2613)
норма кватерниона: 1.000000 (ожидается 1)
обратно в углы (град): (10.0, -5.0, 30.0)
task4: тангаж 20 град: матрицы одинаковы? False
тангаж 90 град: матрицы одинаковы? True
тангаж 90 град: кватернионы одинаковы? True
tests: 10 passedСтрока «-0.» в выводе матриц — отрицательный ноль, знакомый по уроку K13: математически это ноль, на сравнения np.allclose() он не влияет.
Задача 14.7. Сохранение в Git
Добавьте в репозиторий пять файлов урока и сделайте коммит.
# Из корня репозитория курса
git add code/M01_python/k14_task1_yaw_rotation.py code/M01_python/k14_task2_euler_dcm.py code/M01_python/k14_task3_quaternion.py code/M01_python/k14_task4_gimbal_lock_demo.py code/M01_python/k14_task5_test_attitude.py
git commit -m "K14: euler angles, DCM, quaternions, gimbal lock demo, 10 tests"Ожидаемый результат. Команда git status показывает чистое рабочее дерево по файлам k14_*, в истории появился коммит урока K14.
13. Проверьте себя
- Что такое ориентация аппарата и почему без неё невозможен пересчёт векторов из
bodyвNED? - Вокруг каких осей связанной системы измеряются крен, тангаж и рыскание и как правило правой руки задаёт положительное направление?
- Почему важен порядок поворотов ZYX и что произойдёт, если применить повороты в другом порядке?
- Что делает матрица поворота DCM с вектором и откуда произошло название «матрица направляющих косинусов»?
- Какие три свойства матрицы поворота проверяют тесты и что означает каждое из них?
- Что такое gimbal lock, при каком тангаже он возникает и как это проявляется численно в нашей демонстрации?
- Что такое кватернион ориентации и какие два преимущества дают ему место внутри автопилота?
- Зачем в формуле извлечения тангажа нужен
math.atan2()для крена и рыскания и защемление аргумента дляmath.asin()? - Чем угловая скорость отличается от углов ориентации и в каких полях сообщения
ATTITUDEприходит каждое из них? - Что означает принцип «свою архитектуру изобретай, свою математику — нет» и какие четыре шага он предписывает?
14. Чек-лист самопроверки
- Я создал все пять файлов практики в каталоге
code/M01_python. - Я могу показать на себе оси крена, тангажа и рыскания и объяснить знаки через правило правой руки.
- Демо задачи 14.1 показывает, что при курсе 90° «нос» становится восточным вектором.
- Демо задачи 14.2 показывает единичный определитель и единичную матрицу в произведении
R @ R.T. - Демо задачи 14.3 возвращает исходные углы после кругового перевода через кватернион.
- Демо задачи 14.4 напечатало
Trueпри тангаже 90° — я могу объяснить, почему две разные тройки углов совпали. - Команда
python -m pytest k14_task5_test_attitude.py -vзавершается результатом 10 passed. - Я могу назвать два преимущества кватерниона перед углами Эйлера и одну причину хранить ссылки на источник формул.
- Я сделал git commit с файлами урока K14.
K14 чек-лист закрыт. Следующий урок — K15 «Геокоординаты: широта, долгота, расстояние и направление», завершающий модуль M2 «Математика дрона».15. Что дальше
- Предыдущий урок: K13 · Векторы и системы координат: body, NED, ENU
- Текущий урок: K14 · Углы Эйлера, кватернионы и DCM
- Следующий урок: K15 · Геокоординаты: lat/lon/alt, расстояние haversine, направление bearing
- После K15 модуль M2 завершается, и курс переходит к модулю M3 «Протоколы и интерфейсы» (K16–K22) с гейтом G2 после лабораторной работы K22.
code, формулы набраны в LaTeX-нотации \( ... \) и \[ ... \]. Вводный alert излагает мысль без клишированного заголовка. Служебные врезки про особенности публикации в шапке отсутствуют. Dual-code с подписью «Чистый код». Стандарт: docs/05_EDITOR_STYLE_GUIDE.md.