Aproximación de un caso especial de la función Theta de Riemann

27

Este desafío es escribir código rápido que pueda realizar una suma infinita computacionalmente difícil.

Entrada

Una matriz nby con entradas enteras que son más pequeñas que en valor absoluto. Al realizar la prueba, me complace proporcionar información a su código en cualquier formato sensible que su código desee. El valor predeterminado será una línea por fila de la matriz, separada por espacios y proporcionada en la entrada estándar.nP100

Pserá positivo definitivo que implica que siempre será simétrico. Aparte de eso, realmente no necesita saber qué significa positivo definido para responder al desafío. Sin embargo, significa que en realidad habrá una respuesta a la suma definida a continuación.

Sin embargo, debe saber qué es un producto de matriz-vector .

Salida

Su código debe calcular la suma infinita:

ingrese la descripción de la imagen aquí

dentro de más o menos 0.0001 de la respuesta correcta. Aquí Zestá el conjunto de enteros y también Z^ntodos los vectores posibles con nelementos enteros y ees la famosa constante matemática que equivale aproximadamente a 2.71828. Tenga en cuenta que el valor en el exponente es simplemente un número. Vea a continuación un ejemplo explícito.

¿Cómo se relaciona esto con la función Theta de Riemann?

En la notación de este documento sobre la aproximación de la función Theta de Riemann que estamos tratando de calcular ingrese la descripción de la imagen aquí. Nuestro problema es un caso especial por al menos dos razones.

  • Establecemos el parámetro inicial llamado zen el documento vinculado a 0.
  • Creamos la matriz Pde tal manera que el tamaño mínimo de un valor propio sea1 . (Vea a continuación cómo se crea la matriz).

Ejemplos

P = [[ 5.,  2.,  0.,  0.],
     [ 2.,  5.,  2., -2.],
     [ 0.,  2.,  5.,  0.],
     [ 0., -2.,  0.,  5.]]

Output: 1.07551411208

Con más detalle, veamos solo un término en la suma de esta P. Tomemos, por ejemplo, solo un término en la suma:

ingrese la descripción de la imagen aquí

y x^T P x = 30. Tenga en cuenta que e^(-30)se trata 10^(-14)y, por lo tanto, es poco probable que sea importante para obtener la respuesta correcta hasta la tolerancia dada. Recuerde que la suma infinita en realidad usará todos los posibles vectores de longitud 4 donde los elementos son enteros. Acabo de elegir uno para dar un ejemplo explícito.

P = [[ 5.,  2.,  2.,  2.],
     [ 2.,  5.,  4.,  4.],
     [ 2.,  4.,  5.,  4.],
     [ 2.,  4.,  4.,  5.]]

Output = 1.91841190706

P = [[ 6., -3.,  3., -3.,  3.],
     [-3.,  6., -5.,  5., -5.],
     [ 3., -5.,  6., -5.,  5.],
     [-3.,  5., -5.,  6., -5.],
     [ 3., -5.,  5., -5.,  6.]]

Output = 2.87091065342

P = [[6., -1., -3., 1., 3., -1., -3., 1., 3.],
     [-1., 6., -1., -5., 1., 5., -1., -5., 1.],
     [-3., -1., 6., 1., -5., -1., 5., 1., -5.],
     [1., -5., 1., 6., -1., -5., 1., 5., -1.],
     [3., 1., -5., -1., 6., 1., -5., -1., 5.],
     [-1., 5., -1., -5., 1., 6., -1., -5., 1.],
     [-3., -1., 5., 1., -5., -1., 6., 1., -5.],
     [1., -5., 1., 5., -1., -5., 1., 6., -1.],
     [3., 1., -5., -1., 5., 1., -5., -1., 6.]]

Output: 8.1443647932

P = [[ 7.,  2.,  0.,  0.,  6.,  2.,  0.,  0.,  6.],
     [ 2.,  7.,  0.,  0.,  2.,  6.,  0.,  0.,  2.],
     [ 0.,  0.,  7., -2.,  0.,  0.,  6., -2.,  0.],
     [ 0.,  0., -2.,  7.,  0.,  0., -2.,  6.,  0.],
     [ 6.,  2.,  0.,  0.,  7.,  2.,  0.,  0.,  6.],
     [ 2.,  6.,  0.,  0.,  2.,  7.,  0.,  0.,  2.],
     [ 0.,  0.,  6., -2.,  0.,  0.,  7., -2.,  0.],
     [ 0.,  0., -2.,  6.,  0.,  0., -2.,  7.,  0.],
     [ 6.,  2.,  0.,  0.,  6.,  2.,  0.,  0.,  7.]]

Output = 3.80639191181

Puntuación

Probaré su código en matrices P elegidas al azar de tamaño creciente.

Su puntaje es simplemente el más grande npara el que obtengo una respuesta correcta en menos de 30 segundos cuando se promedia más de 5 carreras con matrices elegidas al azar Pde ese tamaño.

¿Qué tal una corbata?

Si hay un empate, el ganador será aquel cuyo código se ejecute más rápido en promedio durante 5 carreras. En el caso de que esos tiempos también sean iguales, el ganador es la primera respuesta.

¿Cómo se creará la entrada aleatoria?

  1. Sea M una matriz aleatoria de m por n con m <= n y entradas que son -1 o 1. En Python / numpy M = np.random.choice([0,1], size = (m,n))*2-1. En la práctica me voy a poner ma tratar n/2.
  2. Sea P la matriz de identidad + M ^ T M. En Python / numpy P =np.identity(n)+np.dot(M.T,M). Ahora estamos garantizados de que Pes definitivo positivo y las entradas están en un rango adecuado.

Tenga en cuenta que esto significa que todos los valores propios de P son al menos 1, lo que hace que el problema sea potencialmente más fácil que el problema general de aproximar la función Theta de Riemann.

Idiomas y bibliotecas

Puede usar cualquier idioma o biblioteca que desee. Sin embargo, para fines de puntuación, ejecutaré su código en mi máquina, así que proporcione instrucciones claras sobre cómo ejecutarlo en Ubuntu.

Mi máquina Los tiempos se ejecutarán en mi máquina. Esta es una instalación estándar de Ubuntu en un procesador AMD FX-8350 de ocho núcleos de 8GB. Esto también significa que necesito poder ejecutar su código.


Respuestas principales

  • n = 47en C ++ por Ton Hospel
  • n = 8en Python por Maltysen
Glorfindel
fuente
Cabe mencionar que una matriz definida positiva es simétrica por definición.
2012rcampion
@ 2012 Arcampion Gracias. Adicional.
Ok, tal vez esta es una pregunta tonta, pero he contempló esto para las edades y no puedo averiguar cómo ha llegado una xde [-1,0,2,1]. ¿Puedes dar más detalles sobre esto? (Sugerencia: no soy un gurú de las matemáticas)
wnnmaw
@wnnmaw Perdón por ser confuso. La suma tiene un término para cada posible vector x de longitud 4 en este caso. [-1,0,2,1] es solo uno que elegí al azar para mostrar explícitamente cuál sería el término en ese caso.
1
@Lembik La forma en que genera las matrices SPD implica que ningún valor singular tiene un valor absoluto por debajo de 1. ¿Podemos usar ese conocimiento?
flawr

Respuestas:

15

C ++

No más acercamiento ingenuo. Solo evalúe dentro del elipsoide.

Utiliza las bibliotecas armadillo, ntl, gsl y pthread. Instalar usando

apt-get install libarmadillo-dev libntl-dev libgsl-dev

Compila el programa usando algo como:

g++ -Wall -std=c++11 -O3 -fno-math-errno -funsafe-math-optimizations -ffast-math -fno-signed-zeros -fno-trapping-math -fomit-frame-pointer -march=native -s infinity.cpp -larmadillo -lntl -lgsl -lpthread -o infinity

En algunos sistemas, es posible que deba agregar -lgslcblasdespués -lgsl.

Ejecute con el tamaño de la matriz seguido de los elementos en STDIN:

./infinity < matrix.txt

matrix.txt:

4
5  2  0  0
2  5  2 -2
0  2  5  0
0 -2  0  5

O para probar una precisión de 1e-5:

./infinity -p 1e-5 < matrix.txt

infinity.cpp:

// Based on http://arxiv.org/abs/nlin/0206009

#include <iostream>
#include <vector>
#include <stdexcept>
#include <cstdlib>
#include <cmath>
#include <string>
#include <thread>
#include <future>
#include <chrono>

using namespace std;

#include <getopt.h>

#include <armadillo>

using namespace arma;

#include <NTL/mat_ZZ.h>
#include <NTL/LLL.h>

using namespace NTL;

#include <gsl/gsl_sf_gamma.h>
#include <gsl/gsl_errno.h>
#include <gsl/gsl_roots.h>

double const EPSILON = 1e-4;       // default precision
double const GROW    = 2;          // By how much we grow the ellipsoid volume
double const UPSCALE = 1e9;        // lattice reduction, upscale real to integer
double const THREAD_SEC = 0.1;     // Use threads if need more time than this
double const RADIUS_MAX = 1e6;     // Maximum radius used in root finding
double const RADIUS_INTERVAL = 1e-6; // precision of target radius
int const ITER_MAX = 1000;         // Maximum iterations in root finding
unsigned long POINTS_MIN = 1000;   // Minimum points before getting fancy

struct Result {
    Result& operator+=(Result const& add) {
        sum     += add.sum;
        elapsed += add.elapsed;
        points  += add.points;
        return *this;
    }

    friend Result operator-(Result const& left, Result const& right) {
        return Result{left.sum - right.sum,
                left.elapsed - right.elapsed,
                left.points - right.points};
    }

    double sum, elapsed;
    unsigned long points;
};

struct Params {
    double half_rho, half_N, epsilon;
};

double fill_factor_error(double r, void *void_params) {
    auto params = static_cast<Params*>(void_params);
    r -= params->half_rho;
    return gsl_sf_gamma_inc(params->half_N, r*r) - params->epsilon;
}

// Calculate radius needed for target precision
double radius(int N, double rho, double lat_det, double epsilon) {
    Params params;

    params.half_rho = rho / 2.;
    params.half_N   = N   / 2.;
    params.epsilon = epsilon*lat_det*gsl_sf_gamma(params.half_N)/pow(M_PI, params.half_N);

    // Calculate minimum allowed radius
    auto r = sqrt(params.half_N)+params.half_rho;
    auto val = fill_factor_error(r, &params);
    cout << "Minimum R=" << r << " -> " << val << endl;

    if (val > 0) {
        // The minimum radius is not good enough. Work out a better one by
        // finding the root of a tricky function
        auto low  = r;
        auto high = RADIUS_MAX * 2 * params.half_rho;
        auto val = fill_factor_error(high, &params);
        if (val >= 0)
            throw(logic_error("huge RADIUS_MAX is still not big enough"));

        gsl_function F;
        F.function = fill_factor_error;
        F.params   = &params;

        auto T = gsl_root_fsolver_brent;
        auto s = gsl_root_fsolver_alloc (T);
        gsl_root_fsolver_set (s, &F, low, high);

        int status = GSL_CONTINUE;
        for (auto iter=1; status == GSL_CONTINUE && iter <= ITER_MAX; ++iter) {
            gsl_root_fsolver_iterate (s);
            low  = gsl_root_fsolver_x_lower (s);
            high = gsl_root_fsolver_x_upper (s);
            status = gsl_root_test_interval(low, high, 0, RADIUS_INTERVAL  * 2 * params.half_rho);
        }
        r = gsl_root_fsolver_root(s);
        gsl_root_fsolver_free(s);
        if (status == GSL_CONTINUE)
            throw(logic_error("Search for R did not converge"));
    }
    return r;
}

// Recursively walk down the ellipsoids in each dimension
void ellipsoid(int d, mat const& A, double const* InvD, mat& Accu,
               Result& result, double r2) {
    auto r = sqrt(r2);
    auto offset = Accu(d, d);
    // InvD[d] = 1/ A(d, d)
    auto from = ceil((-r-offset) * InvD[d]);
    auto to   = floor((r-offset) * InvD[d]);
    for (auto v = from; v <= to; ++v) {
        auto value  = v * A(d, d)+offset;
        auto residu = r2 - value*value;
        if (d == 0) {
            result.sum += exp(residu);
            ++result.points;
        } else {
            for (auto i=0; i<d; ++i) Accu(d-1, i) = Accu(d, i) + v * A(d, i);
            ellipsoid(d-1, A, InvD, Accu, result, residu);
        }
    }
}

// Specialised version of ellipsoid() that will only process points an octant
void ellipsoid(int d, mat const& A, double const* InvD, mat& Accu,
               Result& result, double r2, unsigned int octant) {
    auto r = sqrt(r2);
    auto offset = Accu(d, d);
    // InvD[d] = 1/ A(d, d)
    long from = ceil((-r-offset) * InvD[d]);
    long to   = floor((r-offset) * InvD[d]);
    auto points = to-from+1;
    auto base = from + points/2;
    if (points & 1) {
        auto value = base * A(d, d) + offset;
        auto residu = r2 - value * value;
        if (d == 0) {
            if ((octant & (octant - 1)) == 0) {
                result.sum += exp(residu);
                ++result.points;
            }
        } else {
            for (auto i=0; i<d; ++i) Accu(d-1, i) = Accu(d, i) + base * A(d, i);
            ellipsoid(d-1, A, InvD, Accu, result, residu, octant);
        }
        ++base;
    }
    if ((octant & 1) == 0) {
        to = from + points / 2 - 1;
        base = from;
    }
    octant /= 2;
    for (auto v = base; v <= to; ++v) {
        auto value = v * A(d,d)+offset;
        auto residu = r2 - value*value;
        if (d == 0) {
            if ((octant & (octant - 1)) == 0) {
                result.sum += exp(residu);
                ++result.points;
            }
        } else {
            for (auto i=0; i<d; ++i) Accu(d-1, i) = Accu(d, i) + v * A(d, i);
            if (octant == 1)
                ellipsoid(d-1, A, InvD, Accu, result, residu);
            else
                ellipsoid(d-1, A, InvD, Accu, result, residu, octant);
        }
    }
}

// Prepare call to ellipsoid()
Result sym_ellipsoid(int N, mat const& A, const vector<double>& InvD, double r,
                     unsigned int octant = 1) {
    auto start = chrono::steady_clock::now();
    auto r2 = r*r;

    mat Accu(N, N);
    Accu.row(N-1).zeros();

    Result result{0, 0, 0};
    // 2*octant+1 forces the points into the upper half plane, skipping 0
    // This way we use the lattice symmetry and calculate only half the points
    ellipsoid(N-1, A, &InvD[0], Accu, result, r2, 2*octant+1);
    // Compensate for the extra factor exp(r*r) we always add in ellipsoid()
    result.sum /= exp(r2);
    auto end = chrono::steady_clock::now();
    result.elapsed = chrono::duration<double>{end-start}.count();

    return result;
}

// Prepare multithreaded use of sym_ellipsoid(). Each thread gets 1 octant
Result sym_ellipsoid_t(int N, mat const& A, const vector<double>& InvD, double r, unsigned int nr_threads) {
    nr_threads = pow(2, ceil(log2(nr_threads)));

    vector<future<Result>> results;
    for (auto i=nr_threads+1; i<2*nr_threads; ++i)
        results.emplace_back(async(launch::async, sym_ellipsoid, N, ref(A), ref(InvD), r, i));
    auto result = sym_ellipsoid(N, A, InvD, r, nr_threads);
    for (auto i=0U; i<nr_threads-1; ++i) result += results[i].get();
    return result;
}

int main(int argc, char* const* argv) {
    cin.exceptions(ios::failbit | ios::badbit);
    cout.precision(12);

    double epsilon    = EPSILON; // Target absolute error
    bool inv_modular  = true;    // Use modular transform to get the best matrix
    bool lat_reduce   = true;    // Use lattice reduction to align the ellipsoid
    bool conservative = false;   // Use provable error bound instead of a guess
    bool eigen_values = false;   // Show eigenvalues
    int  threads_max  = thread::hardware_concurrency();

    int option_char;
    while ((option_char = getopt(argc, argv, "p:n:MRce")) != EOF)
        switch (option_char) {
            case 'p': epsilon      = atof(optarg); break;
            case 'n': threads_max  = atoi(optarg); break;
            case 'M': inv_modular  = false;        break;
            case 'R': lat_reduce   = false;        break;
            case 'c': conservative = true;         break;
            case 'e': eigen_values = true;         break;
            default:
              cerr << "usage: " << argv[0] << " [-p epsilon] [-n threads] [-M] [-R] [-e] [-c]" << endl;
              exit(EXIT_FAILURE);
        }
    if (optind < argc) {
        cerr << "Unexpected argument" << endl;
        exit(EXIT_FAILURE);
    }
    if (threads_max < 1) threads_max = 1;
    threads_max = pow(2, ceil(log2(threads_max)));
    cout << "Using up to " << threads_max << " threads" << endl;

    int N;
    cin >> N;

    mat P(N, N);
    for (auto& v: P) cin >> v;

    if (eigen_values) {
        vec eigval = eig_sym(P);
        cout << "Eigenvalues:\n" << eigval << endl;
    }

    // Decompose P = A * A.t()
    mat A = chol(P, "lower");

    // Calculate lattice determinant
    double lat_det = 1;
    for (auto i=0; i<N; ++i) {
        if (A(i,i) <= 0) throw(logic_error("Diagonal not Positive"));
        lat_det *= A(i,i);
    }
    cout << "Lattice determinant=" << lat_det << endl;

    auto factor = lat_det / pow(M_PI, N/2.0);
    if (inv_modular && factor < 1) {
        epsilon *= factor;
        cout << "Lattice determinant is small. Using inverse instead. Factor=" << factor << endl;
        P = M_PI * M_PI * inv(P);
        A = chol(P, "lower");
        // We could simple calculate the new lat_det as pow(M_PI,N)/lat_det
        lat_det = 1;
        for (auto i=0; i<N; ++i) {
            if (A(i,i) <= 0) throw(logic_error("Diagonal not Positive"));
            lat_det *= A(i,i);
        }
        cout << "New lattice determinant=" << lat_det << endl;
    } else
        factor = 1;

    // Prepare for lattice reduction.
    // Since the library works on integer lattices we will scale up our matrix
    double min = INFINITY;
    for (auto i=0; i<N; ++i) {
        for (auto j=0; j<N;++j)
            if (A(i,j) != 0 && abs(A(i,j) < min)) min = abs(A(i,j));
    }

    auto upscale = UPSCALE/min;
    mat_ZZ a;
    a.SetDims(N,N);
    for (auto i=0; i<N; ++i)
        for (auto j=0; j<N;++j) a[i][j] = to_ZZ(A(i,j)*upscale);

    // Finally do the actual lattice reduction
    mat_ZZ u;
    auto rank = G_BKZ_FP(a, u);
    if (rank != N) throw(logic_error("Matrix is singular"));
    mat U(N,N);
    for (auto i=0; i<N;++i)
        for (auto j=0; j<N;++j) U(i,j) = to_double(u[i][j]);

    // There should now be a short lattice vector at row 0
    ZZ sum = to_ZZ(0);
    for (auto j=0; j<N;++j) sum += a[0][j]*a[0][j];
    auto rho = sqrt(to_double(sum))/upscale;
    cout << "Rho=" << rho << " (integer square " <<
        rho*rho << " ~ " <<
        static_cast<int>(rho*rho+0.5) << ")" << endl;

    // Lattice reduction doesn't gain us anything conceptually.
    // The same number of points is evaluated for the same exponential values
    // However working through the ellipsoid dimensions from large lattice
    // base vectors to small makes ellipsoid() a *lot* faster
    if (lat_reduce) {
        mat B = U * A;
        P = B * B.t();
        A = chol(P, "lower");
        if (eigen_values) {
            vec eigval = eig_sym(P);
            cout << "New eigenvalues:\n" << eigval << endl;
        }
    }

    vector<double> InvD(N);;
    for (auto i=0; i<N; ++i) InvD[i] = 1 / A(i, i);

    // Calculate radius needed for target precision
    auto r = radius(N, rho, lat_det, epsilon);
    cout << "Safe R=" << r << endl;

    auto nr_threads = threads_max;
    Result result;
    if (conservative) {
        // Walk all points inside the ellipsoid with transformed radius r
        result = sym_ellipsoid_t(N, A, InvD, r, nr_threads);
    } else {
        // First grow the radius until we saw POINTS_MIN points or reach the
        // target radius
        double i = floor(N * log2(r/rho) / log2(GROW));
        if (i < 0) i = 0;
        auto R = r * pow(GROW, -i/N);
        cout << "Initial R=" << R << endl;
        result = sym_ellipsoid_t(N, A, InvD, R, nr_threads);
        nr_threads = result.elapsed < THREAD_SEC ? 1 : threads_max;
        auto max_new_points = result.points;
        while (--i >= 0 && result.points < POINTS_MIN) {
            R = r * pow(GROW, -i/N);
            auto change = result;
            result = sym_ellipsoid_t(N, A, InvD, R, nr_threads);
            nr_threads = result.elapsed < THREAD_SEC ? 1 : threads_max;
            change = result - change;

            if (change.points > max_new_points) max_new_points = change.points;
        }

        // Now we have enough points that it's worth bothering to use threads
        while (--i >= 0) {
            R = r * pow(GROW, -i/N);
            auto change = result;
            result = sym_ellipsoid_t(N, A, InvD, R, nr_threads);
            nr_threads = result.elapsed < THREAD_SEC ? 1 : threads_max;
            change = result - change;
            // This is probably too crude and might misestimate the error
            // I've never seen it fail though
            if (change.points > max_new_points) {
                max_new_points = change.points;
                if (change.sum < epsilon/2) break;
            }
        }
        cout << "Final R=" << R << endl;
    }

    // We calculated half the points and skipped 0.
    result.sum = 2*result.sum+1;

    // Modular transform factor
    result.sum /= factor;

    // Report result
    cout <<
        "Evaluated " << result.points << " points\n" <<
        "Sum = " << result.sum << endl;
}
Ton Hospel
fuente
Esto es muy impresionante y mucho mejor que el enfoque ingenuo en mi opinión. Espero con interés la documentación :)
1
@TonHospel ¿Puedes contarnos un poco más sobre cómo se te ocurren los límites?
flawr
2
Estoy usando Arch Linux y necesitaba la -lgslcblasbandera para compilar. Increíble respuesta por cierto!
Rhyzomatic
2

Python 3

12 segundos n = 8 en mi computadora, ubuntu 4 core.

Realmente ingenuo, no tengo idea de lo que estoy haciendo.

from itertools import product
from math import e

P = [[ 6., -3.,  3., -3.,  3.],
     [-3.,  6., -5.,  5., -5.],
     [ 3., -5.,  6., -5.,  5.],
     [-3.,  5., -5.,  6., -5.],
     [ 3., -5.,  5., -5.,  6.]]

N = 2

n = [1]

while e** -n[-1] > 0.0001:
    n = []
    for x in product(list(range(-N, N+1)), repeat = len(P)):
        n.append(sum(k[0] * k[1] for k in zip([sum(j[0] * j[1] for j in zip(i, x)) for i in P], x)))
    N += 1

print(sum(e** -i for i in n))

Esto seguirá aumentando el rango Zque usa hasta que obtenga una respuesta lo suficientemente buena. Escribí mi propia matriz de multiplicación, prolly debería usar numpy.

Maltysen
fuente
Gracias ! ¿Puedes mostrar algunas salidas y tiempos en tu computadora?
Su código se ejecuta en pypy, lo cual es excelente y rápido. Desafortunadamente, [[6.0, -1.0, -3.0, 1.0, 3.0, -1.0, -3.0, 1.0, 3.0], [-1.0, 6.0, -1.0, -5.0, 1.0, 5.0, -1.0, -5.0, 1.0 ], [-3.0, -1.0, 6.0, 1.0, -5.0, -1.0, 5.0, 1.0, -5.0], [1.0, -5.0, 1.0, 6.0, -1.0, -5.0, 1.0, 5.0, -1.0] , [3.0, 1.0, -5.0, -1.0, 6.0, 1.0, -5.0, -1.0, 5.0], [-1.0, 5.0, -1.0, -5.0, 1.0, 6.0, -1.0, -5.0, 1.0], [-3.0, -1.0, 5.0, 1.0, -5.0, -1.0, 6.0, 1.0, -5.0], [1.0, -5.0, 1.0, 5.0, -1.0, -5.0, 1.0, 6.0, -1.0], [ 3.0, 1.0, -5.0, -1.0, 5.0, 1.0, -5.0, -1.0, 6.0]] da la respuesta incorrecta.
8.1443647932-8.14381938863 = 0.00054540457> 0.0001.
3
@Maltysen Su programa solo verifica si el último término es más pequeño que la precisión dada. ¡Pero el error que cometes es mucho más grande, ya que también deberías considerar la suma de todos los otros términos para el error!
flawr