Repository: cupy/cupy

Issue:
matrix power is missing from cupy.linalg.
There is a corresponding `numpy.linalg.matrix_power` in NumPy. Is there a reason why this has not made it into CuPy?

If it helps, I am willing to take a shot at this one, though I might need a bit of hand-holding as I've not much experience with Cython code.


Discussion:
Thanks, I believe there's no specific reasons not to add it. (maybe it can be done without Cython)
https://github.com/cupy/cupy/blob/v5.0.0b1/cupy/linalg/product.py#L197

Generally, contribution of implementing the corresponding method of `numpy`/`scipy` is always welcomed 😉
WOOHOO!! I have a first implementation available! Essentially just copied over the `numpy` implementation, and tweaked it so that it somewhat follows CuPy coding conventions. 

Going to PR in, but I am sure I am going to need guidance on tests to write!

Resolving patch:
diff --git a/cupy/linalg/__init__.py b/cupy/linalg/__init__.py
--- a/cupy/linalg/__init__.py
+++ b/cupy/linalg/__init__.py
@@ -25,3 +25,5 @@
 from cupy.linalg.solve import solve  # NOQA
 from cupy.linalg.solve import tensorinv  # NOQA
 from cupy.linalg.solve import tensorsolve  # NOQA
+
+from cupy.linalg.product import matrix_power  # NOQA
diff --git a/cupy/linalg/product.py b/cupy/linalg/product.py
--- a/cupy/linalg/product.py
+++ b/cupy/linalg/product.py
@@ -7,6 +7,7 @@
 from cupy import core
 from cupy import internal
 
+from cupy.linalg.solve import inv
 
 matmul = core.matmul
 
@@ -194,7 +195,49 @@ def tensordot(a, b, axes=2):
     return core.tensordot_core(a, b, None, n, m, k, ret_shape)
 
 
-# TODO(okuta): Implement matrix_power
+def matrix_power(M, n):
+    """Raise a square matrix to the (integer) power `n`.
+
+    Args:
+        M (~cupy.ndarray): Matrix to raise by power n.
+        n (~int): Power to raise matrix to.
+
+    Returns:
+        ~cupy.ndarray: Output array.
+
+    .. note:: M must be of dtype `float32` or `float64`.
+
+    ..seealso:: :func:`numpy.linalg.matrix_power`
+    """
+    if M.ndim != 2 or M.shape[0] != M.shape[1]:
+        raise ValueError("input must be a square array")
+    if not isinstance(n, six.integer_types):
+        raise TypeError("exponent must be an integer")
+
+    if n == 0:
+        return cupy.identity(M.shape[0], dtype=M.dtype)
+    elif n < 0:
+        M = inv(M)
+        n *= -1
+
+    # short-cuts
+    if n <= 3:
+        if n == 1:
+            return M
+        elif n == 2:
+            return cupy.matmul(M, M)
+        else:
+            return cupy.matmul(cupy.matmul(M, M), M)
+
+    # binary decomposition to reduce the number of Matrix
+    # multiplications for n > 3.
+    result, Z = None, None
+    for b in cupy.binary_repr(n)[::-1]:
+        Z = M if Z is None else cupy.matmul(Z, Z)
+        if b == '1':
+            result = Z if result is None else cupy.matmul(result, Z)
+
+    return result
 
 
 def kron(a, b):


Test patch:
diff --git a/tests/cupy_tests/linalg_tests/test_product.py b/tests/cupy_tests/linalg_tests/test_product.py
--- a/tests/cupy_tests/linalg_tests/test_product.py
+++ b/tests/cupy_tests/linalg_tests/test_product.py
@@ -2,6 +2,7 @@
 
 import numpy
 
+from cupy import cuda
 from cupy import testing
 
 
@@ -346,3 +347,67 @@ def test_zerodim_kron(self, xp, dtype):
         a = xp.array(2, dtype=dtype)
         b = testing.shaped_arange((4, 5), xp, dtype)
         return xp.kron(a, b)
+
+
+@unittest.skipUnless(
+    cuda.cusolver_enabled, 'Requires CUDA 8.0 for cuSOLVER')
+class TestMatrixPower(unittest.TestCase):
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_allclose()
+    def test_matrix_power_0(self, xp, dtype):
+        a = testing.shaped_arange((3, 3), xp, dtype)
+        return xp.linalg.matrix_power(a, 0)
+
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_allclose()
+    def test_matrix_power_1(self, xp, dtype):
+        a = testing.shaped_arange((3, 3), xp, dtype)
+        return xp.linalg.matrix_power(a, 1)
+
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_allclose()
+    def test_matrix_power_2(self, xp, dtype):
+        a = testing.shaped_arange((3, 3), xp, dtype)
+        return xp.linalg.matrix_power(a, 2)
+
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_allclose()
+    def test_matrix_power_3(self, xp, dtype):
+        a = testing.shaped_arange((3, 3), xp, dtype)
+        return xp.linalg.matrix_power(a, 3)
+
+    @testing.for_float_dtypes(no_float16=True)
+    @testing.numpy_cupy_allclose(rtol=1e-5)
+    def test_matrix_power_inv1(self, xp, dtype):
+        a = testing.shaped_arange((3, 3), xp, dtype) ** 2
+        return xp.linalg.matrix_power(a, -1)
+
+    @testing.for_float_dtypes(no_float16=True)
+    @testing.numpy_cupy_allclose(rtol=1e-5)
+    def test_matrix_power_inv2(self, xp, dtype):
+        a = testing.shaped_arange((3, 3), xp, dtype) ** 2
+        return xp.linalg.matrix_power(a, -2)
+
+    @testing.for_float_dtypes(no_float16=True)
+    @testing.numpy_cupy_allclose(rtol=1e-4)
+    def test_matrix_power_inv3(self, xp, dtype):
+        a = testing.shaped_arange((3, 3), xp, dtype) ** 2
+        return xp.linalg.matrix_power(a, -3)
+
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_allclose()
+    def test_matrix_power_of_two(self, xp, dtype):
+        a = xp.eye(23, k=17, dtype=dtype) + xp.eye(23, k=-6, dtype=dtype)
+        return xp.linalg.matrix_power(a, 1 << 50)
+
+    @testing.for_all_dtypes()
+    @testing.numpy_cupy_allclose()
+    def test_matrix_power_large(self, xp, dtype):
+        a = xp.eye(23, k=17, dtype=dtype) + xp.eye(23, k=-6, dtype=dtype)
+        return xp.linalg.matrix_power(a, 123456789123456789)
+
+    @testing.for_float_dtypes(no_float16=True)
+    @testing.numpy_cupy_allclose()
+    def test_matrix_power_invlarge(self, xp, dtype):
+        a = xp.eye(23, k=17, dtype=dtype) + xp.eye(23, k=-6, dtype=dtype)
+        return xp.linalg.matrix_power(a, -987654321987654321)