import numpy as np
import numba

np.random.seed(0)

data = np.random.randn(2000, 2000)

"""
Explanation: Volvemos al torneo del rendimiento!!!
Recapitulando. Un artículo sobre Cython donde conseguíamos mejoras de velocidad de código Python con numpy arrays de 40x usando Cython desembocó en mejoras de 70x usando numba. En esta tercera toma vamos a ver si con Cython conseguimos las velocidades de numba tomando algunas ideas de la implementación de JuanLu y definiendo una función un poco más inteligente que mi implementación con Cython (busca_min_cython9).
Preparamos el setup inicial.
End of explanation
"""


def busca_min_np_jit(malla):
    minimos = np.zeros_like(malla, dtype=bool)
    _busca_min(malla, minimos)
    return minimos  # en lugar de 'return np.nonzero(minimos)'

@numba.jit(nopython=True)
def _busca_min(malla, minimos):
    for i in range(1, malla.shape[1]-1):
        for j in range(1, malla.shape[0]-1):
            if (malla[j, i] < malla[j-1, i-1] and
                malla[j, i] < malla[j-1, i] and
                malla[j, i] < malla[j-1, i+1] and
                malla[j, i] < malla[j, i-1] and
                malla[j, i] < malla[j, i+1] and
                malla[j, i] < malla[j+1, i-1] and
                malla[j, i] < malla[j+1, i] and
                malla[j, i] < malla[j+1, i+1]):
                minimos[i, j] = True

%timeit -n 100 busca_min_np_jit(data)

"""
Explanation: JuanLu hizo alguna trampa usando un numpy array en lugar de dos listas y devolviendo el resultado usando numpy.nonzero. En realidad no es trampa, es pura envidia mía al ver que ha usado una forma más inteligente de conseguir lo mismo que hacía mi función original :-P
Usando esa implementación considero que es más inteligente tener un numpy array de salida por lo que el uso de np.nonzero sería innecesario y añadiría algo de pérdida de rendimiento si luego vamos a seguir trabajando con numpy arrays. Por tanto, la implementación de JuanLu eliminando el uso de numpy.nonzero sería:
End of explanation
"""


def busca_min(malla):
    minimos = np.zeros_like(malla)
    for i in range(1, malla.shape[1]-1):
        for j in range(1, malla.shape[0]-1):
            if (malla[j, i] < malla[j-1, i-1] and
                malla[j, i] < malla[j-1, i] and
                malla[j, i] < malla[j-1, i+1] and
                malla[j, i] < malla[j, i-1] and
                malla[j, i] < malla[j, i+1] and
                malla[j, i] < malla[j+1, i-1] and
                malla[j, i] < malla[j+1, i] and
                malla[j, i] < malla[j+1, i+1]):
                minimos[i, j] = 1

    return minimos

%timeit busca_min(data)

"""
Explanation: Ejecutándolo 100 veces obtenemos un valor más bajo de 33.6 ms devolviendo un numpy.array de 1's y 0's con los 1's indicando la posición de los máximos.
La implementación original la vamos a modificar un poco para que devuelva lo mismo.
End of explanation
"""


# antes cythonmagic
%load_ext Cython

"""
Explanation: Los tiempos son similares a la función original y, aunque estamos usando más memoria, tenemos una mejora con numba que ya llega a los dos órdenes de magnitud (alrededor de 100x!!) y una función más usable para trabajar con numpy.
Vamos a modificar la opción Cython más rápida que obtuvimos para que se comporte igual que las de Numba y Python.
Primero cargamos la extensión Cython.
End of explanation
"""


%%cython --annotate
import numpy as np
from cython cimport boundscheck, wraparound

cpdef char[:,::1] busca_min_cython10(double[:, ::1] malla):
    cdef unsigned int i, j
    cdef unsigned int ii = malla.shape[1]-1
    cdef unsigned int jj = malla.shape[0]-1
    cdef char[:,::1] minimos = np.zeros_like(malla, dtype = np.int8)
    #minimos[...] = 0
    cdef unsigned int start = 1
    #cdef float [:, :] malla_view = malla
    with boundscheck(False), wraparound(False):
        for j in range(start, ii):
            for i in range(start, jj):
                if (malla[j, i] < malla[j-1, i-1] and
                    malla[j, i] < malla[j-1, i] and
                    malla[j, i] < malla[j-1, i+1] and
                    malla[j, i] < malla[j, i-1] and
                    malla[j, i] < malla[j, i+1] and
                    malla[j, i] < malla[j+1, i-1] and
                    malla[j, i] < malla[j+1, i] and
                    malla[j, i] < malla[j+1, i+1]):
                    minimos[i,j] = 1

    return minimos

"""
Explanation: Vamos a usar la opción annotate para ver cuanto 'blanco' tenemos y la nueva versión Cython la vamos a llamar busca_min_cython10.
End of explanation
"""


%timeit -n 100 busca_min_cython10(data)

"""
Explanation: Vemos que la mayor parte está en 'blanco'. Eso significa que estamos evitando usar la C-API de CPython y la mayor parte sucede en C. Estoy usando typed memoryviews que permite trabajar de forma 'transparente' numpy arrays.
Vamos a ejecutar la nueva versión 100 veces, de la misma forma que hemos hecho con Numba:
End of explanation
"""


res_numba = busca_min_np_jit(data)
res_cython = busca_min_cython10(data)
res_python = busca_min(data)

np.testing.assert_array_equal(res_numba, res_cython)
np.testing.assert_array_equal(res_numba, res_python)
np.testing.assert_array_equal(res_cython, res_python)

"""
Explanation: Wow, virtualmente obtenemos la misma velocidad entre Numba y Cython y dos órdenes de magnitud de mejora con respecto a la versión Python.
End of explanation
"""


data = np.random.randn(500, 500)
%timeit -n 3 busca_min_np_jit(data)
%timeit -n 3 busca_min_cython10(data)
%timeit busca_min(data)

data = np.random.randn(5000, 5000)
%timeit -n 3 busca_min_np_jit(data)
%timeit -n 3 busca_min_cython10(data)
%timeit busca_min(data)

"""
Explanation: Parece que el resultado es el mismo en todo momento
Probemos con arrays de menos y más tamaño.
End of explanation
"""


