Reputation: 581
Following are 2 classes,which do some calculation based on input and give back some output. One class is implemented in python another is converted into numpy array. So obviously 2 classes with python codes and numpy. Both classes return the same result. Purpose of numpy class is to speed up running performance. It turns out that in that case numpy class with array takes for ages to perfom the task, whereas the quicker performance is expected, Perhaps for loop slows numpy class. longness of class may not hope to bother you. Why numpy is slower and take more time? What is wrong?
A class with implementation of python:
import math
import numpy as np
from time import time
from itertools import islice, count
## A function to generate for-loop numbers with decimals.
def seq(start, end, step):
assert(step != 0)
sample_count = int(abs(end - start) / step)
return islice(count(start, step), sample_count)
class ULSMomentCapacity(object):
def __init__(self, WIDTH, HEIGHT, SIZE_OF_REBAR_TOP, SIZE_OF_REBAR_BOTTOM, SIZE_OF_STIRRUP, STEEL_E_MODULE, STEEL_STRENGTH, COMPRESSIVE_STRENGTH_CONCRETE, CONCRETE_STRAIN,
MAX_STEEL_STRAIN, MIN_STEEL_STRAIN, EXTERNAL_MOMENT):
self.width, self.height = WIDTH, HEIGHT
self.rebar_size_top, self.rebar_size_bottom, self.rebar_size_stirrup = SIZE_OF_REBAR_TOP, SIZE_OF_REBAR_BOTTOM, SIZE_OF_STIRRUP
self.max_steel_strain, self.min_steel_strain, self.concrete_strain = 0.08, 0.002, 0.0035
self.steel_E_module, self.steel_strength, self.compressive_concrete_strength = STEEL_E_MODULE, STEEL_STRENGTH, COMPRESSIVE_STRENGTH_CONCRETE
self.external_moment = EXTERNAL_MOMENT
self.number_of_rebars_xy = (7, 4)
self.minimum_numbers_top_rebars = 2
self.k1 = 0.8
def getdata(self):
self.effective_distance_y = [self.height - y for y in [63.0, 103.5, 144.0, 184.5, 225.0] for x in range(self.number_of_rebars_xy[0]) ]
self.effective_distance_y_top = [63, 63]
self.effective_distance_y_bottom = [ y for y in [63.0, 103.5, 144.0, 184.5, 225.0] for x in range(self.number_of_rebars_xy[0]) ]
LAMBDA_AREA_REBARS = lambda SIZE_OF_REBAR:int(math.pi/4*(SIZE_OF_REBAR)**2)
self.AREA_OF_REBARS_TOP = [LAMBDA_AREA_REBARS(self.rebar_size_top) for x in range(self.minimum_numbers_top_rebars)]
MAX_NUMBER_REBAR =self.number_of_rebars_xy[0]*self.number_of_rebars_xy[1]
for generator_rebars in np.arange(2, MAX_NUMBER_REBAR+1):
self.AREA_OF_REBARS_BOTTOM = [LAMBDA_AREA_REBARS(self.rebar_size_bottom) for x in range(generator_rebars)]
for x in seq(0.1, self.height+0.1, 0.1):
strain_in_rebars_top = [self.strain(self.concrete_strain , distance_of_rebar = self.effective_distance_y_top[i], x_iteration = x) for i in range(len(self.effective_distance_y_top)) ]
strain_in_rebars_bottom = [self.strain(self.concrete_strain , distance_of_rebar = self.effective_distance_y[i], x_iteration = x) for i in range(len(self.effective_distance_y[:generator_rebars])) ]
stress_in_rebars_top = [self.stress(self.steel_E_module , strain_in_rebars_top[i], self.steel_strength) for i in range(len(strain_in_rebars_top))]
stress_in_rebars_bottom = [ self.stress(self.steel_E_module , strain_in_rebars_bottom[i], self.steel_strength) for i in range(len(strain_in_rebars_bottom)) ]
normal_force = ( - 0.8 * x * self.width * self.compressive_concrete_strength + self.addingforces(self.AREA_OF_REBARS_BOTTOM, stress_in_rebars_bottom)
+ self.addingforces(self.AREA_OF_REBARS_TOP, stress_in_rebars_top)) * 10**-3
if 0.50 >= round(normal_force, 2) >= -1.0:
moment = ( self.k1* x * self.width * self.compressive_concrete_strength * (self.height/2 - (self.k1* x)/2) + self.addingmomentsbottom(self.AREA_OF_REBARS_BOTTOM, stress_in_rebars_bottom,
self.effective_distance_y, self.height) - self.addingmomentstop(self.AREA_OF_REBARS_TOP, stress_in_rebars_top, self.effective_distance_y_top, self.height)) * 10**-6
if moment > self.external_moment and strain_in_rebars_bottom[0] > self.min_steel_strain and strain_in_rebars_bottom[0] < self.max_steel_strain :
return moment, x, len(self.AREA_OF_REBARS_BOTTOM), self.AREA_OF_REBARS_BOTTOM, self.effective_distance_y_bottom
@staticmethod
def strain(concrete_strain, distance_of_rebar, x_iteration):
concrete_strain = concrete_strain
distance_of_rebar = distance_of_rebar
x_iteration = x_iteration
try:
strain = - concrete_strain / x_iteration * ( x_iteration - distance_of_rebar)
except (ZeroDivisionError, RuntimeWarning):
strain = 0
return strain
@staticmethod
def stress(STEEL_E_MODULE, strain_in_rebars, STEEL_STRENGTH):
STRESS = STEEL_E_MODULE * strain_in_rebars
stress = - STEEL_STRENGTH if STRESS <= - STEEL_STRENGTH else STRESS if STRESS >= - STEEL_STRENGTH and STRESS < 0 else min(STRESS, STEEL_STRENGTH)
return stress
@staticmethod
def addingforces(AREA_OF_REBARS, stress_in_rebars):
force = 0
for index in range(len(AREA_OF_REBARS)):
force += AREA_OF_REBARS[index] * stress_in_rebars[index]
return force
@staticmethod
def addingmomentsbottom(AREA_OF_REBARS, stress_in_rebars, distance_to_rebars, height):
add_moment = 0
for index in range(len(AREA_OF_REBARS)):
add_moment += AREA_OF_REBARS[index] * stress_in_rebars[index] * (distance_to_rebars[index] - 0.5 * height )
return add_moment
@staticmethod
def addingmomentstop(AREA_OF_REBARS, stress_in_rebars, distance_to_rebars, height):
add_moment = 0
for index in range(len(AREA_OF_REBARS)):
add_moment += AREA_OF_REBARS[index] * stress_in_rebars[index] * ( 0.5 * height - distance_to_rebars[index] )
return add_moment
A class with implementation of numpy array:
class ULSMomentCapacitynumpy(object):
def __init__(self, WIDTH, HEIGHT, SIZE_OF_REBAR_TOP, SIZE_OF_REBAR_BOTTOM, SIZE_OF_STIRRUP, STEEL_E_MODULE, STEEL_STRENGTH, COMPRESSIVE_STRENGTH_CONCRETE, CONCRETE_STRAIN,
MAX_STEEL_STRAIN, MIN_STEEL_STRAIN, EXTERNAL_MOMENT):
self.width, self.height = WIDTH, HEIGHT
self.rebar_size_top, self.rebar_size_bottom, self.rebar_size_stirrup = SIZE_OF_REBAR_TOP, SIZE_OF_REBAR_BOTTOM, SIZE_OF_STIRRUP
self.max_steel_strain, self.min_steel_strain, self.concrete_strain = 0.08, 0.002, 0.0035
self.steel_E_module, self.steel_strength, self.compressive_concrete_strength = STEEL_E_MODULE, STEEL_STRENGTH, COMPRESSIVE_STRENGTH_CONCRETE
self.external_moment = EXTERNAL_MOMENT
self.number_of_rebars_xy = (7, 4)
self.minimum_numbers_top_rebars = 2
self.k1 = 0.8
def getdata(self):
self.effective_distance_y_numpy = np.subtract( self.height, np.repeat([63.0, 103.5, 144.0, 184.5, 225.0], self.number_of_rebars_xy[0]))
self.effective_distance_y_top_numpy = np.array([63, 63])
self.effective_distance_y_bottom_numpy = np.repeat([63.0, 103.5, 144.0, 184.5, 225.0], self.number_of_rebars_xy[0])
LAMBDA_AREA_REBARS = lambda SIZE_OF_REBAR:int(math.pi/4*(SIZE_OF_REBAR)**2)
self.AREA_OF_REBARS_TOP_NUMPY = np.repeat(np.around(np.multiply( np.divide(np.pi, 4), np.power(self.rebar_size_top, 2)), 3), self.minimum_numbers_top_rebars)
MAX_NUMBER_REBAR =self.number_of_rebars_xy[0]*self.number_of_rebars_xy[1]
for generator_rebars in np.arange(2, MAX_NUMBER_REBAR+1):
self.AREA_OF_REBARS_BOTTOM_NUMPY = np.repeat(np.around(np.multiply( np.divide(np.pi, 4), np.power(self.rebar_size_bottom, 2)), 3), generator_rebars)
for x in np.arange(0.1, self.height+0.1, 0.1):
strain_in_rebars_top_numpy = np.apply_along_axis(lambda i: self.strain(self.concrete_strain , distance_of_rebar = self.effective_distance_y_top_numpy[i], x_iteration = x),0,np.arange( self.effective_distance_y_top_numpy.size))
strain_in_rebars_bottom_numpy = np.apply_along_axis(lambda i: self.strain(self.concrete_strain , distance_of_rebar = self.effective_distance_y_numpy[i], x_iteration = x),0,np.arange( self.effective_distance_y_numpy[:generator_rebars].size))
stress_in_rebars_top_numpy = np.apply_along_axis(lambda i: self.stress(self.steel_E_module , strain_in_rebars_top_numpy[i], self.steel_strength),0,np.arange( strain_in_rebars_top_numpy.size))
stress_in_rebars_bottom_numpy = np.apply_along_axis(lambda i: self.stress(self.steel_E_module , strain_in_rebars_bottom_numpy[i], self.steel_strength),0,np.arange( strain_in_rebars_bottom_numpy.size))
normal_force_numpy = np.divide(np.add(np.multiply(np.multiply(-0.8, x), np.multiply(self.width, self.compressive_concrete_strength)),
np.add(self.addingforces(self.AREA_OF_REBARS_BOTTOM_NUMPY, stress_in_rebars_bottom_numpy),
self.addingforces(self.AREA_OF_REBARS_TOP_NUMPY, stress_in_rebars_top_numpy))),1000)
if 0.50 >= round(normal_force_numpy, 2) >= -1.0:
moment_numpy = np.divide(
np.add(
np.multiply(np.multiply(np.multiply(self.k1, x), np.multiply(self.width, self.compressive_concrete_strength)), np.subtract(np.divide(self.height,2), np.divide(np.multiply(self.k1, x),2)))
,
np.subtract( self.addingmomentsbottomnumpy(self.AREA_OF_REBARS_BOTTOM_NUMPY, stress_in_rebars_bottom_numpy, self.effective_distance_y_numpy[:self.AREA_OF_REBARS_BOTTOM_NUMPY.size], self.height),
self.addingmomentstopnumpy(self.AREA_OF_REBARS_TOP_NUMPY, stress_in_rebars_top_numpy, self.effective_distance_y_top_numpy[:self.AREA_OF_REBARS_TOP_NUMPY.size], self.height)
)
)
,
1000000 )
if moment_numpy > self.external_moment and strain_in_rebars_bottom_numpy[0] > self.min_steel_strain and strain_in_rebars_bottom_numpy[0] < self.max_steel_strain :
return moment_numpy, x, self.AREA_OF_REBARS_BOTTOM_NUMPY.size, self.AREA_OF_REBARS_BOTTOM_NUMPY, self.effective_distance_y_bottom_numpy
@staticmethod
def strain(concrete_strain, distance_of_rebar, x_iteration):
concrete_strain = concrete_strain
distance_of_rebar = distance_of_rebar
x_iteration = x_iteration
try:
strain = np.multiply(
np.subtract( x_iteration, distance_of_rebar ) ,
np.divide(- concrete_strain, x_iteration ) )
except (FloatingPointError ,ZeroDivisionError, RuntimeWarning):
strain = 0
return strain
@staticmethod
def stress( STEEL_E_MODULE, strain_in_rebars_array, STEEL_STRENGTH):
STRESS = STEEL_E_MODULE * strain_in_rebars_array
stress_array = np.where(
STRESS <= -STEEL_STRENGTH, -STEEL_STRENGTH,
np.where(
np.logical_and(STRESS >= -STEEL_STRENGTH, STRESS < 0), STRESS,
np.minimum(STRESS, STEEL_STRENGTH)
)
)
return stress_array
@staticmethod
def addingforces(AREA_OF_REBARS, stress_in_rebars):
return np.sum(np.multiply(AREA_OF_REBARS, stress_in_rebars))
@staticmethod
def addingmomentsbottomnumpy(AREA_OF_REBARS, stress_in_rebars, distance_to_rebars, height):
return np.sum(np.multiply(np.multiply( np.subtract(distance_to_rebars, np.multiply(0.5, height)),stress_in_rebars), AREA_OF_REBARS ))
@staticmethod
def addingmomentstopnumpy(AREA_OF_REBARS, stress_in_rebars, distance_to_rebars, height):
return np.sum(np.multiply(np.multiply( np.subtract(np.multiply(0.5, height), distance_to_rebars),stress_in_rebars), AREA_OF_REBARS ))
The following codes to test speed of both classes, every arguments are fixed, just one value varies:
print("________________________Starting... ____________________________", "\n")
def withpython():
EXTERNAL_MOMENT=[100, 200, 300, 400, 500, 200, 400]
c = [ULSMomentCapacity(WIDTH=400, HEIGHT=500, SIZE_OF_REBAR_TOP=16, SIZE_OF_REBAR_BOTTOM=16, SIZE_OF_STIRRUP=10, STEEL_E_MODULE=200000,
STEEL_STRENGTH=425, COMPRESSIVE_STRENGTH_CONCRETE=20, CONCRETE_STRAIN=0.0035, MAX_STEEL_STRAIN=0.08, MIN_STEEL_STRAIN=0.002, EXTERNAL_MOMENT=n) for n in EXTERNAL_MOMENT]
for i in range(len(c)):
if c[i].getdata():
M,x, n, As, dy = c[i].getdata()
return M,x, n, As, dy
def withnumpy():
EXTERNAL_MOMENT=[100, 200, 300, 400, 500, 200, 400]
c = [ULSMomentCapacitynumpy(WIDTH=400, HEIGHT=500, SIZE_OF_REBAR_TOP=16, SIZE_OF_REBAR_BOTTOM=16, SIZE_OF_STIRRUP=10, STEEL_E_MODULE=200000,
STEEL_STRENGTH=425, COMPRESSIVE_STRENGTH_CONCRETE=20, CONCRETE_STRAIN=0.0035, MAX_STEEL_STRAIN=0.08, MIN_STEEL_STRAIN=0.002, EXTERNAL_MOMENT=n) for n in EXTERNAL_MOMENT]
for i in range(len(c)):
if c[i].getdata():
M,x, n, As, dy = c[i].getdata()
return M,x, n, As, dy
tstart = time()
withpython()
tend = time()
tstartn = time()
withnumpy()
tendn = time()
print("Time taken with plain python:",round(tend-tstart,3),"\n" ,"Time taken with numpy:", round(tendn-tstartn,3))
print("________________________Ending... ____________________________", "\n")
Time differences:
________________________Starting... ____________________________
Time taken with plain python: 7.696
Time taken with numpy: 188.523
________________________Ending... ____________________________
Upvotes: 1
Views: 210
Reputation: 2372
This is my take, it reduces execution time with numpy to 0.05 seconds. With no documentation I can't guarantee results will be correct, but the philosophy behind the changes definitively is.
As has been said, you need to vectorize your logic, most importantly substituting loops for matrix operations. Other considerations include:
arange
to create an array and use it to loop on another is a very very bad idea.This is the code, with comment on every change; I have also removed all _numpy suffixes for sanity.
import numpy as np
import math
class ULSMomentCapacitynumpy(object):
def __init__(
self,
WIDTH,
HEIGHT,
SIZE_OF_REBAR_TOP,
SIZE_OF_REBAR_BOTTOM,
SIZE_OF_STIRRUP,
STEEL_E_MODULE,
STEEL_STRENGTH,
COMPRESSIVE_STRENGTH_CONCRETE,
CONCRETE_STRAIN,
MAX_STEEL_STRAIN,
MIN_STEEL_STRAIN,
EXTERNAL_MOMENT,
):
self.width, self.height = WIDTH, HEIGHT
self.rebar_size_top, self.rebar_size_bottom, self.rebar_size_stirrup = (
SIZE_OF_REBAR_TOP,
SIZE_OF_REBAR_BOTTOM,
SIZE_OF_STIRRUP,
)
self.max_steel_strain, self.min_steel_strain, self.concrete_strain = (
0.08,
0.002,
0.0035,
)
self.steel_E_module, self.steel_strength, self.compressive_concrete_strength = (
STEEL_E_MODULE,
STEEL_STRENGTH,
COMPRESSIVE_STRENGTH_CONCRETE,
)
self.external_moment = EXTERNAL_MOMENT
self.number_of_rebars_xy = (7, 4)
self.minimum_numbers_top_rebars = 2
self.k1 = 0.8
def getdata(self):
self.effective_distance_y = np.subtract(
self.height,
np.repeat([63.0, 103.5, 144.0, 184.5, 225.0], self.number_of_rebars_xy[0]),
)
self.effective_distance_y_top = np.array([63, 63])
self.effective_distance_y_bottom = np.repeat(
[63.0, 103.5, 144.0, 184.5, 225.0], self.number_of_rebars_xy[0]
)
# this lambda is not being used,
LAMBDA_AREA_REBARS = lambda SIZE_OF_REBAR: int(
math.pi / 4 * (SIZE_OF_REBAR) ** 2
)
# Truncated rounding to 0 decimals, since original implementation is rounding to int
self.AREA_OF_REBARS_TOP = np.ones(self.minimum_numbers_top_rebars) * np.around(
math.pi / 4 * (self.rebar_size_top) ** 2, 0
)
MAX_NUMBER_REBAR = self.number_of_rebars_xy[0] * self.number_of_rebars_xy[1]
for generator_rebars in np.arange(2, MAX_NUMBER_REBAR + 1):
# Truncated rounding to 0 decimals, since original implementation is rounding to int
self.AREA_OF_REBARS_BOTTOM = np.ones(generator_rebars) * np.around(
math.pi / 4 * (self.rebar_size_bottom) ** 2, 0
)
# instead of looping the arange, we will work with it as an array
x = np.arange(0.1, self.height + 0.1, 0.1)
# this new axis makes x into a 2D array with size [5000,1], to allow for division by x
# aftwards we will squeeze it back to a vector of size [5000,]
x = x[:, np.newaxis]
# unnecesary looping, array creation and lambda call substitued by array arithmetic.
# No need to protect against division by zero since x never takes that value.
strain_in_rebars_top = (x - self.effective_distance_y_top) * (
-self.concrete_strain / x
)
# unnecesary looping, array creation and lambda call substitued by array arithmetic.
strain_in_rebars_bottom = (
x - self.effective_distance_y[:generator_rebars]
) * (-self.concrete_strain / x)
# unnecesary looping, array creation and lambda call substitued by array arithmetic.
stress_in_rebars_top = np.clip(
self.steel_E_module * strain_in_rebars_top,
-self.steel_strength,
self.steel_strength,
)
# unnecesary looping, array creation and lambda call substitued by array arithmetic.
stress_in_rebars_bottom = np.clip(
self.steel_E_module * strain_in_rebars_bottom,
-self.steel_strength,
self.steel_strength,
)
# x can convert back to a vector since now we are going to use dot
# np.dot multiplies 2 vectors item wise and the sums the result, what addingforces was doing.
x = np.squeeze(x)
normal_force = (
-0.8 * x * self.width * self.compressive_concrete_strength
+ np.dot(self.AREA_OF_REBARS_BOTTOM, stress_in_rebars_bottom.T,)
+ np.dot(self.AREA_OF_REBARS_TOP, stress_in_rebars_top.T)
) * 10 ** -3
# This is tricky; in the original loop you were returning after encountering
# the first element that passed the condition. Because we have vectorized x
# we calculate nomal_force for every value, and now we keep only the ones that pass
# the condition. It might seem like unnecesary work is done, but the result is still
# orders of magnitude faster than the scalar approach.
normal_force = np.around(normal_force, 2)
valid_force_indexes = np.argwhere(
(normal_force <= 0.50) & (normal_force >= -1.0)
)[:, 0]
# we reduce x and stress to only those where a valid normal_force was found:
x = x[valid_force_indexes]
stress_in_rebars_bottom = stress_in_rebars_bottom[valid_force_indexes, :]
stress_in_rebars_top = stress_in_rebars_top[valid_force_indexes]
moment = (
self.k1
* x
* self.width
* self.compressive_concrete_strength
* (self.height / 2 - (self.k1 * x) / 2)
+ np.sum(
self.AREA_OF_REBARS_BOTTOM
* stress_in_rebars_bottom
* (
self.effective_distance_y[: self.AREA_OF_REBARS_BOTTOM.shape[0]]
- 0.5 * self.height
)
)
- np.sum(
self.AREA_OF_REBARS_TOP
* stress_in_rebars_top
* (-0.5 * self.height - self.effective_distance_y_top)
)
) * 10 ** -6
# finally we loop the valid x points, and return the first that matches the condition
for moment, strain, x in zip(
moment, strain_in_rebars_bottom[valid_force_indexes, 0], x
):
if (
(moment > self.external_moment)
and (strain > self.min_steel_strain)
and (strain < self.max_steel_strain)
):
return (
moment,
x,
len(self.AREA_OF_REBARS_BOTTOM),
self.AREA_OF_REBARS_BOTTOM,
self.effective_distance_y_bottom,
)
Upvotes: 2