_En ocasiones nos encontraremos con algoritmos que no serán fácilmente vectorizables o expresables en operaciones sobre arrays de NumPy, y sufriremos los problemas de rendimiento de Python. En este notebook vamos a hacer un repaso exhaustivo de cómo acelerar sustancialmente nuestro código Python usando numba. Esta clase está basada en el artículo http://pybonacci.org/2015/03/13/como-acelerar-tu-codigo-python-con-numba/ _
numba es un compilador JIT (just-in-time) de Python que genera código máquina para CPU o GPU utilizando la infraestructura LLVM especializado en aplicaciones numéricas. Vamos a ver un ejemplo muy básico de cómo funciona:
import numpy as np
from numba import njit
arr2d = np.arange(20 * 30, dtype=float).reshape(20,30)
%%timeit
np.sum(arr2d)
The slowest run took 10.82 times longer than the fastest. This could mean that an intermediate result is being cached. 100000 loops, best of 3: 7.02 µs per loop
def py_sum(arr):
M, N = arr.shape
sum = 0.0
for i in range(M):
for j in range(N):
sum += arr[i,j]
return sum
%%timeit
py_sum(arr2d)
1000 loops, best of 3: 228 µs per loop
fast_sum = njit(py_sum)
%%timeit -n1 -r1
fast_sum(arr2d)
1 loop, best of 1: 328 ms per loop
%%timeit
fast_sum(arr2d)
The slowest run took 8.35 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 1.07 µs per loop
¿Impresionado? La primera vez que hemos llamado a la función, Python ha generado el código correspondiente al tipo de datos que le hemos pasado. Podemos verlo aquí:
fast_sum.signatures
[(array(float64, 2d, C),)]
E imprimir el código generado así:
fast_sum.inspect_types()
py_sum (array(float64, 2d, C),) -------------------------------------------------------------------------------- # File: <ipython-input-4-ea571924c8ab> # --- LINE 1 --- # label 0 # del $0.2 # del $0.5 # del $0.3 # del $0.4 # del $const0.6 def py_sum(arr): # --- LINE 2 --- # arr = arg(0, name=arr) :: array(float64, 2d, C) # $0.2 = getattr(value=arr, attr=shape) :: (int64 x 2) # $0.5 = exhaust_iter(value=$0.2, count=2) :: (int64 x 2) # $0.3 = static_getitem(value=$0.5, index=0, index_var=None) :: int64 # $0.4 = static_getitem(value=$0.5, index=1, index_var=None) :: int64 # M = $0.3 :: int64 # N = $0.4 :: int64 M, N = arr.shape # --- LINE 3 --- # $const0.6 = const(float, 0.0) :: float64 # sum = $const0.6 :: float64 # jump 14 # label 14 sum = 0.0 # --- LINE 4 --- # jump 16 # label 16 # $16.1 = global(range: <class 'range'>) :: Function(<class 'range'>) # $16.3 = call $16.1(M) :: (int64,) -> range_state_int64 # del M # del $16.1 # $16.4 = getiter(value=$16.3) :: range_iter_int64 # del $16.3 # $phi24.1 = $16.4 :: range_iter_int64 # del $16.4 # jump 24 # label 24 # $24.2 = iternext(value=$phi24.1) :: pair<int64, bool> # $24.3 = pair_first(value=$24.2) :: int64 # $24.4 = pair_second(value=$24.2) :: bool # del $24.2 # $phi26.1 = $24.3 :: int64 # del $24.3 # branch $24.4, 26, 64 # label 26 # del $24.4 # i = $phi26.1 :: int64 # del $phi26.1 # jump 28 # label 28 for i in range(M): # --- LINE 5 --- # jump 30 # label 30 # $30.1 = global(range: <class 'range'>) :: Function(<class 'range'>) # $30.3 = call $30.1(N) :: (int64,) -> range_state_int64 # del $30.1 # $30.4 = getiter(value=$30.3) :: range_iter_int64 # del $30.3 # $phi38.1 = $30.4 :: range_iter_int64 # del $30.4 # jump 38 # label 38 # $38.2 = iternext(value=$phi38.1) :: pair<int64, bool> # $38.3 = pair_first(value=$38.2) :: int64 # $38.4 = pair_second(value=$38.2) :: bool # del $38.2 # $phi40.1 = $38.3 :: int64 # del $38.3 # branch $38.4, 40, 60 # label 40 # del $38.4 # j = $phi40.1 :: int64 # del $phi40.1 # del j # del $40.6 # del $40.7 # del $40.8 for j in range(N): # --- LINE 6 --- # $40.6 = build_tuple(items=[Var(i, <ipython-input-4-ea571924c8ab> (4)), Var(j, <ipython-input-4-ea571924c8ab> (5))]) :: (int64 x 2) # $40.7 = getitem(value=arr, index=$40.6) :: float64 # $40.8 = inplace_binop(fn=+=, immutable_fn=+, lhs=sum, rhs=$40.7, static_lhs=<object object at 0x7f2bb7a9af40>, static_rhs=<object object at 0x7f2bb7a9af40>) :: float64 # sum = $40.8 :: float64 # jump 38 # label 60 # del i # del $phi40.1 # del $phi38.1 # del $38.4 # jump 62 # label 62 # jump 24 # label 64 # del arr # del N # del $phi26.1 # del $phi24.1 # del $24.4 # jump 66 # label 66 # del sum sum += arr[i,j] # --- LINE 7 --- # $66.2 = cast(value=sum) :: float64 # return $66.2 return sum ================================================================================
Como podemos leer en la documentación, numba tiene dos modos de funcionamiento básicos: el modo object y el modo nopython.
Por defecto numba usa el modo nopython siempre que puede, y si no pasa a modo object. Nosotros vamos a forzar el modo nopython (o «modo estricto» como me gusta llamarlo) porque es la única forma de aprovechar el potencial de numba.
El problema del modo nopython es que los mensajes de error son totalmente inservibles en la mayoría de los casos, así que antes de lanzarnos a compilar funciones con numba conviene hacer un repaso de qué no podemos hacer para anticipar la mejor forma de programar nuestro código. Podéis consultar en la documentación el subconjunto de Python soportado por numba en modo nopython, y ya os aviso que, al menos de momento, no tenemos list comprehensions, delegación de generadores ni algunas cosas más. Permitidme que resalte una frase sacada de la página principal de numba:
"With a few annotations, array-oriented and math-heavy Python code* can be just-in-time compiled to native machine instructions, similar in performance to C, C++ and Fortran*". [Énfasis mío]
Siento decepcionar a la audiencia pero numba no acelerará todo el código Python que le echemos: está enfocado a operaciones matemáticas con arrays. Aclarado este punto, vamos a ponernos manos a la obra con un ejemplo aplicado :)
Vamos a intentar acelerar la siguiente función, tomada del artículo http://pybonacci.org/2015/03/09/c-elemental-querido-cython/:
"Por ejemplo, imaginemos que tenemos que detectar valores mínimos locales dentro de una malla. Los valores mínimos deberán ser simplemente valores más bajos que los que haya en los 8 nodos de su entorno inmediato. En el siguiente gráfico, el nodo en verde será un nodo con un mínimo y en su entorno son todo valores superiores:
(2, 0) | (2, 1) | (2, 2) |
(1, 0) | (1. 1) | (1, 2) |
(0, 0) | (0, 1) | (0, 2) |
Creamos nuestro array de datos:
data = np.random.randn(2000, 2000)
Y copiemos directamente la función original:
def busca_min(malla):
minimosx = []
minimosy = []
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]):
minimosx.append(i)
minimosy.append(j)
return np.array(minimosx), np.array(minimosy)
busca_min(data)
(array([ 1, 1, 1, ..., 1998, 1998, 1998]), array([ 5, 9, 13, ..., 1978, 1980, 1996]))
Guía sobre cómo analizar el rendimiento en Python: https://www.huyng.com/posts/python-performance-analysis
%%timeit
busca_min(data)
1 loop, best of 3: 5.85 s per loop
stats = %prun -s cumtime -rq busca_min(data)
stats.print_stats()
887224 function calls in 6.214 seconds Ordered by: cumulative time ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 6.214 6.214 {built-in method builtins.exec} 1 0.007 0.007 6.214 6.214 <string>:1(<module>) 1 6.046 6.046 6.207 6.207 <ipython-input-12-08c7a5f9247e>:1(busca_min) 887218 0.086 0.000 0.086 0.000 {method 'append' of 'list' objects} 2 0.074 0.037 0.074 0.037 {built-in method numpy.core.multiarray.array} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
<pstats.Stats at 0x7f2b89f72b70>
Parece que está habiendo demasiadas llamadas a list.append
, aunque representan un porcentaje pequeño del tiempo de ejecución.
%load_ext line_profiler
stats = %lprun -f busca_min -r busca_min(data)
stats.print_stats()
Timer unit: 1e-06 s Total time: 15.7669 s File: <ipython-input-12-08c7a5f9247e> Function: busca_min at line 1 Line # Hits Time Per Hit % Time Line Contents ============================================================== 1 def busca_min(malla): 2 1 2 2.0 0.0 minimosx = [] 3 1 1 1.0 0.0 minimosy = [] 4 1999 997 0.5 0.0 for i in range(1, malla.shape[1]-1): 5 3994002 2136183 0.5 13.5 for j in range(1, malla.shape[0]-1): 6 3992004 5055297 1.3 32.1 if (malla[j, i] < malla[j-1, i-1] and 7 1996500 2249400 1.1 14.3 malla[j, i] < malla[j-1, i] and 8 1331963 1585630 1.2 10.1 malla[j, i] < malla[j-1, i+1] and 9 999863 1135817 1.1 7.2 malla[j, i] < malla[j, i-1] and 10 799538 898426 1.1 5.7 malla[j, i] < malla[j, i+1] and 11 666055 813437 1.2 5.2 malla[j, i] < malla[j+1, i-1] and 12 570797 647823 1.1 4.1 malla[j, i] < malla[j+1, i] and 13 499279 588639 1.2 3.7 malla[j, i] < malla[j+1, i+1]): 14 443609 310427 0.7 2.0 minimosx.append(i) 15 443609 271825 0.6 1.7 minimosy.append(j) 16 17 1 73032 73032.0 0.5 return np.array(minimosx), np.array(minimosy)
Hacer append
a esas dos listas tantas veces no parece una buena idea. De hecho, se puede comprobar que se hace para un porcentaje significativo de los elementos:
mx, my = busca_min(data)
mx.size / data.size
0.11090225
Tenemos que más de un 10 % de los elementos de la matriz cumplen la condición de ser «mínimos locales», así que no es nada despreciable. Esto en nuestro ejemplo hace un total de más de 400 000 elementos:
mx.size
443609
En lugar de esto, lo que vamos a hacer va a ser crear otro array, de la misma forma que nuestros datos, y almacenar un valor True
en aquellos elementos que cumplan la condición de mínimo local. De esta forma cumplimos también una de las reglas de oro de Software Carpentry: "Always initialize from data".
def busca_min_np(malla):
minimos = np.zeros_like(malla, dtype=bool)
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
return np.nonzero(minimos)
Encima puedo aprovechar la estupenda función nonzero
de NumPy. Compruebo que las salidas son iguales:
np.testing.assert_array_equal(busca_min(data)[0], busca_min_np(data)[0])
np.testing.assert_array_equal(busca_min(data)[1], busca_min_np(data)[1])
Y evalúo el rendimiento de la nueva función:
%timeit busca_min_np(data)
1 loop, best of 3: 5.93 s per loop
Como era de esperar, los tiempos son parecidos, porque no he optimizado el cuello de botella que son las comprobaciones de los arrays. Al menos, ya no tenemos dos objetos en memoria que van a crecer de manera aleatoria: ya podemos utilizar numba.
numba.jit(nopython=True)
¶Como hemos dicho antes, vamos a forzar que numba funcione en modo nopython para garantizar que obtenemos una mejora en el rendimiento. Si intentamos compilar la primera función, ya vamos a ver una ganancia de rendimiento sustancial:
busca_min_jit = njit(busca_min)
busca_min_jit(data)
(array([ 1, 1, 1, ..., 1998, 1998, 1998]), array([ 5, 9, 13, ..., 1978, 1980, 1996]))
%timeit busca_min_jit(data)
10 loops, best of 3: 70 ms per loop
¿Qué pasa si hacemos lo mismo con la versión que no utiliza listas?
busca_min_np_jit = njit(busca_min_np)
busca_min_np_jit(data)
--------------------------------------------------------------------------- TypingError Traceback (most recent call last) <ipython-input-26-dd874ea45398> in <module>() 1 busca_min_np_jit = njit(busca_min_np) ----> 2 busca_min_np_jit(data) /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/dispatcher.py in _compile_for_args(self, *args, **kws) 308 for i, err in failed_args)) 309 e.patch_message(msg) --> 310 raise e 311 312 def inspect_llvm(self, signature=None): /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/dispatcher.py in _compile_for_args(self, *args, **kws) 285 argtypes.append(self.typeof_pyval(a)) 286 try: --> 287 return self.compile(tuple(argtypes)) 288 except errors.TypingError as e: 289 # Intercept typing error that may be due to an argument /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/dispatcher.py in compile(self, sig) 553 554 self._cache_misses[sig] += 1 --> 555 cres = self._compiler.compile(args, return_type) 556 self.add_overload(cres) 557 self._cache.save_overload(sig, cres) /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/dispatcher.py in compile(self, args, return_type) 79 impl, 80 args=args, return_type=return_type, ---> 81 flags=flags, locals=self.locals) 82 # Check typing error if object mode is used 83 if cres.typing_error is not None and not flags.enable_pyobject: /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in compile_extra(typingctx, targetctx, func, args, return_type, flags, locals, library) 697 pipeline = Pipeline(typingctx, targetctx, library, 698 args, return_type, flags, locals) --> 699 return pipeline.compile_extra(func) 700 701 /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in compile_extra(self, func) 350 self.lifted = () 351 self.lifted_from = None --> 352 return self._compile_bytecode() 353 354 def compile_ir(self, func_ir, lifted=(), lifted_from=None): /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in _compile_bytecode(self) 658 """ 659 assert self.func_ir is None --> 660 return self._compile_core() 661 662 def _compile_ir(self): /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in _compile_core(self) 645 646 pm.finalize() --> 647 res = pm.run(self.status) 648 if res is not None: 649 # Early pipeline completion /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in run(self, status) 236 # No more fallback pipelines? 237 if is_final_pipeline: --> 238 raise patched_exception 239 # Go to next fallback pipeline 240 else: /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in run(self, status) 228 try: 229 event(stage_name) --> 230 stage() 231 except _EarlyPipelineCompletion as e: 232 return e.result /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in stage_nopython_frontend(self) 442 self.args, 443 self.return_type, --> 444 self.locals) 445 446 with self.fallback_context('Function "%s" has invalid return type' /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/compiler.py in type_inference_stage(typingctx, interp, args, return_type, locals) 798 799 infer.build_constraint() --> 800 infer.propagate() 801 typemap, restype, calltypes = infer.unify() 802 /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/typeinfer.py in propagate(self, raise_errors) 765 if errors: 766 if raise_errors: --> 767 raise errors[0] 768 else: 769 return errors /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/typeinfer.py in propagate(self, typeinfer) 126 lineno=loc.line): 127 try: --> 128 constraint(typeinfer) 129 except TypingError as e: 130 errors.append(e) /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/typeinfer.py in __call__(self, typeinfer) 377 fnty = typevars[self.func].getone() 378 with new_error_context("resolving callee type: {0}", fnty): --> 379 self.resolve(typeinfer, typevars, fnty) 380 381 def resolve(self, typeinfer, typevars, fnty): /home/juanlu/.miniconda36/envs/py36/lib/python3.6/site-packages/numba/typeinfer.py in resolve(self, typeinfer, typevars, fnty) 399 desc = context.explain_function_type(fnty) 400 msg = '\n'.join([head, desc]) --> 401 raise TypingError(msg, loc=self.loc) 402 403 typeinfer.add_type(self.target, sig.return_type, loc=self.loc) TypingError: Failed at nopython (nopython frontend) Invalid usage of Function(<function zeros_like at 0x7f2bac14f048>) with parameters (array(float64, 2d, C), dtype=Function(<class 'bool'>)) * parameterized File "<ipython-input-21-c316e0a5a386>", line 2 [1] During: resolving callee type: Function(<function zeros_like at 0x7f2bac14f048>) [2] During: typing of call at <ipython-input-21-c316e0a5a386> (2)
Obtenemos un error porque numba no reconoce la función np.zeros_like
con los argumentos que le hemos pasado. Si acudimos a la documentación http://numba.pydata.org/numba-doc/0.31.0/reference/numpysupported.html#other-functions, vemos que hay que utilizar tipos de NumPy, en este caso np.bool_
.
@njit
def busca_min_np2_jit(malla):
minimos = np.zeros_like(malla, np.bool_) # <-- Cambiar esta línea
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
return np.nonzero(minimos)
busca_min_np2_jit(data)
(array([ 1, 1, 1, ..., 1998, 1998, 1998]), array([ 5, 9, 13, ..., 1978, 1980, 1996]))
%timeit busca_min_np2_jit(data)
10 loops, best of 3: 65.4 ms per loop
Lo hemos conseguido: 70x más rápido :)
%matplotlib inline
import matplotlib.pyplot as plt
from numpy import sin, pi
El cálculo de propiedades termodinámicas de la atmósfera estándar es un problema clásico que todo aeronáutico ha afrontado alguna vez muy al principio de su carrera formativa. La teoría es simple: imponemos una ley de variación de la temperatura con la altura $T = T(h)$, la presión se obtiene por consideraciones hidrostáticas $p = p(T)$ y la densidad por la ecuación de los gases ideales $\rho = \rho(p, T)$. La particularidad de la atmósfera estándar es que imponemos que la variación de la temperatura con la altura es una función simplificada y definida a trozos, así que calcular temperatura, presión y densidad dada una altura se parece mucho a hacer esto:
$$T(h) = \begin{cases} T_0 + \alpha h & 0 <= h <= 11000 \\ T(11000) & 11000 < h <= 20000 \end{cases} \\ ~\\ T_0 = 288.16 K \\ \alpha = -6.5 \cdot 10^{-3}~\text{K/m}$$$$ \rho(h) = \begin{cases} \rho_0 \left( \frac{T}{T_0} \right)^{-\frac{g}{\alpha R} - 1} & 0 <= h <= 11000 \\ \rho(11000)~e^{\frac{-g(z - 11000)}{R T}} & 11000 < h <= 20000 \end{cases} $$$$\rho_0 = 1.225~\text{[SI]} \\ R = 287~\text{[SI]}$$$$p = \rho R_a T$$if 0.0 <= h < 11000.0:
T = T0 + alpha * h
rho = ... # Algo que depende de T
p = rho * R_a * T
elif 11000.0 <= h < 20000.0:
T = T1
rho = ...
p = rho * R_a * T
El problema viene cuando se quiere vectorizar esta función y permitir que h
pueda ser un array de alturas. Esto es muy conveniente cuando queremos pintar alguna propiedad con matplotlib, por ejemplo.
Se intuye que hay dos formas de hacer esto: utilizando funciones de NumPy o iterando por cada elemento del array.
# Constants
R_a = 287.05287 # J/(Kg·K)
g0 = 9.80665 # m/s^2
T0 = 288.15 # K
p0 = 101325.0 # Pa
alpha = np.array([-6.5e-3, 0.0]) # K / m
# Computed constants
T1 = T0 + alpha[0] * 11000.0
p1 = p0 * (T0 / (T0 + alpha[0] * 11000.0)) ** (g0 / (R_a * alpha[0]))
def atm(h):
"""Standard atmosphere temperature, pressure and density.
Parameters
----------
h : array-like
Geopotential altitude, m.
"""
h = np.atleast_1d(h).astype(float)
scalar = (h.size == 1)
assert len(h.shape) == 1
T = np.empty_like(h)
p = np.empty_like(h)
rho = np.empty_like(h)
# Actually compute the values
_atm(h, T, p, rho)
if scalar:
T = T[0]
p = p[0]
rho = rho[0]
return T, p, rho
@njit
def _atm(h, T, p, rho):
for ii in range(h.size):
if 0.0 <= h[ii] < 11000.0:
T[ii] = T0 + alpha[0] * h[ii]
p[ii] = p0 * (T0 / (T0 + alpha[0] * h[ii])) ** (g0 / (R_a * alpha[0]))
rho[ii] = p[ii] / (R_a * T[ii])
elif 11000.0 <= h[ii] <= 20000.0:
T[ii] = T1 # + alpha[1] * (h[ii] - 11000.0)
p[ii] = p1 * np.exp(-g0 * (h[ii] - 11000.0) / (R_a * T1))
rho[ii] = p[ii] / (R_a * T[ii])
# aeropython: preserve
h = np.linspace(0, 20000)
T, p, _ = atm(h)
fig, ax1 = plt.subplots()
l1, = ax1.plot(T - 273, h, color="C0")
ax1.set_xlabel("T (°C)")
ax2 = ax1.twiny()
l2, = ax2.plot(p, h, color="C1")
ax2.set_xlabel("p (Pa)")
ax1.legend((l1, l2), ["Temperature", "Pressure"], loc=0)
ax1.grid()
Implementar y representar gráficamente la solución de Navier para calcular la deflexión de una placa rectangular, simplemente apoyada en sus cuatro bordes (es decir, los bordes pueden girar: no están empotrados) sometida a una carga transversal. La expresión matemática es:
$$w(x,y) = \sum_{m=1}^\infty \sum_{n=1}^\infty \frac{a_{mn}}{\pi^4 D}\,\left(\frac{m^2}{a^2}+\frac{n^2}{b^2}\right)^{-2}\,\sin\frac{m \pi x}{a}\sin\frac{n \pi y}{b}$$siendo $a_{mn}$ los coeficientes de Fourier de la carga aplicada.
Para cada punto $(x, y)$ hay que hacer una doble suma en serie; si además queremos evaluar esto en un meshgrid
, necesitamos un cuádruple bucle.
@njit
def a_mn_point(P, a, b, xi, eta, mm, nn):
"""Navier series coefficient for concentrated load.
"""
return 4 * P * sin(mm * pi * xi / a) * sin(nn * pi * eta / b) / (a * b)
@njit
def plate_displacement(xx, yy, ww, a, b, P, xi, eta, D, max_m, max_n):
max_i, max_j = ww.shape
for mm in range(1, max_m):
for nn in range(1, max_n):
for ii in range(max_i):
for jj in range(max_j):
a_mn = a_mn_point(P, a, b, xi, eta, mm, nn)
ww[ii, jj] += (a_mn / (mm**2 / a**2 + nn**2 / b**2)**2
* sin(mm * pi * xx[ii, jj] / a)
* sin(nn * pi * yy[ii, jj] / b)
/ (pi**4 * D))
# aeropython: preserve
# Plate geometry
a = 1.0 # m
b = 1.0 # m
h = 50e-3 # m
# Material properties
E = 69e9 # Pa
nu = 0.35
# Series terms
max_m = 16
max_n = 16
# Computation points
# NOTE: With an odd number of points the center of the place is included in
# the grid
NUM_POINTS = 101
# Load
P = 10e3 # N
xi = 3 * a / 4
eta = a / 2
# Flexural rigidity
D = h**3 * E / (12 * (1 - nu**2))
# ---
# Set up domain
x = np.linspace(0, a, num=NUM_POINTS)
y = np.linspace(0, b, num=NUM_POINTS)
xx, yy = np.meshgrid(x, y)
# Compute displacement field
ww = np.zeros_like(xx)
plate_displacement(xx, yy, ww, a, b, P, xi, eta, D, max_m, max_n)
# Print maximum displacement
w_max = abs(ww).max()
print("Maximum displacement = %14.12f mm" % (w_max * 1e3))
print("alpha = %7.5f" % (w_max / (P * a**2 / D)))
print("alpha * P a^2 / D = %6.4f mm" % (0.01160 * P * a**2 / D * 1e3))
plt.contourf(xx, yy, ww)
plt.colorbar()
Maximum displacement = 0.101508051672 mm alpha = 0.00831 alpha * P a^2 / D = 0.1416 mm
<matplotlib.colorbar.Colorbar at 0x7f2b81a604e0>
Las siguientes celdas contienen configuración del Notebook
Para visualizar y utlizar los enlaces a Twitter el notebook debe ejecutarse como seguro
File > Trusted Notebook
# Esta celda da el estilo al notebook
from IPython.core.display import HTML
css_file = '../styles/aeropython.css'
HTML(open(css_file, "r").read())