import math
import numpy as np
from numba import cuda, float64, int8, int32
from numba.cuda.testing import unittest, CUDATestCase


def cu_mat_power(A, power, power_A):
    y, x = cuda.grid(2)

    m, n = power_A.shape
    if x >= n or y >= m:
        return

    power_A[y, x] = math.pow(A[y, x], int32(power))


def cu_mat_power_binop(A, power, power_A):
    y, x = cuda.grid(2)

    m, n = power_A.shape
    if x >= n or y >= m:
        return

    power_A[y, x] = A[y, x] ** power


def vec_pow(r, x, y):
    i = cuda.grid(1)

    if i < len(r):
        r[i] = pow(x[i], y[i])


def vec_pow_binop(r, x, y):
    i = cuda.grid(1)

    if i < len(r):
        r[i] = x[i] ** y[i]


def vec_pow_inplace_binop(r, x):
    i = cuda.grid(1)

    if i < len(r):
        r[i] **= x[i]


def random_complex(N):
    np.random.seed(123)
    return (np.random.random(1) + np.random.random(1) * 1j)


class TestCudaPowi(CUDATestCase):
    def test_powi(self):
        dec = cuda.jit(argtypes=[float64[:, :], int8, float64[:, :]])
        kernel = dec(cu_mat_power)

        power = 2
        A = np.arange(10, dtype=np.float64).reshape(2, 5)
        Aout = np.empty_like(A)
        kernel[1, A.shape](A, power, Aout)
        self.assertTrue(np.allclose(Aout, A ** power))

    def test_powi_binop(self):
        dec = cuda.jit(argtypes=[float64[:, :], int8, float64[:, :]])
        kernel = dec(cu_mat_power_binop)

        power = 2
        A = np.arange(10, dtype=np.float64).reshape(2, 5)
        Aout = np.empty_like(A)
        kernel[1, A.shape](A, power, Aout)
        self.assertTrue(np.allclose(Aout, A ** power))

    # Relative tolerance kwarg is provided because 1.0e-7 (the default for
    # assert_allclose) is a bit tight for single precision.
    def _test_cpow(self, dtype, func, rtol=1.0e-7):
        N = 32
        x = random_complex(N).astype(dtype)
        y = random_complex(N).astype(dtype)
        r = np.zeros_like(x)

        cfunc = cuda.jit(func)
        cfunc[1, N](r, x, y)
        np.testing.assert_allclose(r, x ** y, rtol=rtol)

        # Checks special cases
        x = np.asarray([0.0j, 1.0j], dtype=dtype)
        y = np.asarray([0.0j, 1.0], dtype=dtype)
        r = np.zeros_like(x)

        cfunc[1, 2](r, x, y)
        np.testing.assert_allclose(r, x ** y, rtol=rtol)

    def test_cpow_complex64_pow(self):
        self._test_cpow(np.complex64, vec_pow, rtol=3.0e-7)

    def test_cpow_complex64_binop(self):
        self._test_cpow(np.complex64, vec_pow_binop, rtol=3.0e-7)

    def test_cpow_complex128_pow(self):
        self._test_cpow(np.complex128, vec_pow)

    def test_cpow_complex128_binop(self):
        self._test_cpow(np.complex128, vec_pow_binop)

    def _test_cpow_inplace_binop(self, dtype, rtol=1.0e-7):
        N = 32
        x = random_complex(N).astype(dtype)
        y = random_complex(N).astype(dtype)
        r = x ** y

        cfunc = cuda.jit(vec_pow_inplace_binop)
        cfunc[1, N](x, y)
        np.testing.assert_allclose(x, r, rtol=rtol)

    def test_cpow_complex64_inplace_binop(self):
        self._test_cpow_inplace_binop(np.complex64, rtol=3.0e-7)

    def test_cpow_complex128_inplace_binop(self):
        self._test_cpow_inplace_binop(np.complex128, rtol=3.0e-7)


if __name__ == '__main__':
    unittest.main()
