Cómo envolver alrededor de un rango

Cómo envolver alrededor de un rango

Lo que buscas es el módulo. La función fmod no funcionará porque calcula el resto y no el módulo aritmético. Algo como esto debería funcionar:

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

Editar:

El resto se define comúnmente como lo que sobra después de una división larga (p. ej., el resto de 18/4 es 2, porque 18 =4 * 4 + 2 ). Esto se pone peludo cuando tienes números negativos. La forma común de encontrar el resto de una división con signo es que el resto tenga el mismo signo que el resultado (p. ej., el resto de -18/4 es -2, porque -18 =-4 * 4 + - 2 ).

La definición de x módulo y es el valor positivo más pequeño de m en la ecuación x=y*c+m, dado que c es un número entero. Así que 18 mod 4 sería 2 (donde c=4), sin embargo -18 mod 4 también sería 2 (donde c=-5).

El cálculo más simple de x mod y es x-y*piso(x/y) , donde piso es el entero más grande que es menor o igual que la entrada.


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

Editar:después de volver a leer esto (y mirar la respuesta de Jonathan Leffler), me sorprendió un poco su conclusión, así que reescribí el código a lo que consideré una forma algo más adecuada (por ejemplo, imprimir un resultado del cálculo para asegurar el compilador no podía simplemente descartar el cálculo por completo porque nunca se usó). También lo cambié para usar el contador de rendimiento de Windows (ya que no incluyó su clase de temporizador y el std::chrono::high_resolution_timer está completamente roto en los dos compiladores que tengo a mano en este momento).

También realicé un poco de limpieza general del código (esto está etiquetado como C++, no como C), para obtener esto:

#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";

}

Los resultados que obtuve fueron los siguientes:

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

Al menos para mí, los resultados parecen respaldar una conclusión bastante diferente a la que llegó Jonathon. Mirando la versión que resta en un bucle, vemos dos puntos:para la prueba de ángulos grandes, produce una suma que es diferente de las otras dos (es decir, es inexacta) y segundo, es horriblemente lento. A menos que sepa con certeza que sus entradas siempre comienzan casi normalizadas, esto es básicamente inutilizable.

Entre el fmod versión y el floor versión parece que no hay lugar para la discusión:ambos producen resultados precisos, pero el fmod La versión es más rápida tanto en las pruebas de ángulo pequeño como de ángulo grande.

Hice un poco más de prueba, experimentando aumentando el número de repeticiones y disminuyendo el tamaño de los pasos en la prueba de ángulos grandes. Aunque supongo que es posible simplemente se debe a una diferencia en la plataforma o el compilador, no pude encontrar ninguna circunstancia o situación que estuvo cerca de confirmar los resultados o la conclusión de Jonathan.

En pocas palabras:si tiene mucho conocimiento previo sobre su entrada y sabe siempre estará casi normalizado antes lo normalizas, entonces podrías ser capaz de salirse con la suya haciendo la resta en un bucle. En cualquier otra circunstancia, fmod es la elección clara. Parece que no circunstancia en la que el floor versión tiene algún sentido.

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

Por curiosidad, experimenté con tres algoritmos en otras respuestas, cronometrándolos.

Cuando los valores a normalizar están cerca del rango 0..2π, ​​entonces el while el algoritmo es el más rápido; el algoritmo usando fmod() es el más lento, y el algoritmo que usa floor() está en el medio.

Cuando los valores a normalizar no están cerca del rango 0..2π, ​​entonces el while el algoritmo es el más lento, el algoritmo que usa floor() es el más rápido, y el algoritmo que usa fmod() está en el medio.

Entonces, concluyo que:

  • Si los ángulos están (generalmente) cerca de la normalización, el while algoritmo es el que se debe utilizar.
  • Si los ángulos no están cerca de normalizarse, entonces el floor() algoritmo es el que se debe utilizar.

Resultados de la prueba:

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

Código de prueba:

El código de prueba usó el valor que se muestra para PI . El estándar C no define un valor para π, pero POSIX define M_PI y una serie de constantes relacionadas, por lo que podría haber escrito mi código usando M_PI en lugar de 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);
}

Pruebas en Mac OS X 10.7.4 con el estándar /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) ). Se muestra el código de prueba 'cercano a normalizado'; los datos de prueba 'lejos de normalizarse' se crearon descomentando el // comentario en los datos de prueba.

El tiempo con un GCC 4.7.1 construido en casa es similar (se sacarían las mismas conclusiones):

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