Попалась мне недавно статья Синус, косинус, квадратный корень FixedPoint. Автор размышляет как можно не затратно рассчитывать координаты и углы в микроконтроллере. Попробовал я подсказать автору пару аппроксимаций, но он оказался общителен только на тему "упадка автоматизации в РФ", а по делу как то не сложился диалог. Посмотрел, такие статьи с такой тематикой не редкость. Например, очень хорошая статья Как посчитать синус быстрее всех на Xабре. В общем разгрузил себе голову на майских праздниках от главного хобби - геометрической алгебры.

В процессе изучения всего этого, возник у меня вопрос - а зачем вообще нужно аппроксимировать sin,cos, arctan и еще и в привязке к числу в двоичной системе, если есть декартовы координаты?

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

Автоматическим дифференцированием можно назвать любую конечную разность, например dy=(y(x+ε)-y(x-ε))/(2*ε). Разность здесь взята центральная, так как она дает меньшую погрешность. Параметр ε это машинный ноль. За счет округления до младшего бита его главное свойство: ε^2=0.

Эта статья по сути не более, чем описание основных моментов идеи. И если у кого то появится желание поставить эту идею на строгие математические рельсы, с удовольствием готов поучаствовать. Кто в этом случае опубликует финальную версию мне искренне не важно.

Дискретные синус и косинус как координаты на единичной окружности

(1) Зададим координаты на окружности по школьным формулам.

Преобразуем их для случая единичной окружности r=1.

Если y определяем через x, то сделать это можем только с точностью до отражения относительно оси {x}. В общем такое задание координат задает окружность в виде двух ветвей.

(2) Теперь это надо как то привязать это к числу в двоичной системе: x`=1+i*ε.

Привяжусь к числу fixed point (мантиссе числа в двоичной системе). i - порядковый номер числа.

Координаты x,y принадлежат отрезкам [-1,1]. Преобразуем данный отрезок в формат мантиссы binary18 (как я понял, автор первой упомянутой статьи хотел ограничиться 18-ю битами, чтобы экономить место в микроконтроллере). А мне так понравилось сделать, чтобы оставить в резерве часть битов мантиссы float32, что полезно для исключения ошибок типа overflow и underflow. Хорошая статья про форматы чисел, вдруг кому будет полезным.

 В общем далее по умолчанию числа округляются до искусственного машинного ноля ε=2^(-18) операцией: round(x*ε^(-1))*ε.

 Мантисса принадлежит отрезку [1,2). Квадратная скобка означает, что граница включена в диапазон. Круглая скобка означает, что граница в диапазон не включена. Пусть число мантиссы 18 бит, тогда число имеет точность ε~5*10^(-6) или в двоичной записи ε=2^(-18).

(3) Для начала найдем коэффициенты преобразования числа из диапазона
x[-1,1] --> x`[1,2). И учтем, что начало отсчета углов в математике обычно от точки x=x`=1.

a,b - границы x, a`,b` - границы x`.
a,b - границы x, a`,b` - границы x`.

Тогда можем записать линейную систему уравнений и получить коэффициенты преобразования:

Подставим вычисленные коэффициенты в выражение для x` и сразу выведем обратное преобразование:

(4) Квадраты чисел c упрощением за счет свойства ε^2=0.

(5) Мы вводили, что r^2=x^+y^2=1. Подставляя выражения для x,y получаем это выражение:

* разумеется сокращая ε^2=0
* разумеется сокращая ε^2=0

Решая уравнение относительно y`, и снова используя ε^2=0 получаем уравнение для сдвинутой окружности.

(6) Количество чисел в каждой координате 2^18=262144

(7) Итак у функции y две ветви. Округления я применил в ключевых местах, где это сделает компьютер. При этом я рассматриваю неплохой случай, в котором программист пользовался округлением до ближайшего. С округлением до меньшего, погрешности, от рассматриваемых здесь, возрастут в 100 раз. Поэтому использую всюду "round".

Функции верхней и нижней ветви и их график:

(8) Оценим погрешности округления. График относительной погрешности приведен ниже.

Погрешности примерно от x=1.02 до x=1.98 в пределах исходных 5*10^(-6). Дальше погрешность возрастает до 10^(-5). В пике погрешность возрастает до 2*10^(-5). Итого погрешность к краям окружности вырастает примерно в 10 раз.

Лекарство от увеличения погрешностей простое:

  1. При углах единичного вектора, (от -пи/4 до +пи/4)~(1.85<x<2) и (от 3пи/4 до 5пи/4)~ (1<x<1.15), выражать x`=f(y`)

  2. Во всем остальном диапазоне выражать y`=f(x`).

Ну или как я можно данный эффект проигнорировать, чтобы не плодить математику.

(9) Теперь зная с заданной точностью x,y на единичной окружности можно расcчитать x,y, которые определяют дискретные cos,sin.

Проверим как будет влиять устранение ε из знаменателя. Оно повышает погрешность в среднем в 2 раза. Упрощать себе жизнь или нет на ваш выбор. Я не упрощаю, и как исключение ε влияет на дальнейшее не проверял.

(10) Ремарка. Переходя к истории про определение углов, надо уточнить следующее.
Косинус и синус кратных углов задают полиномы Чебышева. Формулы из Википедии:

Зная значение cos(φ),sin(φ) для какого то малого угла для перекрытия окружности с кратностью n, по этим формулам не сложно расcчитать cos(nφ),sin(nφ). Множественное возведение в степень пусть вас не пугает. Так как есть хорошая статья на тему быстрого возведения в целую степень, ну и на Википедии на эту тему много есть. Написал для альтернативы, не все знают про эти ряды.

Определение углов через координаты

(11) Подумайте перед тем как пользоваться обратными тригонометрическими функциями, нужны ли они вам вообще. С ними не все так просто. Ряды для обратных тригонометрических функций сходятся медленно, или если простым языком, требуют МНОГО членов ряда для точного вычисления. Для реализации на микроконтроллерах это не вариант. Можно определить угол через длину дуги, что как увидите проще, и позволяет проиллюстрировать ограничения для применения операций с машинным нулем. До этого момента кому то могло показаться, что данный прием панацея..

Штрих так же как и ранее обозначает преобразованную величину, а не производную. Интегрировать будем верхнюю ветвь окружности.

Для единичной окружности угол равен длине дуги окружности. i - как и ранее порядковый номер координаты на окружности.

(12) Дифференциал y определим центральной разностью, так как нецентральная дает большую погрешность.

Математически данная производная - это дискретный котангенс, так как она равна отношению производной синуса к производной косинуса. На графике действительно кривая котангенса, ноль находится между точками i=262142/2 и i=262144/2, именно там, где находится угол пи/2. Произведение с дискретным тангенсом (y/x) добавил на график, чтобы видеть, что их произведение действительно =1.

(13) Безболезненно можно сокращать только элементы типа iε^2, а i^2ε^2 уже нельзя, почему думаю понятно. Возведем cot.d в квадрат и упростим.

Сравним, что получилось, сравнив погрешность для полной формы и сокращенной. Выясняется, что чем ближе к пи/2 (в самой точке пи/2 погрешность 400%), тем метод хуже работает, аналогично тому как это происходило по краям аппроксимации sin. Чем функция ближе к нулю, тем хуже работает такой способ дифференцирования. Ну и, конечно, оно от типа функции зависит.

(14) Посмотрим сколько погрешности накопится при вычислении угла пи. Во-первых видим, что число получилось комплексным, что требует при программировании исключать точки с мнимым результатом. Во-вторых, казалось бы, накопилась не маленькая погрешность определения угла (4-3,14)/3.14=+27.5%

Но посмотрим, что будет если рассчитывать угол для всех точек окружности. Считать 262144 суммы из 262144 элементов, долго даже для мощных компьютеров, поэтому в этой части снова перейду от суммы к интегралу, на суть это не особо влияет.

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

Вторая функция на графике это прямая для угла от 0 до пи в зависимости от номера.

(15) Если разделить первую функцию на вторую, зависимость оказывается почти линейна, изменения линейного коэффициента при углах > пи/64 происходят в пределах 0,1%. При малых углах погрешность очень большая. Но если такая точность определения угла устраивает, то на этом все. Если не устраивает, то более точно можно, например, методом Ньютона посчитать, с начальным приближением из описанного подхода.

Но можно немного по другому. "1.27326" это коэффициент пропорциональности прямой φ(i) и интегрального угла. Нелинейный множитель пропорционален (пи-φ/2)/(1-cos(φ/2)), что можно использовать, для развития описанного метода.

На графике погрешность в области малых углов
На графике погрешность в области малых углов

Чтобы ликвидировать этот нюанс, я воспользовался методом наименьших квадратов в виде решения основанного на псевдо-обратной матрице. После применения интерполяции полиномом 2-й степени от аргумента (пи-φ/2)/(1-cos(φ/2)), остаточная погрешность получилась 10^(-6).

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

Итого, аппроксимация для угла с аргументом в виде номера точки оказывается полиномом второй степени от аргумента X, с нулевой константой C0. Зависимость для угла получилась вполне вычислимой даже на микроконтроллере.

Как быстро рассчитывать корни есть много методов. Не решенным остались три вопроса на перспективу:

  1. Как эффективно использовать экспоненциальную часть float32 для расчета. 10^(-10) это за пределами точности представления числа только 18-ти битной мантиссой.

  2. Как эффективно рассчитывать дроби.

  3. Все таки потребовалось и косинусы вычислять от дискретных углов.

Выводы

Описанное выше означает, что зависимость углов от номера на окружности как то можно заложить в логику микроконтроллера, и относительно точно аппроксимировать углы без интегралов и обратных тригонометрических функций.

Нюанс есть всегда, в данном случае он в том, что такая математика потребует коэффициентов для калибровки в контроллере под каждый конкретный метод реализации. Но на мой взгляд это небольшая цена. Когда закончу с геометрической алгеброй, думаю обобщить этот метод на три измерения. Спойлер - там свойства матричного машинного эпсилон еще интереснее, чем здесь. И так же будет интересно зашить в число float32 три мнимых единицы, это так же одна из причин написать здесь про число 18 бит, чтобы оставить свободными необходимые три бита и 2 бита оставить в резерве.

Комментарии (19)


  1. schulzr
    11.05.2025 08:17

    Добрый день.

    По моему разумению ваше определение автоматического дифференцирования не является классическим . И даже противоречит ссылке, которую вы привели.

    "In contrast to the more traditional numerical methods based on finite differences, auto-differentiation is 'in theory' exact."

    Вы же привели вариант конечно-разностный. И разница для меня этих походов принципиальная. Или я что-то упускаю в вашей логике?


    1. Exlt8 Автор
      11.05.2025 08:17

      Добрый день. Все сложнее..

      Во первых в Википедии что то очень странное написано, но если разобраться, то про то же самое. Во вторых автоматическое дифференцирование из того, что я видел, чаще применяется матричное. Но все способы сводится к использованию нильпотентного элемента в выражении.


      1. aleex
        11.05.2025 08:17

        Вот же хорошая статья на Хабре: https://habr.com/ru/companies/intel/articles/170729/

        И да, ключевое отличие автоматического дифференцирования в том, что оно "точное", в отличие от конечно-разностного, в котором выбор шага дифференцирования ой как влияет


        1. Exlt8 Автор
          11.05.2025 08:17

          Статью читал еще давно, но не очень представляю себе как это применить к описанию единичной окружности в микроконтроллере..


          1. Refridgerator
            11.05.2025 08:17

            Я бы вам подсказал, но из статьи совершенно непонятно - а какую собственно задачу вы решаете?


      1. schulzr
        11.05.2025 08:17

        Похоже вы не разобрались в смысле автоматического дифференцирования. Эта техника достаточно универсальна к большому классу задач и часто используется для определения градиента функции многих переменных, когда конечные разности неприменимы.


        1. Exlt8 Автор
          11.05.2025 08:17

          Ок, значит разберусь со временем


      1. Refridgerator
        11.05.2025 08:17

        В википедии да, написано странное. Но я могу разъяснить: автоматическое дифференцирование - это когда при вычислениях вы вместо обычных чисел вы берёте дуальные. И тогда коэффициент при мнимой единице будет иметь значение производной. Причём точной (в пределах погрешности вычислений, если с плавающей точкой считать), а не конечно-разностной.


    1. Exlt8 Автор
      11.05.2025 08:17

      1. Refridgerator
        11.05.2025 08:17

        Только не двойных, а дуальных. В двойных квадрат мнимой единицы равен 1, а не 0. Да, по-русски не сильно большая разница и не особо удачно выбранные названия.


        1. Exlt8 Автор
          11.05.2025 08:17

          Ну это само собой) Я конечно стараюсь быть аккуратен, но не до фанатизма. Все равно в разной литературе по разному обзывают


  1. bear11
    11.05.2025 08:17

    А вообще интересно было бы написать "математический анализ над полем представимых в IEEE 754 чисел", а именно:

    что происходит

    • с пределами

    • с производными

    • с основными функциями

    • с основными теоремами

    • с дифференциальным исчислением

    • с интегральным исчислением


    1. Exlt8 Автор
      11.05.2025 08:17

      С удовольствием, если будут соучастники для такого масштабного проекта


    1. Refridgerator
      11.05.2025 08:17

      Быстрый ответ - ничего. Всё вами перечисленное - это разделы аналитической математики. Если вас интересуют погрешности вычислений - то они будут разные в разных случаях, а для их контроля и обхода существуют отдельная матчасть. Ещё в математике нет основных функций, есть элементарные, а IEEE 754 не является полем, потому что там два нуля.


  1. Simonov77
    11.05.2025 08:17

    Вы, случайно, не метод CORDIC заново изобрести пытаетесь?


    1. Exlt8 Автор
      11.05.2025 08:17

      Если бы я шел путем CORDIC или его апгрейда, то привязался бы к полиномам Чебышева, а так я их просто упомянул. Ну и описанное тут работает без итераций... Вообще статья больше про численное дифференцирование и интерполяцию, даже не могу предположить как разумно повороты к этому сейчас притянуть.. А вот тот же прием в составе геометрической алгебры влечет собой все интересное - повороты, отражения, трансляции. Но до этого надо дорасти.


      1. Refridgerator
        11.05.2025 08:17

        Повороты на 2D-плоскости притягиваются через умножение комплексных чисел.


  1. lgorSL
    11.05.2025 08:17

    При прочтении с первого раза толком не понял, что происходит. Есть какие-то формулы, но что-то ход мысли я не смог уловить (и заодно не понял, при чём тут геометрическая алгебра).
    В голосовалке пока не участвовал, вечером попробую второй раз прочитать.


    1. Exlt8 Автор
      11.05.2025 08:17

      ГА тут будет причем, если трехмерные преобразования с машинным нулем записать, например по аналогии с единичной окружностью разбить сферу с шагом эпсилон по каждой координате.. А так про ГА просто упоминание для тех, кто привык читать от меня на тему ГА) Основной лейтмотив данной статьи, в том, что похоже, применяя только корни можно писать производительный код для слабых процессоров микроконтроллеров с точностью расчетов ~10^(-5) -10^(-6), используя числа в формате float32 (single precision). Автор статьи, которая была для меня исходной точкой, автоматизировал станок на контроллере STM32.