Как обернуть вокруг диапазона - программирование

Как обернуть вокруг диапазона

Углы в моей программе выражены от 0 до 2pi. Я хочу, чтобы добавить два угла и обернуть вокруг 2pi до 0, если результат выше 2pi. Или, если бы я вычитал угол под углом и был ниже 0, он бы обернулся вокруг 2pi.

Есть ли способ сделать это?

Спасибо.

4b9b3361

Ответ 1

То, что вы ищете, - это модуль. Функция fmod не будет работать, потому что вычисляет остаток, а не арифметический модуль. Что-то вроде этого должно работать:

inline double wrapAngle( double angle )
{
    double twoPi = 2.0 * 3.141592865358979;
    return angle - twoPi * floor( angle / twoPi );
}

Edit:

Остальная часть обычно определяется как то, что осталось после длительного деления (например, остаток 18/4 равен 2, потому что 18 = 4 * 4 + 2). Это становится волосатым, когда у вас есть отрицательные числа. Общим способом найти остаток от подписанного деления является то, что остаток имеет тот же знак, что и результат (например, остаток от -18/4 равен -2, поскольку -18 = -4 * 4 + - 2).

Определение модуля x y является наименьшим положительным значением m в уравнении x = y * c + m, поскольку c является целым числом. Таким образом, 18 mod 4 будет 2 (где c = 4), однако -18 mod 4 также будет 2 (где c = -5).

Простейшим вычислением x mod y является xy * floor (x/y), где floor - наибольшее целое число, которое меньше или равно вводимому.

Ответ 2

angle = fmod(angle, 2.0 * pi);
if (angle < 0.0)
   angle += 2.0 * pi;

Edit: перечитав это (и посмотрев на ответ Джонатана Леффлера), я был немного удивлен его заключением, поэтому переписал код на то, что я считал несколько более подходящей формой (например, распечатывая результат из чтобы компилятор не мог просто полностью отказаться от вычислений, потому что он никогда не использовался). Я также изменил его, чтобы использовать счетчик производительности Windows (так как он не включил его класс таймера, а std::chrono::high_resolution_timer полностью сломан в обоих компиляторах, которые мне сейчас очень удобны).

Я также немного обработал код (это помечен С++, а не C), чтобы получить следующее:

#include <math.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <windows.h>

static const double PI = 3.14159265358979323844;

static double r1(double angle)
{
    while (angle > 2.0 * PI)
        angle -= 2.0 * PI;
    while (angle < 0)
        angle += 2.0 * PI;
    return angle;
}

static double r2(double angle)
{
    angle = fmod(angle, 2.0 * PI);
    if (angle < 0.0)
        angle += 2.0 * PI;
    return angle;
}

static double r3(double angle)
{
    double twoPi = 2.0 * PI;
    return angle - twoPi * floor(angle / twoPi);
}

struct result {
    double sum;
    long long clocks;
    result(double d, long long c) : sum(d), clocks(c) {}

    friend std::ostream &operator<<(std::ostream &os, result const &r) {
        return os << "sum: " << r.sum << "\tticks: " << r.clocks;
    }
};

result operator+(result const &a, result const &b) {
    return result(a.sum + b.sum, a.clocks + b.clocks);
}

struct TestSet { double start, end, increment; };

template <class F>
result tester(F f, TestSet const &test, int count = 5)
{
    LARGE_INTEGER start, stop;

    double sum = 0.0;

    QueryPerformanceCounter(&start);

    for (int i = 0; i < count; i++) {
        for (double angle = test.start; angle < test.end; angle += test.increment)
            sum += f(angle);
    }
    QueryPerformanceCounter(&stop);

    return result(sum, stop.QuadPart - start.QuadPart);
}

int main() {

    std::vector<TestSet> tests {
        { -6.0 * PI, +6.0 * PI, 0.01 },
        { -600.0 * PI, +600.0 * PI, 3.00 }
    };


    std::cout << "Small angles:\n";
    std::cout << "loop subtraction: " << tester(r1, tests[0]) << "\n";
    std::cout << "            fmod: " << tester(r2, tests[0]) << "\n";
    std::cout << "           floor: " << tester(r3, tests[0]) << "\n";
    std::cout << "\nLarge angles:\n";
    std::cout << "loop subtraction: " << tester(r1, tests[1]) << "\n";
    std::cout << "            fmod: " << tester(r2, tests[1]) << "\n";
    std::cout << "           floor: " << tester(r3, tests[1]) << "\n";

}

Результаты, которые я получил, были следующими:

Small angles:
loop subtraction: sum: 59196    ticks: 684
            fmod: sum: 59196    ticks: 1409
           floor: sum: 59196    ticks: 1885

Large angles:
loop subtraction: sum: 19786.6  ticks: 12516
            fmod: sum: 19755.2  ticks: 464
           floor: sum: 19755.2  ticks: 649

По крайней мере, мне кажется, результаты, похоже, поддерживают совсем другой вывод, чем Джонатон. Рассматривая версию, которая делает вычитание в цикле, мы видим две точки: для теста с большими углами получается сумма, отличная от двух других (то есть, неточная), а во-вторых, она ужасно медленная. Если вы точно не знаете, что ваши входы всегда начинают почти нормализоваться, это в основном просто непригодно.

Между версией fmod и версией floor, похоже, нет места для аргумента - они оба дают точные результаты, но версия fmod выполняется быстрее как при малом угле, так и при больших углах.

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

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

Oh, for what it worth:
OS: Windows 7 ultimate
Compiler: g++ 4.9.1
Hardware: AMD A6-6400K

Ответ 3

Из любопытства я экспериментировал с тремя алгоритмами в других ответах, определяя их.

Когда нормализуемые значения близки к диапазону 0..2π, ​​тогда алгоритм while является самым быстрым; алгоритм с использованием fmod() является самым медленным, а алгоритм с использованием floor() находится между ними.

Когда нормализуемые значения не близки к диапазону 0..2π, ​​алгоритм while является самым медленным, алгоритм с использованием floor() выполняется быстрее, а алгоритм с использованием fmod() находится между ними.

Итак, я заключаю, что:

  • Если углы (обычно) близки к нормированным, то используется алгоритм while.
  • Если углы не близки к нормированным, то используется алгоритм floor().

Результаты тестирования:

r1 = while, r2 = fmod(), r3 = floor()

Near Normal     Far From Normal
r1 0.000020     r1 0.000456
r2 0.000078     r2 0.000085
r3 0.000058     r3 0.000065
r1 0.000032     r1 0.000406
r2 0.000085     r2 0.000083
r3 0.000057     r3 0.000063
r1 0.000033     r1 0.000406
r2 0.000085     r2 0.000085
r3 0.000058     r3 0.000065
r1 0.000033     r1 0.000407
r2 0.000086     r2 0.000083
r3 0.000058     r3 0.000063

Тестовый код:

В тестовом коде используется значение, показанное для PI. Стандарт C не определяет значение для π, но POSIX определяет M_PI и ряд связанных констант, поэтому я мог бы написать свой используя M_PI вместо PI.

#include <math.h>
#include <stdio.h>
#include "timer.h"

static const double PI = 3.14159265358979323844;

static double r1(double angle)
{
    while (angle > 2.0 * PI)
        angle -= 2.0 * PI;
    while (angle < 0)
        angle += 2.0 * PI;
    return angle;
}

static double r2(double angle)
{
    angle = fmod(angle, 2.0 * PI);
    if (angle < 0.0)
        angle += 2.0 * PI;
    return angle;
}

static double r3(double angle)
{
    double twoPi = 2.0 * PI;
    return angle - twoPi * floor( angle / twoPi );
}

static void tester(const char * tag, double (*test)(double), int noisy)
{
    typedef struct TestSet { double start, end, increment; } TestSet;
    static const TestSet tests[] =
    {
        {   -6.0 * PI,   +6.0 * PI, 0.01 },
    //  { -600.0 * PI, +600.0 * PI, 3.00 },
    };
    enum { NUM_TESTS = sizeof(tests) / sizeof(tests[0]) };
    Clock clk;
    clk_init(&clk);
    clk_start(&clk);
    for (int i = 0; i < NUM_TESTS; i++)
    {
        for (double angle = tests[i].start; angle < tests[i].end; angle += tests[i].increment)
        {
            double result = (*test)(angle);
            if (noisy)
                printf("%12.8f : %12.8f\n", angle, result);
        }
    }
    clk_stop(&clk);
    char buffer[32];
    printf("%s %s\n", tag, clk_elapsed_us(&clk, buffer, sizeof(buffer)));
}

int main(void)
{
    tester("r1", r1, 0);
    tester("r2", r2, 0);
    tester("r3", r3, 0);
    tester("r1", r1, 0);
    tester("r2", r2, 0);
    tester("r3", r3, 0);
    tester("r1", r1, 0);
    tester("r2", r2, 0);
    tester("r3", r3, 0);
    tester("r1", r1, 0);
    tester("r2", r2, 0);
    tester("r3", r3, 0);
    return(0);
}

Тестирование на Mac OS X 10.7.4 со стандартным /usr/bin/gcc (i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.9.00)). Показан "близкий к нормализованному" тестовый код; "далеко не нормализованные" тестовые данные были созданы путем раскомментации комментария // в тестовых данных.

Сроки с встроенным GCC 4.7.1 аналогичны (будут сделаны одни и те же выводы):

Near Normal     Far From Normal
r1 0.000029     r1 0.000321
r2 0.000075     r2 0.000094
r3 0.000054     r3 0.000065
r1 0.000028     r1 0.000327
r2 0.000075     r2 0.000096
r3 0.000053     r3 0.000068
r1 0.000025     r1 0.000327
r2 0.000075     r2 0.000101
r3 0.000053     r3 0.000070
r1 0.000028     r1 0.000332
r2 0.000076     r2 0.000099
r3 0.000050     r3 0.000065

Ответ 4

Вы можете использовать что-то вроде этого:

while (angle > 2pi)
    angle -= 2pi;

while (angle < 0)
    angle += 2pi;

В принципе, вам нужно изменить угол на 2pi, пока не убедитесь, что он не превышает 2pi или опускается ниже 0.

Ответ 5

Простой трюк: просто добавьте смещение, которое должно быть multiple из 2pi, чтобы довести результат до положительного диапазона до выполнения fmod(). Fmod() вернет его обратно в диапазон [0, 2pi) автоматически. Это работает до тех пор, пока вы знаете априори ряд возможных входов, которые вы можете получить (что вы часто делаете). Чем больше применяется смещение, тем больше бит точности FP вы потеряете, поэтому вы, вероятно, не захотите добавить, скажем, 20000pi, хотя это, безусловно, будет "более безопасным" с точки зрения обработки очень больших внеочередных входов, Предполагая, что никто никогда не пройдет входные углы, которые суммируются вне довольно сумасшедшего диапазона [-8pi, + inf), мы просто добавим 8pi перед fmod() ing.

double add_angles(float a, float b)
{
    ASSERT(a + b >= -8.0f*PI);
    return fmod(a + b + 8.0f*PI, 2.0f*PI);
}

Ответ 6

Аналогично, если вы хотите находиться в диапазоне от -2pi до 2pi, fmod отлично работает

Ответ 7

В зависимости от вашего варианта использования есть несколько эффективных особых случаев, если у вас есть возможность выбрать собственное представление угла. (Обратите внимание, что нижний предел равен 0 - это уже особый случай, обеспечивающий эффективность.)

Выражение углов в виде единичных векторов

Если вы можете представить углы в виде значений между [0 и 1) вместо [0 и 2π), вам просто нужно взять дробную часть:

float wrap(float angle) {
    // wrap between [0 and 2*PI)
    return angle - floor(angle);
}

Отрицательные углы просто работают.

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

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

Выражение углов в виде целых чисел без знака, ограниченных степенью двух

Если вы можете представлять углы в виде значений между [0 и 2 ^ n) вместо [0 и 2π), то вы можете обернуть их с точностью до двух с помощью побитовой операции и операции:

unsigned int wrap(unsigned int angle) {
    // wrap between [0 and 65,535)
    return angle & 0xffff;
}

Еще лучше, если вы можете выбрать степень двойки, равную размеру целочисленного типа, числа просто переносятся естественным образом. uint16_t всегда переносится в [0 и 2 ^ 16), а uint32_t всегда переносится в [0 и 2 ^ 32). Шестьдесят пять тысяч заголовков должно быть достаточно для каждого, не так ли? (-:

Я использовал это в играх и демо-типах в 8-битные дни и даже для наложения текстур перед 3D-видеокартами. Я полагаю, это все еще было бы полезно в коде для эмуляторов и ретро-игр, но, возможно, даже на крошечных микроконтроллерах?