4 Utilizando o método de Rayleigh-Ritz na linguagem Python

Em se tratando de programação, existem diversas formas de se trabalhar o método de Rayleigh-Ritz em diversas linguagens. Para este trabalho foi escolhida a linguagem Python por ser uma liguagem gratuita, de código aberto, e com forte apelo a computação científica. Ainda assim, existem diversas formas de se implementar algoritmos para o uso deste método. O objetivo deste capítulo é de trabalhar e exemplificar como o método pode ser implementado utilizando a linguagem de programação Python.

Voltando ao funcional \(\Pi\), definido em no do , tem-se

\[ \Pi = \frac{1}{2} \int_0^l EI \left [ \frac{d^2v}{dx^2} \right ]^2 dx - \int_0^l qvdx \]
e tomando, ainda, a função aproximadora \(v_3(x)=a_0+a_1 x + a_2 x^2 + a_3 x^3 + a_4 x^4\), donde aplicando as condições de contorno essenciais, \(v_3(0)=0\), obtendo \(a_0=0\), e \(v_3(l)=0\), encontrando \(a_1=-a_2l-a_3l^2-a_4l^3\). Substituindo \(a_0\) e \(a_1\) em \(v_3(x)\), tem-se a função
\[ v_3(x)= a_2 (x^2 - lx) + a_3 (x^3 - l^2x) + a_4 (x^4 - l^3x) \text{.} \]

Derivando \(v_3\) em relação a \(x\) duas vezes,

\[ v_3''(x)= 2a_2 + 6a_3 x + 12a_4 x^2 \text{.} \]

Para a determinação dos parâmetros \(a_2\), \(a_3\) e \(a_4\), deve-se substituir \(v_3(x)\) no funcional \(\Pi\), e resolver o sistema

\[ \begin{cases} \dfrac{\partial \Pi}{\partial a_2}=0\\[10pt] \dfrac{\partial \Pi}{\partial a_3}=0\\[10pt] \dfrac{\partial \Pi}{\partial a_4}=0 \end{cases} \text{.} \]

Tanto a derivação quanto a integração podem ser feitas manualmente ou utilizando o pacote SymPy em Python, como feito no Código 4.1.

Código 4.1 - Código para a resolução do .
from sympy import symbols, integrate, diff, Matrix
from sympy.solvers import solve

l, E, I, x, q = symbols('l E I x q')
a2, a3, a4 = symbols("a2 a3 a4")

v = a2 * (x**2 - l*x) 
v = v + a3 * (x**3 - l**2 * x)
v = v + a4 * (x**4 - l**3 * x)

dv = diff(v, x)
dv2 = diff(dv, x)

f = (E*I*((dv2)**2)) / 2 - q * v

df_a2 = diff(f, a2)
df_a3 = diff(f, a3)
df_a4 = diff(f, a4)

eq1 = integrate(df_a2, (x, 0, l))
eq2 = integrate(df_a3, (x, 0, l))
eq3 = integrate(df_a4, (x, 0, l))

sol = solve([eq1, eq2, eq3], [a2, a3, a4])
print(sol)
Fonte: Elaborado pelo autor, 2019.

O Código 4.1 é executado rapidamente e o resultado é mostrado na Figura 4.1, com \(a_2=0\), \(a_3=-\dfrac{ql}{12EI}\) e \(a_4=\dfrac{q}{24EI}\). Esses resultados são exatamente os mesmos apresentados no .

Figura 4.1 - Resultado apresentado pelo Código 4.1.
Fonte: Elaborada pelo autor, 2019.

O valor de \(a_1=-a_2l-a_3l^2-a_4l^3\) é determinado substituindo os valores de \(a_2\), \(a_3\) e \(a_4\). Esse processo pode ser realizado de modo manual ou, mais rapidamente, utilizando a própria linguagem, adicionando o Código 4.2 ao final do Código 4.1. Os comandos a1 = a1.subs(a2, sol[a2]) indica que a variável a1 receberá o valor de a1 substituindo o simbólo a2 pela solução do sistema encontrada no Código 4.1.

Código 4.2 - Código para a determinação do coeficiente \(a_1\) do .
a1 = -a2*l - a3*l**2 - a4 * l**3
a1 = a1.subs(a2, sol[a2])
a1 = a1.subs(a3, sol[a3])
a1 = a1.subs(a4, sol[a4])
print(a1)
Fonte: Elaborado pelo autor, 2019.

É possível, ainda, construir uma função \(v(x)\) utilizando os coeficientes \(a_1\), \(a_2\), \(a_3\) e \(a_4\) (O coeficiente \(a_0\) foi ignorado devido ao seu valor ser nulo) com o Código 4.3. Utilizando, novamente, o comando subs e a função definida no Código 4.3, pode-se calcular o valor de \(v_3(x)\) em algum ponto, como, por exemplo, em \(x=\dfrac{l}{2}\). O valor \(v_3(\frac{l}{2})\) é o valor de máxima deflexão.

Código 4.3 - Código para a definição da função \(v_3(x)\) do .
v = a1*x + sol[a2]*x**2 + sol[a3]*x**3 + sol[a4]*x**4
print(v)
maxdef = v.subs(x, l/2)
print(maxdef)
Fonte: Elaborado pelo autor, 2019.

É importante observar, no Código 4.3, que ao construir a variável v para representar a função, no primeiro coeficiente foi utilizado a variável a1, enquanto que nas demais, foi utilizado sol[a2], sol[a3] e sol[a4]. Isso se deve ao fato de que os valores para os coeficientes \(a_2\), \(a_3\) e \(a_4\) foram encontrados pela resolução do sistema, sendo a variável sol a sua solução. Já, a1 foi definida como sendo a equação do termo \(a_1\) isolado em função de \(a_2\), \(a_3\) e \(a_4\).

Os Códigos: Código 4.1, Código 4.2 e Código 4.3 são executados em sequência e, por essa razão, estão com numeração de linhas sequenciais: O Código 4.1 tendo as linhas de 1 a 25, o Código 4.2 de 26 a 30 e o Código 4.3 de 31 a 34. A Figura 4.2 mostra o resultado da execução dos Códigos Código 4.1, Código 4.2 e Código 4.3 no terminal.

Figura 4.2 - Resultado apresentado pelos Códigos: Código 4.1, Código 4.2 e Código 4.3.
Fonte: Elaborada pelo autor, 2019.

Observando, no terminal da Figura 4.2, pela ordem dos comandos print executados no código, a primeira linha apresenta a solução do sistema, a segunda linha apresenta o valor do coeficiente \(a_1\), a terceira linha apresenta a função \(v_3(x)\) e a quarta linha apresenta o resultado de \(v_3(\frac{l}{2})\) que é o valor de máxima deflexão da viga.

Ressaltando, novamente, que a escolha das funções de forma utilizadas na função aproximadora são de suma importância para o método de Rayleigh-Ritz. Para o uso de matemática simbólica, como nesse exemplo, deve-se tomar ainda mais cuidado com a escolha de tais funções pois, em alguns casos, a integração simbólica toma grande custo computacional. Em casos onde a integração simbólica se torna árdua, ou até mesmo impossível, o uso de algoritmos para integração numérica é necessário.

4.1 Resolvendo problemas com o método de Rayleigh-Ritz utilizando funções triangulares em Python

O sistema \(M\cdot A=B\), encontrado na do , pode ser resolvido, numericamente, utilizando a linguagem de programação Python. A título de comparação serão utilizadas as funções \(p(x)=e^x\), \(q(x)=e^x\) e \(f(x)=x+(2-x)e^x\) que possui função exata \(y(x)\) conhecida dada por \(y(x)=(x-1)(e^{-x}-1)\).

Viabilizando a implementação do algoritmo, faz-se necessário relembrar \(m_{j,i}\) e \(b_j\), obtidos em e , respectivamente:

\[ m_{j,i}=\int_{0}^{1} \left [ 2p(x)\phi_i'(x)\phi_j'(x) + 2q(x)\phi_i(x)\phi_j(x) \right ]dx \text{,} \]
\[ b_j=\int_{0}^{1} 2f(x)\phi_j(x)dx \text{.} \]

É útil, também, a definição da função de forma \(\phi_i(x)\), feita em , e sua derivada, \(\phi_i'(x)\), obtida em :

\[ \phi_i (x) = \begin{cases} 0 & \text{se } 0 \leqslant x \leqslant x_{i - 1}\\ \dfrac{x-x_{i-1}}{h_{i-1}} & \text{se } x_{i-1} < x \leqslant x_i\\ \dfrac{x_{i+1}-x}{h_i} & \text{se } x_i < x \leqslant x_{i+1}\\ 0 & \text{se } x_{i+1}<x\leqslant 1 \end{cases} \text{,} \]
\[ \phi_i ' (x) = \begin{cases} 0 & \text{se } 0 \leqslant x < x_{i - 1}\\ \dfrac{1}{h_{i-1}} & \text{se } x_{i-1} < x < x_i\\ \dfrac{-1}{h_i} & \text{se } x_i < x < x_{i+1}\\ 0 & \text{se } x_{i+1}<x\leqslant 1 \end{cases} \text{.} \]

Para a resolução do problema, o primeiro passo é importar os componentes a serem utilizados. No Código 4.4 são importados os componentes numpy, utilizado para a criação de alguns intervalos numéricos espaçados. Do pacote math é importando a constante \(e\). Do pacote sympy é importado o método symbols para a definição de símbolos matemáticos, de sympy.matrices é importado o método zeros para a criação de um matriz de tamanho determinado preenchida por números zeros e de sympy.solvers é importado o método linsolve utilizado para a resolução de sistemas lineares. Por último, é importado o pacote matplotlib.pyplot como pyplot1 utilizado para plotar gráficos.

1 - Utilizar o comando import matplotlib.pyplot as pyplot faz com que, em vez de sempre escrever matplotlib.pyplot, se possa escrever apenas pyplot nos próximos comandos.

Código 4.4 - Importação dos pacotes utilizados para a resolução.
import numpy
from math import e
from sympy import symbols
from sympy.matrices import zeros
from sympy.solvers import linsolve
import matplotlib.pyplot as pyplot
import matplotlib.patches as patches
Fonte: Elaborado pelo autor, 2019.

Para o uso das funções de forma \(\phi_i(x)\) e \(\phi_i'(x)\), definidas em e , respectivamente, é preciso criar um conjunto numérico espaçado (aqui o conjunto será igualmente espaçado, mas nada impede o uso de conjuntos com espaçamentos diferentes). Para a criação desse conjunto foi utilizada a função numpy.arange2, em Código 4.5. No comando arange. O uso do extremo \(1.0001\) é apenas para garantir que o número \(1\) esteja no conjunto gerado. A variável n representa a quantidade de divisões a serem utilizadas no intervalo para a discretização da função exata.

2 - A função numpy.arange cria valores espaçados uniformemente dentro de um determinado intervalo.

Código 4.5 - Criação dos intervalos numéricos.
n = 5
space = 1/(n + 1)
intervals = numpy.arange(0, 1.0001, space)
Fonte: Elaborado pelo autor, 2019.

Código 4.6 - Função de forma \(\phi\) e sua derivada.
def fi(i, x):
	if x <= intervals[i - 1]:
		return 0
	elif x <= intervals[i]:
		h = (intervals[i] - intervals[i - 1])
		return ((x - intervals[i - 1]) / h)
	elif x <= intervals[i + 1]:
		h = (intervals[i + 1] - intervals[i])
		return ((intervals[i + 1] - x) / h)
	else:
		return 0

def fi_diff(i, x):
	if x <= intervals[i - 1]:
		return 0
	elif x <= intervals[i]:
		return (1 / (intervals[i] - intervals[i - 1]))
	elif x <= intervals[i + 1]:
		return (1 / (intervals[i] - intervals[i + 1]))
	else:
		return 0
Fonte: Elaborado pelo autor, 2019.

Numericamente, as funções \(\phi_i(x)\) e \(\phi_i'(x)\) foram programadas utilizando condicionais, em Código 4.6.

No Código 4.6 o método utilizado para a integração numérica, que implementa a chamada regra do trapézio, é mostrado. Dentro do método, a variável steps define em quantos intervalos o domínio de integração será dividido. Os parâmetros opcionais *args, do método integrate, são passados para a função a ser integrada, func, e serão utilizados para passar os índices i e j quando necessário.

Código 4.7 - Algoritmo para integração numérica usando a regra dos trapézios.
def integrate(func, start, end, *args):
	steps = 10000
	h = ((end - start) / steps)
	half = h / 2
	value = 0
	for i in range(0, steps):
		dh = func(start + h * i + half, *args)
		if i == 0 or i == (steps - 1):
			value += dh
		else:
			value += (2 * dh)
	return (value * (h / 2))
Fonte: Elaborado pelo autor, 2019.

Para a integração numérica por meio do algoritmo implementado em Código 4.7 é preciso definir funções com os integrandos de e . Essas funções são chamadas, no código, de m_intern e b_intern, respectivamente. Na implementação do Código 4.8, os métodos m(i, j) e b(i) calculam os elementos das matrizes \(M\) e \(B\), respectivamente. Nesses métodos, é somado 1 aos índices \(i\) e \(j\) pois, na programação os elementos são indexados a partir de 0 e, na forma como definimos as funções de forma \(\phi_i(x)\), elas são indexadas a partir de 1.

Código 4.8 - Funções utilizadas para criação das matrizes do sistema linear.
def p(x):
	return e**x

def q(x):
	return e**x

def f(x):
	return (x+(2-x)*e**x)

def m_intern(x, i, j):
	value = p(x) * fi_diff(i, x) * fi_diff(j, x)
	value = value + q(x) * fi(i, x) * fi(j, x)
	return value

def m(i, j):
	i = i + 1
	j = j + 1
	return integrate(m_intern, 0, 1, i, j)

def b_intern(x, i):
	return (f(x) * fi(i, x))

def b(i):
	i = i + 1
	return integrate(b_intern, 0, 1, i)
Fonte: Elaborado pelo autor, 2019.

O próximo passo, consiste em criar a matriz aumentada do sistema, os símbolos que representam as incógnitas e, então, resolvê-lo. Isso é feito no Código 4.9.

Código 4.9 - Definição e resolução do sistema linear.
M = zeros(n, n+1)
for i in range(0, n):
	M[i, n] = b(i)
	M[i, i] = m(i, i)
	if (i - 1) >= 0:
		M[i, i - 1] = m(i, i - 1)
	if (i + 1) < n:
		M[i, i + 1] = m(i, i + 1)
coeficients = symbols('a_0:{}'.format(n))
solution, = linsolve(M, coeficients)
Fonte: Elaborado pelo autor, 2019.

Utilizando a solução do Código 4.9 e a função exata \(y(x)=(x-1)(e^{-x}-1)\), é possível implementar métodos que representam a função exata e a função aproximada, plotando-as no mesmo gráfico, como feito no Código Código 4.10.

Código 4.10 - Plotagem das funções exata e aproximada.
def function(solution, x):
	value = 0
	i = 0
	for c in solution:
		value += c * fi(i + 1, x)
		i = i + 1
	return value
def function_original(x):
	return ((x - 1) * (e**(-x) - 1))

plot_interval = numpy.arange(0.0, 1.0, 0.01)
plot_original = function_original(plot_interval)
plot_aprox = []
for i in plot_interval:
	plot_aprox.append(function(solution, i))
fig = pyplot.figure()
axes = fig.add_subplot(1, 1, 1)
color1='tab:blue'
color2='tab:orange'
axes.plot(plot_interval, plot_aprox, color=color1)
axes.plot(plot_interval, plot_original, color=color2)
axes.set_title('Rayleigh-Ritz With n=' + str(n))
blue_legend = patches.Patch(color=color1, label='Aproximada')
orange_legend = patches.Patch(color=color2, label='Exata')
pyplot.legend(handles=[blue_legend, orange_legend])
pyplot.show()
Fonte: Elaborado pelo autor, 2019.

Os Códigos: Código 4.4, Código 4.5, Código 4.6, Código 4.7, Código 4.8, Código 4.9 e Código 4.10 são sequênciais e devem ser executados dentro de um único arquivo, por isso suas numerações estão sequênciais. O resultado da execução do código para a resolução do sistema é mostrado na Figura 4.3, onde a curva de cor laranja é a solução exata e a curva de cor azul é a discretização. Para maior precisão do método, pode-se aumentar o valor da variável n do Código 4.5, por exemplo, para 10. O gráfico plotado para \(n=10\) é apresentado na Figura 4.4.

Quanto maior o valor da variável n no Código 4.5 maior a precisão, porém, com maior custo computacional. Consequentemente, quanto maior o valor de n, mais tempo o computador levará para a resolução do problema.

Figura 4.3 - Resultado do método de Rayleigh-Ritz utilizando funções de forma triangulares com \(n=5\).
Fonte: Elaborada pelo autor, 2019.

Figura 4.4 - Resultado do método de Rayleigh-Ritz utilizando funções de forma triangulares com \(n=10\).
Fonte: Elaborada pelo autor, 2019.
settings