Home > other >  Using map() on a function with multiple inputs to get rid of for loops
Using map() on a function with multiple inputs to get rid of for loops

Time:06-02

Context:

I have a function to upsample multiple arrays that I want to write as efficiently as possible (because I have to run it 370000 times).

This function takes multiple inputs and is composed of 2 for loops. To upsample my arrays, I loop over this function with a parameter k, and I would like to get rid of this loop (which sits outside of the function). I tried using a mix of map() and list-comprehension to minimize my computing time but I can't make it work.

Question:

How to get my map() part of the code working (see last section of code) ? Is there a better way than map() to get rid of for loops ?

Summary:

  • Function interpolate_and_get_results: 2 for loops. Takes 3D, 2D arrays and int as inputs
  • This function is ran inside a for loop (parameter k) that I want to get rid of

I wrote some example code, the part with map() does not work because I can't think of a way to pass the k parameter as a list, but also an input.

Thank you !

ps: code to parallelize the interpolation function that I do not use for this example

import numpy as np
import time

#%% --- SETUP OF THE PROBLEM --- %%#

temperatures = np.random.rand(10,4,7)*100
precipitation = np.random.rand(10,4,7)
snow = np.random.rand(10,4,7)

# Flatten the arrays to make them iterable with map()
temperatures = temperatures.reshape(10,4*7)
precipitation = precipitation.reshape(10,4*7)
snow = snow.reshape(10,4*7)

# Array of altitudes to "adjust" the temperatures
alt = np.random.rand(4,7)*1000

# Flatten the array
alt = alt.reshape(4*7)

# Weight Matrix

w = np.random.rand(4*7, 1000, 1000)


#%% Function
def interpolate_and_get_results(temp, prec, Eprec, w, i, k):
    
    # Do some calculations
    factor1 = ((temperatures[i,k]-272.15)   (-alt[k] * -6/1000))
    factor2 = precipitation[i,k]
    factor3 = snow[i,k]
    
    # Iterate through every cell of the upsampled arrays
    for i in range(w.shape[1]):
        for j in range(w.shape[2]):
            val = w[k, i, j]
            temp[i, j]  = factor1 * val
            prec[i, j]  = factor2 * val
            Eprec[i, j]  = factor3 * val


    

#%% --- Function call without loop simplification --- ##%
# Prepare a template array
dummy = np.zeros((w.shape[1], w.shape[2]))

# Initialize the global arrays to be filled
tempYEAR2 = np.zeros((9, dummy.shape[0], dummy.shape[1]))
precYEAR2 = np.zeros((9, dummy.shape[0], dummy.shape[1]))
EprecYEAR2 = np.zeros((9, dummy.shape[0], dummy.shape[1]))

ts = time.time()
for i in range(temperatures.shape[0]):
    
    # Create empty host arrays
    temp = dummy.copy()
    prec = dummy.copy()
    Eprec = dummy.copy()
    
    for k in range(w.shape[0]):
         interpolate_and_get_results(temp, prec, Eprec, w, i, k)

print('Time: ', (time.time()-ts))




#%% --- With Map (DOES NOT WORK)  --- %%#
del k

dummy = np.zeros((w.shape[1], w.shape[2]))

# Initialize the global arrays to be filled
tempYEAR2 = np.zeros((9, dummy.shape[0], dummy.shape[1]))
precYEAR2 = np.zeros((9, dummy.shape[0], dummy.shape[1]))
EprecYEAR2 = np.zeros((9, dummy.shape[0], dummy.shape[1]))

# Create a list k to be iterated through with the map() function
k = [k for k in range(0, temperatures.shape[1])]

for i in range(temperatures.shape[0]):
    
    # Create empty host arrays
    temp = dummy.copy()
    prec = dummy.copy()
    Eprec = dummy.copy()
    
    # Call the interpolate function with map() iterating through k
    map(interpolate_and_get_results(temp, prec, Eprec, w, i, k), k)

Code from @Jérôme Richard using numba added at the request of user @ken (takes 48.81s to run on my pc):

import numpy as np
import multiprocessing as mp
import time



#%% ------ Create data ------ ###
temperatures = np.random.rand(10,4,7)*100
precipitation = np.random.rand(10,4,7)
snow = np.random.rand(10,4,7)

# Array of altitudes to "adjust" the temperatures
alt = np.random.rand(4,7)*1000


#%% ------ IDW Interpolation ------ ###

# We create a weight matrix that we use to upsample our temperatures, precipitations and snow matrices
# This part is not that important, it works well as it is

MX,MY = np.shape(temperatures[0])
N = 300

T = np.zeros([N*MX 1, N*MY 1])

# create NxM inverse distance weight matrices based on Gaussian interpolation

x = np.arange(0,N*MX 1)
y = np.arange(0,N*MY 1)
X,Y = np.meshgrid(x,y)

k = 0
w =  np.zeros([MX*MY,N*MX 1,N*MY 1])
for mx in range(MX):
    for my in range(MY):

        # Gaussian
        add_point = np.exp(-((mx*N-X.T)**2 (my*N-Y.T)**2)/N**2)
        w[k,:,:]  = add_point
        k  = 1

sum_weights = np.sum(w, axis=0)
for k in range(MX*MY):
    w[k,:,:] /= sum_weights






#%% --- Function --- %%#
# Code from Jérôme Richard: https://stackoverflow.com/questions/72399050/parallelize-three-nested-loops/72399494?noredirect=1#comment127919686_72399494

import numba as nb

# get_results   interpolator
@nb.njit('void(float64[:,::1], float64[:,::1], float64[:,::1], float64[:,:,::1], int_, int_, int_, int_)', parallel=True)
def interpolate_and_get_results(temp, prec, Eprec, w, i, k, mx, my):
    factor1 = ((temperatures[i,mx,my]-272.15)   (-alt[mx, my] * -6/1000))
    factor2 = precipitation[i,mx,my]
    factor3 = snow[i,mx,my]

    # Filling the
    for i in nb.prange(w.shape[1]):
        for j in range(w.shape[2]):
            val = w[k, i, j]
            temp[i, j]  = factor1 * val
            prec[i, j]  = factor2 * val
            Eprec[i, j]  = factor3 * val




#%% --- Main Loop --- %%#



ts = time.time()
if __name__ == '__main__':

    dummy = np.zeros((w.shape[1], w.shape[2]))

    # Initialize the permanent arrays to be filled
    tempYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))
    precYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))
    EprecYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))
    smbYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))
    
    # Initialize semi-permanent array
    smb = np.zeros((dummy.shape[0], dummy.shape[1]))



    # Loop over the "time" axis
    for i in range(0, temperatures.shape[0]):

        # Create empty semi-permanent arrays
        temp = dummy.copy()
        prec = dummy.copy()
        Eprec = dummy.copy()

        # Loop over the different weights
        for k in range(w.shape[0]):

            # Loops over the cells of the array to be upsampled
            for mx in range(MX):
                for my in range(MY):

                    interpolate_and_get_results(temp, prec, Eprec, w, i, k, mx, my)

        # At each timestep, update the semi-permanent array using the results from the interpolate function
        smb[np.logical_and(temp <= 0, prec > 0)]  = prec[np.logical_and(temp <= 0, prec > 0)]

        # Fill the permanent arrays (equivalent of storing the results at the end of every year)
        # and reinitialize the semi-permanent array every 5th timestep
        if i%5 == 0:
            
            # Permanent
            tempYEAR[int(i/5)] = temp
            precYEAR[int(i/5)] = prec
            EprecYEAR[int(i/5)] = Eprec
            smbYEAR[int(i/5)] = smb

            # Semi-permanent
            smb = np.zeros((dummy.shape[0], dummy.shape[1]))


print("Time spent:", time.time()-ts)
  

CodePudding user response:

Note: This answer is not about how to use map, it's about "a better way".

You are doing a lot of redundant calculations. Believe it or not, this code outputs the same result.

# No change in the initialization section above.

ts = time.time()
if __name__ == '__main__':

    dummy = np.zeros((w.shape[1], w.shape[2]))

    # Initialize the permanent arrays to be filled
    tempYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))
    precYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))
    EprecYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))
    smbYEAR = np.zeros((9, dummy.shape[0], dummy.shape[1]))

    smb = np.zeros((dummy.shape[0], dummy.shape[1]))

    temperatures_inter = temperatures - 272.15
    w_inter = w.sum(axis=0)
    alt_inter = (alt * (-6 / 1000)).sum()

    for i in range(0, temperatures_inter.shape[0]):
        temp_i = (temperatures_inter[i].sum() - alt_inter) * w_inter
        prec_i = precipitation[i].sum() * w_inter
        Eprec_i = snow[i].sum() * w_inter

        condition = np.logical_and(temp_i <= 0, prec_i > 0)
        smb[condition]  = prec_i[condition]

        if i % 5 == 0:
            tempYEAR[i // 5] = temp_i
            precYEAR[i // 5] = prec_i
            EprecYEAR[i // 5] = Eprec_i
            smbYEAR[i // 5] = smb
            smb = np.zeros((dummy.shape[0], dummy.shape[1]))

print("Time spent:", time.time() - ts)

I verified the results by comparing them to the output of the code that uses numba. The difference is about 0.0000001, which is probably caused by rounding error.

print((tempYEAR_from_yours - tempYEAR_from_mine).sum())  # -8.429287845501676e-08
print((precYEAR_from_yours - precYEAR_from_mine).sum())  # 2.595697878859937e-09
print((EprecYEAR_from_yours - EprecYEAR_from_mine).sum())  # -7.430216442116944e-09
print((smbYEAR_from_yours - smbYEAR_from_mine).sum())  # -6.875431779462815e-09

On my PC, this code took 0.36 seconds. It does not use numba and is not even parallelized. It just eliminated redundancy.

  • Related