© Ricardo Miranda Martins, 2022 - http://www.ime.unicamp.br/~rmiranda/
Aqui está um caminho rápido para aprender a usar Python para fazer cálculos matemáticos. Se você não sabe programar, esse site é para você. Aqui, seremos duas pessoas que não sabem programar tentando aprender alguma coisa.
Você vai aprender a resolver equações, inverter matrizes, fazer gráficos, resolver equações diferenciais.. o foco não é em detalhes das sintaxes dos comandos. Afinal, é para isso que você paga internet, certo? Ok - vou ser bonzinho e mesmo assim colocar alguns exemplos de coisas simples, usando if
, for
, etc.
O objetivo aqui é ser um guia estilo mão-na-massa ou copie-cole-e-altere.
Se você está meio perdido: para usar o Python, eu recomendo que você use o Jupyter. Vá em https://jupyter.org/try e inicie um JupyterLab. Quando aparecer a tela de seleção, escolha Python na aba "Notebook". Outra opção é usar o Google Colab, clicando aqui, que também é bem intuitivo. A melhor opção mesmo é você instalá-lo em seu computador, usando o Anaconda.
A maioria dos códigos desse tutorial não foram feitos por mim, mas adaptados de códigos encontrados no StackOverFlow, StackExchange, etc. Sempre que ainda tiver o link pro código original, vou indicar, ou então veja os sites da bibliografia que tem muita coisa que foi adaptada deles.
Chega de blá-blá-blá e vamos lá!
Atenção: você está na versão "arquivo único" (seja html ou pdf), que é longa. Clique aqui para ver a versão separada por capítulos.
Alguns sites muito bons para aprender a usar Python com objetivo de fazer coisas de matemática:
Estas notas de aula estão regidas pela Licença Pública Creative Commons Atribuição 4.0 Internacional. Veja um resumo do que você pode ou não fazer clicando aqui.
Vamos colocar o Python para fazer umas contas. Para isso, é só digitar o que você quer na linha aqui de baixo e depois apertar shift+enter. Tudo que você digitar na linha de código começando com # será interpretado como um comentário, e em geral é usado para explicar partes do código.
# isto é um comentário. abaixo, fazemos a soma de 1 com 1
1+1
2
232323*8987
2087886801
Para dividir dois números, é só usar o símbolo "/":
11/4
2.75
O resultado será um número decimal. Se você quiser fazer uma divisão inteira, aquela em que é produzido um quociente e um resto, terá que usar dois comandos. O //
produz o quociente dessa divisão e o %
o resto, como nos exemplos abaixo. Lembre-se que $11=4\cdot 2+3$.
11//4
2
11 % 4
3
O Python tem suas funções ampliadas com base em módulos/bibliotecas. Usaremos muitas delas em nossos exemplos.
Uma das bibliotecas mais poderosas para matemática é a SymPy, para cálculos simbólicos. A SymPy é uma biblioteca enorme, então se importamos todas as funções dela para o ambiente de trabalho, além de começar a dar conflito com algumas outras funções, tudo pode ficar mais lento. Então o comando abaixo vai importar SymPy usando um atalho. Esse é um procedimento totalmente usual. No site do SymPy você tem um tutorial bem legal que eu recomendo.
O comando import sympy as sp
diz que estamos importando a biblioteca SymPy e o prefixo sp
será usado para chamar as funções dessa biblioteca. A seguir, usamos o comando isprime
, que determina se um número é ou não primo.
import sympy as sp
sp.isprime(11)
True
Usar a função isprime
do SymPy é ótimo, mas poderíamos ter programado um "crivo" para verificar primalidade, né? Vamos fazer isso. Vai ser uma primeira oportunidade de conhecer a sintaxe de alguns laços, que podem ser úteis mais pra frente.
Uma forma simples de verificar se um número é primo é a seguinte: tente dividí-lo por todos os inteiros maiores que 1 e menores que ele. Se nenhuma divisão for exata, o número é primo. Se em alguma divisão der resto zero, então o número não será primo. O código abaixo foi adaptado daqui.
def checaprimo(n):
if n == 2:
return True
if n % 2 == 0 or n <= 1:
return False
for divisor in range(3, n, 2):
if n % divisor == 0:
return False
return True
checaprimo(101)
True
Agora vamos pensar um pouco: estamos tentando dividir $n$ por todos os números entre $2$ e $n-1$. Será que precisamos de tudo isso? Ora, se $n$ for divisível por $k$, com $k<n$, então $n$ também será divisível por $n/k$. Das duas, uma: ou $k$ ou $n/k$ será menor que $\sqrt{n}$. A prova disso é fácil: se ambos forem maiores que $\sqrt{n}$, então o produto deles, que sabemos que resulta em $n$, seria $$n=k\cdot (n/k)>\sqrt{n}\cdot \sqrt{n}=n,$$ um absurdo portanto.
Logo, podemos alterar o range
dentro do for
para ir de $3$ até o inteiro maior ou igual a$\sqrt{n}$ (repare no uso da função int
no código).
Como a função $\sqrt{x}$ não está implementada por padrão no Python, ao invés de implementá-la, vamos carregar a biblioteca math
que vem com ela e outras funções simples no pacote. Veja detalhes sobre a math
aqui.
Note que isso aumenta bastante a velocidade de execução do nosso algoritmo.
import math
def checaprimo2(n):
if n == 2:
return True
if n % 2 == 0 or n <= 1:
return False
for divisor in range(3, int(math.sqrt(n)), 2):
if n % divisor == 0:
return False
return True
checaprimo2(101)
True
No código acima, para verificar a primalidade, usamos a função sqrt
do pacote math
do Python, mas isso é um exagero. Podemos facilmente programar uma função que calcula a raiz quadrada.
A função que vamos programar usa um argumento simples, que era usado pelos babilônios por volta do ano 60 d.C. para calcular a raiz quadrada: suponha que $x>0$ e queremos calcular $\sqrt{x}$. Seja $y$ com $y^2<x$. Então $\sqrt{x}$ está entre $y$ e $x/y$ ("perto do ponto médio desse intervalo"). Atualmente essa estratégia é conhecida como método de Heron, e sua demonstração é simples (tem até aqui na Wikipedia).
Em termos mais algoritmicos, a ideia é a seguinte. Queremos calcular $\sqrt{x}$. Escolha $y=1$ (ou qualquer outro número positivo). Então, a menos que $x=1$, a raiz quadrada de $x$ estará entre $y$ e $x/y$. Pegue então o ponto médio desse intervalo e chame de $y_1$. Novamente, a raiz quadrada de $x$ estará entre $y_1$ e $x/y_1$. Pegue novamente o ponto médio desse intervalo e continue sucessivamente. A sequência desses "pontos médios" vai convergir para a raiz quadrada.
def minharaizquadrada(x):
y=1
# aqui é que definimos o numero de iterações, troque 10 pelo que quiser;
# quando maior, melhor aproximação
for k in range(1,10):
y = (y + x/y)/2
return y
# calculando sqrt(2)
minharaizquadrada(2)
1.414213562373095
Pronto, agora temos nossa própria função para calcular raiz quadrada! Vamos rodar novamente aquele teste de primalidade, só que agora com nossa função raiz quadrada.
def checaprimo3(n):
if n == 2:
return True
if n % 2 == 0 or n <= 1:
return False
for divisor in range(3, int(minharaizquadrada(n)), 2):
if n % divisor == 0:
return False
return True
checaprimo3(101)
True
Bom, 101 continua sendo primo, então a função deve funcionar :)
A primeira coisa que a gente faz em um sistema computacional novo é resolver umas equações. Como faremos isso?
No futuro, o computador vai ler a nossa mente e saber exatamente o que a gente quer fazer. Até lá, teremos que nos preocupar com sintaxes. Vamos novamente carregar a biblioteca SymPy (essa linha é opcional, caso você tenha executado o comando anterior, que já carrega a SymPy como sp
).
import sympy as sp
O Python, como padrão, não sabe o que é x, y, z, etc. Temos que definir que essas letras serão reservadas para serem variáveis simbólicas. Isso é feito com o comando abaixo:
x, y = sp.symbols('x y')
Viu como usamos o comando sp.symbols
, com o prefixo sp
? Isso significa que estamos usando o comando symbols
do pacote SymPy. Para resolver equações envolvendo $x,y$, vamos usar o comando solve
, que também está no pacote SymPy. Portanto, teremos que chamá-lo usando sp.solve
.
sp.solve(x+1)
[-1]
O comando sp.solve
assume que você passou uma equação da forma "$\text{equacao}=0$" pra ele, e resolve para as variáveis envolvidas. Portanto, o comando acima resolve a equação $x+1=0$ com respeito à única variável envolvida, que é $x$. Portanto, é claro que se $x+1=0$ então $x=-1$.
Isso é muito importante: ao usar o comando sp.solve
você sempre tem que se lembrar que a equação tem esse formato. Vamos experimentar isso novamente com algumas equações, procurando raízes de $x^2-0=0$ (cuidado com a notação: no Python você deve representar $x^n$ por x**n
).
sp.solve(x**2-9)
[-3, 3]
Contem uma mentirinha para vocês. O SymPy tem a opção de construir uma "equação" para a gente não precisar "passar tudo pro lado esquerdo". O comando para isso é o Eq
, lembrando que precisamos usar sp
antes:
sp.solve(sp.Eq(x**2,9))
[-3, 3]
Vamos resolver mais algumas equações.
sp.solve(x**7-x**6+x**3-9)
[CRootOf(x**7 - x**6 + x**3 - 9, 0), CRootOf(x**7 - x**6 + x**3 - 9, 1), CRootOf(x**7 - x**6 + x**3 - 9, 2), CRootOf(x**7 - x**6 + x**3 - 9, 3), CRootOf(x**7 - x**6 + x**3 - 9, 4), CRootOf(x**7 - x**6 + x**3 - 9, 5), CRootOf(x**7 - x**6 + x**3 - 9, 6)]
Ops! O que aconteceu aqui? Que resposta estranha é essa? Cadê as raízes??????
Tentamos usar o SymPy para encontrar raízes de um polinômio de grau 7 e não deu certo. Como você deve saber, o jovem Galois provou que raízes de polinômios de grau $\geq 5$ não podem ser expressas por radicais envolvendo coeficientes do polinômio, o que popularmente significa que "não tem fórmula para as raízes". A resposta, no entanto, é coerente: ele diz que as soluções de $$x^7-x^6+x^3-9=0$$ são uma das 7 raízes desse polinômio. Tá errado? Não. Adianta de alguma coisa? Também não.
Nesse caso, devemos partir para a busca de soluções numéricas. O SymPy tem um comando parecido com o solve
, só que procura soluções de forma numérica: o nsolve
. A sintaxe para encontrar uma solução de $F(x)=0$, com o chute inicial para a solução sendo $x_0$ é sp.nsolve(F,x0)
. Note que $x_0$ não precisa ser uma solução, é só uma estimativa inicial.
sp.nsolve(x**7-x**6+x**3-9,1)
Encontramos uma solução! Um dos "problemas" do nsolve
é que ele só procura uma solução de cada vez, dependendo do ponto inicial dado. Na dúvida, você pode dar uma "conferida" fazendo o gráfico. Falaremos mais sobre gráficos depois, mas fica aqui um gostinho.
Usaremos o próprio comando de plotar do SymPy. Ele não é o que de melhor existe no Python para plotar gráficos, mas é bem eficiente para coisas simples.
Abaixo plotamos o gráfico da função $y=x^7-x^6+x^3-9$, com $x\in[-2,2]$ e limitamos o eixo $y$ ao intervalo $-50,50$.
x = sp.symbols('x')
sp.plot(x**7-x**6+x**3-9,(x,-2,2),ylim=[-50,50])
<sympy.plotting.plot.Plot at 0x7f79b89c98e0>
Olhando o gráfico, parece que a solução que achamos acima é mesmo uma solução mesmo, né?
Vamos continuar resolvendo equações. Equações envolvendo mais que uma variável, que são em geral difíceis de serem resolvidas, ficam fáceis no Python:
sp.solve((x**2+y**2+x-10,x+y-2),(x,y),dict=True)
[{x: 3/4 - sqrt(57)/4, y: 5/4 + sqrt(57)/4}, {x: 3/4 + sqrt(57)/4, y: 5/4 - sqrt(57)/4}]
Não se iluda com o comando acima. Em geral, para muitas equações e muitas variáveis, será preciso pedir soluções numéricas, com o nsolve
. Você pode mudar o formato da resposta usando a opção dict
, e também pode especificar o número de casas decimais da resposta com a opção prec
:
sp.nsolve((x**2+y**2+x-10,x+y-2),(x,y),[1,1],prec=30)
sp.nsolve((x**2+y**2+x-10,x+y-2),(x,y),[1,1],dict=True,prec=30)
[{x: 2.63745860881768742430917120174, y: -0.637458608817687424309171201737}]
Ao invés de procurar soluções para $F(x)=0$, podemos estar interessados em calcular $F(x_0)$ para algum valor de $x_0$. Em muitos casos, não precisamos definir $F$ como uma função, ela pode estar definida como uma expressão e daí pedir para o Python substituir os valores, como por exemplo no comando abaixo, que define $F=x^2+y^3-2$ e calcula $F(1,2)$.
x, y = sp.symbols('x y')
F=x**2+y**3-2
F.subs(x,1).subs(y,2)
Para armazenar o valor de uma expressão, não precisamos declarar antes como símbolo. E depois podemos usar em contas, normalmente:
k=F.subs(x,1).subs(y,2)
k**2
O Python, assim como a maioria dos sistemas computacionais, mantém um comando especial para sistemas linares. No caso do SymPy, é o linsolve
. Os três comandos abaixo dão a mesma resposta, apesar de usarem métodos diferentes para obter a solução (note que cada saída é também formatada de uma forma):
sp.linsolve((x+y-1,x-y-3),(x,y))
sp.solve((x+y-1,x-y-3),(x,y))
{x: 2, y: -1}
sp.nsolve((x+y-1,x-y-3),(x,y),[1,1],prec=5)
Por fim, vamos resolver um sistema de equações com o nsolve
e depois entender geometricamente o que está acontecendo. Compare as soluções que encontramos com a figura que foi plotada. O comando plot_implicit
plota curvas dadas implicitamente, e usamos o Or
para indicar que são duas curvas.
sp.nsolve((x**5+y**6+x**3-1,x+y-1),(x,y),[0.5,0],dict=True)
[{x: 0.837615759482675, y: 0.162384240517325}]
sp.nsolve((x**5+y**6+x**3-1,x+y-1),(x,y),[1,2],dict=True)
[{x: -1.12228828894446e-23, y: 1.00000000000000}]
sp.plot_implicit(sp.Or(sp.Eq(x**5+y**6+x**3,1),sp.Eq(x+y,1)))
<sympy.plotting.plot.Plot at 0x7f79d8da7df0>
Suponha que você queira achar uma solução para a equação $f(x)=0$. O que é a primeira coisa que você pensa? Matemáticos costumam pensar: será que existe alguma solução?
Faz sentido: quando você quer procurar uma item perdido pela casa, antes de começar a procurar num cômodo, você primeiro pondera se existe mesmo alguma chance dele estar ali. Quem tem filhos sabe que essa lógica nem sempre funciona, e é comum encontrar peças de Lego em lugares nunca antes imaginados, mas vamos continuar com nosso raciocínio.
Em todo caso, vamos pensar sobre condições para existência de solução para $f(x)=0$. Suponha que $f(x)$ seja uma função "boa", o que para a gente no momento significa que ela é contínua e definida num intervalo fechado. Em resumo, ela é daquelas funções que você consegue fazer o gráfico sem tirar o lápis do papel.
Suponha que existam dois números $x_1$ e $x_2$, com a propriedade de que $f(x_1)$ e $f(x_2)$ tem sinais contrários, por exemplo $f(x_1)<0$ e $f(x_2)>0$. Então, ao fazermos o gráfico de $y=f(x)$, teremos os pontos $(x_1,f(x_1))$ e $(x_2,f(x_2))$ em lados opostos do eixo $x$, um abaixo e outro acima do eixo.
Ora, o gráfico de $y=f(x)$ tem que, de alguma forma, passar por esses dois pontos! Faça aí uns gráficos em uma folha de papel e se convença de que a única possibilidade é existir um ponto $x_0$, entre $x_1$ e $x_2$, de modo que $f(x_0)=0$. Se você está convencido disso, saiba que pode ser que você tenha provado o Teorema de Bolzano, ainda que sem formalismo.
O resumo é que se você sabe que $f(x_1)<0$ e $f(x_2)>0$, então existe $x_0$ entre $x_1$ e $x_2$ que resolve a equação $f(x)=0$. Como calcular $x_0$, ou pelo menos uma aproximação para ele? Uma possibilidade é usar o método da bisseção (detalhes aqui), que vamos explicar brevemente.
Esse método usa o seguinte argumento: sejam $a,b$ tais que $f(a)<0$ e $f(b)>0$. Então sabemos que existe uma solução para $f(x)$ com $x\in[a,b]$. Pegue um outro ponto desse intervalo, digamos o ponto médio $x_0=(a+b)/2$. Se $f(x_0)=0$, encontramos um zero. Se $f(x_0)\neq 0$, então temos duas possibilidades:
No primeiro caso, repetindo o argumento anterior, devemos ter uma solução de $f(x)=0$ no intervalo $[a,x_0]$, e no segundo caso teremos uma solução para $f(x)=0$ no intervalo $[x_0,b]$. Agora faça tudo novamente com um desses intervalos. A ideia é que em cada passo você irá confinar a solução a um intervalo menor, encontrando aproximações cada vez melhores para a solução da equação.
Antes do código, vamos fazer um exemplo "manual".
Seja $f(x)=x^7+x-1$. Queremos resolver $f(x)=0$. Essa função é contínua. Temos que $f(0)=-1$ e $f(1)=1$. Então deve existir alguma solução para $f(x)=0$ no intervalo $[0,1]$.
Portanto, existe uma solução entre $[1/2,1]$ e já diminuímos o intervalo onde está a solução para um de comprimento $1/2$. Calculamos novamente o valor da função no ponto médio desse intervalo:
Como $f(3/4)<0$ e $f(1)>0$, temos uma solução para $f(x)=0$ no intervalo $[3/4,1]$. Vamos novamente calcular o valor da função no ponto médio:
Como $f(7/8)>0$ e $f(3/4)<0$, temos uma solução para $f(x)=0$ no intervalo $[3/4,7/8]$. Vamos calcular novamente no ponto médio:
Assim, teremos uma solução para $f(x)=0$ no intervalo $[3/4,13/16]$, ou seja, entre $0,75$ e $0,8125$. Se continuarmos usando o método, chegaremos que uma boa aproximação para a solução é $x=0,7965...$.
Note que, a cada passo, simplesmente calculamos o valor da função no ponto médio, comparamos com os extremos, e daí decidimos por um dos dois subintervalos. A única coisa que devemos decidir é quanto o algoritmo irá parar: isso pode ser feito após um número determinado de passos, ou quando o tamanho do intervalo já for suficiente. Por exemplo, se decidirmos parar sempre que o intervalo tiver comprimento menor que $10^{-(k+1)}$, teremos uma solução com precisão de $k$ casas decimais.
A implementação abaixo é a mesma do Mathematical Python, do Patrick Walls (veja aqui).
# a funcao vai pegar uma funcao f, extremos de um intervalo a, b e N será o
# número máximo de iterações que serão realizadas
def bisseccao(f,a,b,N):
if f(a)*f(b) >= 0:
print("Falha, escolha outro intervalo.")
return None
a_n = a
b_n = b
for n in range(1,N+1):
m_n = (a_n + b_n)/2
f_m_n = f(m_n)
if f(a_n)*f_m_n < 0:
a_n = a_n
b_n = m_n
elif f(b_n)*f_m_n < 0:
a_n = m_n
b_n = b_n
elif f_m_n == 0:
print("Solução encontrada!")
return m_n
else:
print("O método falhou.")
return None
return (a_n + b_n)/2
# vamos testar com o nosso polinômio, fazendo 50 iterações no intervalo [0,1]
f = lambda x: x**7 +x - 1
bisseccao(f,0,1,50)
Solução encontrada!
0.7965443541284571
Veja que após 50 iterações, chegamos numa boa aproximação para a solução. Veja abaixo um gráfico da função $f(x)$, que "comprova" a existência do zero.
import sympy as sp
x = sp.symbols('x')
sp.plot(x**7+x-1,(x,0,1),ylim=[-1,1])
<sympy.plotting.plot.Plot at 0x7f79b8fe2730>
Fazer gráficos no Python é sinônimo para matplotlib
. Esse pacote é fantástico e muito poderoso. Produz gráficos bem legais se for bem utilizada. O site do Matplotlib tem muitos exemplos legais, e aqui vamos abordar somente o básico.
Usaremos também o NumPy, que é o "primo" do SymPy, só que para cálculos numéricos. Para um tutorial do NumPy, veja no site deles a documentação.
Os gráficos mais simples feitos no computador são simples amontoados de pontos, bem colocados. Sabe quando a gente tenta fazer o gráfico de $y=x^2$, marca 2 pontos, risca com a régua, e ganha 0 na questão? Então, se você tivesse marcado 5000 pontos bem colocados, seu professor poderia até ter considerado alguma coisa - e é exatamente aí que o NumPy entra em jogo, para construir essas "malhas" de pontos.
Vamos começar com o gráfico de $f(x)=x^2+1$, com $x\in[-3,3]$.
# importando as bibliotecas
import numpy as np
import matplotlib.pyplot as plt
# definindo a funcao
def f(x):
return x**2+1
# definindo o intervalo de x e quantos pontos serão criados.
# abaixo definimos que o gráfico será criado com base em 50 pontos
# com coordenada x igualmente distribuída entre -3 e 3.
# a variável x armazenará uma lista com esses 50 pontos.
# esse processo é conhecido por "discretização".
x = np.linspace(-3, 3, 50)
# definicao de y=f(x). na verdade, o que esse comando faz é criar uma
# lista aplicando a funcao f(x) que definimos acima nos pontos da lista
# que contém os valores de x (os tais 50 valores entre -3 e 3)
y = f(x)
# plotando a funcao y=f(x).
plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f79b913e9d0>]
Só por curiosidade, o que acontece se ao invés de 50 pontos, escolhermos menos pontos?
Abaixo plotamos o mesmo gráfico acima, só que com base em somente 4 pontos:
x = np.linspace(-3, 3, 4)
y=f(x)
plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f79b929fbb0>]
Viu? É por isso que com poucos pontos a gente ganha zero na questão! :)
Uma boa estratégia ao usar o matplotlib
é construir o gráfico aos poucos, para poder adicionar legendas nos eixos e outros detalhes. Isso pode ser feito como no exemplo abaixo.
# vamos chamar de "a" um primeiro plot vazio, só com os eixos.
a = plt.axes()
# agora damos nomes para os eixos do gráfico vazio.
a.set_xlabel('x');
a.set_ylabel('y');
# delimitando a área da janela de visualização para ser com
# x entre -2 e 2 e y entre -4 e 4
a.set(xlim=(-2,2), ylim=(-4,4))
# e finalmente adicionamos o gráfico.
# note que agora vamos até aumentar o valor de x, mas isso
# não tem impacto na janela de visualização, que já definimos
# anteriormente. colocamos 100 pontos para o gráfico ficar suave.
x = np.linspace(-10, 10, 100)
y=f(x)
# agora o plot tem que ser dado com o prefixo a, para usar as configurações
# anteriores.
a.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f79b9047e50>]
Podemos também usar o Python para plotar curvas parametrizadas. O comando é parecido:
t = np.linspace(0, 2*np.pi, 200)
x = np.cos(t)
y = np.sin(2*t)
plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x7f79da22b130>]
Note que o comando é praticamente o mesmo nos dois casos acima: plotando muitos pontos de uma curva parametrizada (no primeiro caso, a curva é da forma $(t,f(t))$).
Um terceiro tipo de gráfico bidimensional é quando queremos plotar curvas de nível de uma função. Para isso, temos um outro comando, o contour
. Veja muitas outras opções sobre plots de curvas de nível aqui nesse site.
import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
# o comando abaixo é muito parecido com o linspace que usamos antes.
# a diferença é que aqui, ao invés de definirmos o número de pontos
# que iremos usar para plotar, vamos definir ponto inicial, ponto
# final e o incremento, neste caso delta=0.025.
# é só uma outra forma de fazer a discretização.
delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
# aqui o primeiro truque: transformamos os dois "intervalos" de x e y
# numa malha bidimensional, fazendo uma espécie de produto cartesiano
# dos pontos, e renomeamos a primeira coordenada desses pontos como x
# e a segunda como y. esse é só um artifício técnico.
x, y = np.meshgrid(x, y)
# aqui definimos a funcao/equacao da qual vamos plotar as curvas de nivel.
# note que ela usa as variáveis com letras maiúsculas, pois o que vamos
# armazenar no vetor Z é o valor dessa funcao em cada um dos pontos da
# malha criada.
z = x**2-2*y**2-1
fig, ax = plt.subplots()
# a sintaxe do contor é intervalo X, intervalo Y, funcao e quantidade
# de contornos. abaixo, plotamos 20 curvas de nível.
contornos = ax.contour(x, y, z, 20)
ax.clabel(contornos, inline=True, fontsize=10)
ax.set_title('Curvas de nível')
plt.contour(x, y, z)
<matplotlib.contour.QuadContourSet at 0x7f79c97dd9a0>
O matplotlib também faz gráficos tridimensionais. A ideia do plot continua a mesma: o gráfico vai ser obtido pela união de vários pontos.
import numpy as np
import matplotlib.pyplot as plt
# definindo a função
def f(x, y):
return np.sqrt(9-x**2+y**2)
# discretizando x e y
x = np.linspace(-3, 3, 50)
y = np.linspace(-3, 3, 50)
# artifício do produto cartesiano
x, y = np.meshgrid(x, y)
# define z como z=f(x,y)
z = f(x, y)
# indica que iremos fazer um gráfico 3d e nomeia os eixos
ax = plt.axes(projection='3d')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
# finalmente, adiciona a função (no caso, estamos plotando
# em versão wireframe, ou seja, você vai ver a malha sobre a
# superfície.
ax.plot_wireframe(x, y, z)
<mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x7f79b90965e0>
Podemos ainda plotar superfícies parametrizadas. O processo é parecido com o de antes: discretizamos o domínio e daí calculamos a função na malha criada. Vamos fazer isso para plotar um toro, usando sua parametrização usual. O código abaixo foi inspirado nesse site.
# discretizando as duas variáveis de ângulo
angle = np.linspace(0, 2 * np.pi, 32)
theta, phi = np.meshgrid(angle, angle)
# pegando valores comuns para os raios interno e exteno
r, R = 0.3, 1
# parametrização
x = (R + r * np.cos(phi)) * np.cos(theta)
y = (R + r * np.cos(phi)) * np.sin(theta)
z = r * np.sin(phi)
# criando o ambiente 3d
fig = plt.figure()
ax = plt.axes(projection='3d')
# limitando os eixos
ax.set_xlim3d(-1, 1)
ax.set_ylim3d(-1, 1)
ax.set_zlim3d(-1, 1)
# exibindo a figura
ax.plot_surface(x, y, z, rstride=1, cstride=1)
plt.show()
Por fim, podemos ainda fazer plots de curvas parametrizadas em 3D:
# criando o ambiente 3d
ax = plt.axes(projection='3d')
# discretizando t
t = np.linspace(0, 2*np.pi, 200)
# definindo as coordenadas da parametrizacao
x = np.cos(t)
y = np.sin(2*t)
z = 2*t
# plotando
ax.plot(x, y, z)
[<mpl_toolkits.mplot3d.art3d.Line3D at 0x7f79da4e3340>]
Resolver sistemas lineares é o ganha pão de matemáticos. Aliás, você sabe por quê dedicamos tanto tempo ensinando alunos a resolverem sistemas lineares? Simples: é que não sabemos direito resolver os não-lineares.
Existem basicamente duas formas do Python resolver sistemas lineares: pelo comando tradicional solve
do SymPy ou pelo comando específico para sistemas lineares, o linsolve
. Essa segunda forma é mais rápida se o sistema for muito grande. No entanto, como você já deve imaginar, resolver sistemas lineares usando o NumPy é ainda mais rápido do que usando o SymPy.
Para sistemas muito grandes (muito mais que 1000 equações/variáveis), existem outros pacotes mais eficientes - se for seu caso, imagino que você não esteja aprendendo Python aqui nesse tutorial.
A rotina abaixo resolve o sistema
$$\left\{\begin{array}{lcl} x+y-2z&=&1,\\ x+y-z&=&3,\\ 3x+y-z&=&3 \end{array}\right.$$usando o linsolve
do SymPy.
import sympy as sp
x, y, z = sp.symbols('x y z')
eq1=sp.Eq(x+y-2*z,1)
eq2=sp.Eq(x+y-z,3)
eq3=sp.Eq(3*x+y-z,3)
sp.linsolve((eq1,eq2,eq3),(x,y,z))
Se quisermos a solução num outro formato, podemos usar o solve
:
sp.solve((eq1,eq2,eq3),(x,y,z))
{x: 0, y: 5, z: 2}
Sejam $P=(\alpha_1,\beta_1,\gamma_1)$, $Q=(\alpha_2,\beta_2,\gamma_2)$ e $R=(\alpha_3,\beta_3,\gamma_3)$ pontos não-colineares em $\mathbb R^3.$ Encontre a equação do plano $\pi$, da forma $ax+by+cz=d$, que contém estes três pontos.
Primeiro vamos a uma solução "algébrica", levando em conta a equação do plano. Vamos usar os valores $P=(0,0,1)$, $Q=(3,1,0)$ e $R=(0,2,2)$ no exemplo abaixo.
import sympy as sp
# define as variaveis e constantes que vamos usar
x, y, z = sp.symbols('x y z')
a, b, c, d = sp.symbols('a b c d')
# equacao do plano na forma geral
eq = sp.Eq(a*x+b*y+c*z,d)
# definindo os pontos
P=sp.Array([0,0,1])
Q=sp.Array([3,1,0])
R=sp.Array([0,2,2])
# substituindo x,y,z na equacao do plano pelos pontos
eq1=eq.subs([(x,P[0]),(y,P[1]),(z,P[2])])
eq2=eq.subs([(x,Q[0]),(y,Q[1]),(z,Q[2])])
eq3=eq.subs([(x,R[0]),(y,R[1]),(z,R[2])])
# as equacoes acima resultam num sistema nas variaveis a, b, c, d.
# resolvendo o sistema:
sol=sp.linsolve((eq1,eq2,eq3),(a,b,c,d))
#armazenando as solucoes
(A,B,C,D)=tuple(*sol)
# finalmente, exibindo a equação do plano, substituindo a,b,c,d pelos
# valores que encontramos
eq.subs([(a,A),(b,B),(c,C),(d,D)])
A solução acima fica na dependência de uma constante, no caso $d$. Pode-se substituir qualquer valor para $d$.
Agora vamos a uma solução um pouco mais geométrica. Vamos obter o vetor normal do plano como sendo o produto vetorial $n=u\times v$, onde $u=PQ$ e $v=PR$. A seguir, obtemos a equação do plano utilizando um dos pontos por onde ele passa. A solução é mais longa, porém é bem mais elegante e evita o aparecimento de constantes indesejadas.
# importando a biblioteca para calculos simbolicos
import sympy as sp
# vamos usar a biblioteca NumPy para calcular o produto vetorial e tambem
# o produto interno
import numpy as np
# define as variaveis e constantes que vamos usar
x, y, z = sp.symbols('x y z')
a, b, c, d = sp.symbols('a b c d')
# equacao do plano na forma geral
eq = sp.Eq(a*x+b*y+c*z,d)
# definindo os pontos
P=sp.Array([0,0,1])
Q=sp.Array([3,1,0])
R=sp.Array([0,2,2])
# criando os vetores
u=Q-P
v=R-P
# obtendo o vetor normal
n=np.cross(u,v)
(a,b,c)=n
# termo nao-homogeneo
d=np.dot(n,P)
# exibindo a equação do plano
sp.Eq(a*x+b*y+c*z,d)
Claro que poderíamos ter mesclado os capítulos sobre matrizes e sistemas lineares. Só deixamos separado para poder explorar um pouco melhor as propriedades desses retângulos cheios de números que nós adoramos.
Aliás, você sabe de onde vem a regra estranha de multiplicar matrizes? Vem da composição de operadores lineares. O Python trabalha com matrizes muito bem. Só devemos ter um pouco de cuidado com a notação.
Vamos fixar a notação e trabalhar abaixo com as matrizes $$A=\begin{pmatrix}1&2\\ -3&1\end{pmatrix},$$ $$B=\begin{pmatrix}2\\ 4\end{pmatrix}$$ e $$C=\begin{pmatrix}2&5\\1&1\end{pmatrix}$$ em nossos exemplos.
import sympy as sp
A=sp.Matrix([[1,2],[-3,1]])
B=sp.Matrix([[2],[4]])
C=sp.Matrix([[2,5],[4,10]])
A
B
C
Algumas matrizes especiais podem ser criadas de modo mais simples. Por exemplo, a identidade $n\times n$ pode ser criada com o comando eye(n)
.
sp.eye(2)
Já a matriz nula $m\times n$ pode ser criada com o comando zeros(m,n)
:
sp.zeros(2,2)
Podemos também criar de modo muito fácil uma matriz $m\times n$ só com 1's com o comando ones(m,n)
:
sp.ones(2,2)
O produto de matrizes é feito com o mesmo símbolo usual: para obter o produto de A por B, o comando é A*B
:
A*C
A*B
Lembre-se que nem sempre dá para multiplicar duas matrizes - só podemos multiplicar uma matriz $m\times n$ por uma $n\times r$. Se tentarmos com matrizes de dimensões "estranhas", vai dar erro:
B*A
--------------------------------------------------------------------------- ShapeError Traceback (most recent call last) /var/folders/90/lb7fswt93477cpj1pl8z494m0000gn/T/ipykernel_41171/3451148411.py in <module> ----> 1 B*A ~/opt/anaconda3/lib/python3.9/site-packages/sympy/core/decorators.py in binary_op_wrapper(self, other) 134 if f is not None: 135 return f(self) --> 136 return func(self, other) 137 return binary_op_wrapper 138 return priority_decorator ~/opt/anaconda3/lib/python3.9/site-packages/sympy/matrices/common.py in __mul__(self, other) 2761 """ 2762 -> 2763 return self.multiply(other) 2764 2765 def multiply(self, other, dotprodsimp=None): ~/opt/anaconda3/lib/python3.9/site-packages/sympy/matrices/common.py in multiply(self, other, dotprodsimp) 2783 getattr(other, 'is_MatrixLike', True))): 2784 if self.shape[1] != other.shape[0]: -> 2785 raise ShapeError("Matrix size mismatch: %s * %s." % ( 2786 self.shape, other.shape)) 2787 ShapeError: Matrix size mismatch: (2, 1) * (2, 2).
Lembre-se também que o produto de matrizes não é comutativo, ou seja, $A\cdot C$ pode ser diferente de $C\cdot A$, ou seja, $AC-CA$ pode ser diferente de zero:
A*C-C*A
Se $A$ é uma matriz quadrada, dizemos que ela é invertível se existe outra matriz $D$ tal que $AD=DA=I_{2\times 2}$. Essa matriz $D$ é chamada de matriz inversa de $A$ e denotada por $D=A^{-1}$. Nem toda matriz admite inversa, mas quando a inversa existe ela pode ser calculada no Python com o comando A**(-1)
:
A**(-1)
Uma propriedade das matrizes que tem inversa é que ela tem o determinante diferente de zero. O determinante é uma "propriedade" da matriz, então podemos "resgatar" o determinante de $A$ com o sufixo A.det()
:
A.det()
A mesma lógica é seguida se quisermos os autovalores ou autovetores da matriz:
A.eigenvals()
{1 - sqrt(6)*I: 1, 1 + sqrt(6)*I: 1}
A.eigenvects()
[(1 - sqrt(6)*I, 1, [Matrix([ [sqrt(6)*I/3], [ 1]])]), (1 + sqrt(6)*I, 1, [Matrix([ [-sqrt(6)*I/3], [ 1]])])]
Se você achou a saída do comando acima um pouco feia, talvez prefira usar o pprint
(pretty print):
sp.init_printing(use_unicode=True)
A.eigenvects()
Agora sim, dá pra entender né? A saída é composta pelo autovalor, sua multiplicidade, e o(s) autovetor(es) associado(s). Vamos fazer um exemplo $3\times 3$ que tem um autovalor de multiplicidade 2 para ilustrar melhor isso:
M=sp.Matrix([[3,0,0],[0,3,0],[0,1,1]])
M.eigenvects()
Determinar a solução do sistema linear $AX=0$ é o mesmo que calcular o "núcleo" da matriz $A$. Isso pode ser feito com o sufixo nullspace()
. Vamos fazer isso para as matrizes $A$ e $C$ para comparar os resultados:
A.nullspace()
C.nullspace()
Note que o resultado acima nos permite concluir que a matriz $C$ tem um autovalor nulo, e o autovetor associado é justamente a resposta anterior, que é o gerador do núcleo de $C$. Quer conferir? Então toma:
C.eigenvects()
Também podemos obter os vetores que são geradores da imagem de $A$, ou seja, se considerarmos o operador $T_A:\mathbb R^2\rightarrow\mathbb R^2$, $T_A(v)\mapsto Av$, então o sufixo columnspace()
nos dará os vetores que são geradores da imagem desse operador:
A.columnspace()
C.columnspace()
Uma coisa que sempre fazemos com matrizes é calcular a sua forma de Jordan, ou, quando possível, diagonalizá-la. Isso pode ser feito com o comando diagonalize()
:
A.diagonalize()
Esse comando retorna duas matrizes: a primeira delas é a matriz mudança de base; a segunda é a forma diagonal da matriz. Se quisermos usar notações para essas matrizes, vamos denotar por $Q$ a matriz mudança de base e $D$ a matriz diagonal:
Q,D = A.diagonalize()
Da teoria, sabemos que $Q\cdot D\cdot Q^{-1}=A$. Em geral, isso só dá certo de usarmos o simplify
no produto matricial do lado esquerdo:
sp.simplify(Q*D*(Q**(-1)))
Considere uma equação quadrática da forma $$ax^2+bxy+cy^2+dx+ey+f=0.$$
Você já deve saber que a representação dos pontos $(x,y)$ que satisfaem essa equação tem uma classificação bem detalhada: basicamente, com exceção de alguns casos degenerados, teremos ou uma elipse ou uma hipérbole ou uma parábola. São as famosas "cônicas", curvas que são obtidas na interseção de um plano com um cone duplo.
O procedimento mais comum usado para decidir qual dos casos temos, e para escrever uma equação mais "simpática" para essa cônica é usar autovalores e autovetores. Vamos fazer isso no exemplo abaixo.
Você pode trocar a equação que deve continuar funcionando (cuidado com algumas mudanças de coordenadas e com divisões por zero que podem aparecer no processo ao alterar os coeficientes).
A ideia do "reconhecimento de cônicas" é razoavelmente simples, mas os cálculos são bem chatos de fazer na mão.
A equação $$ax^2+bxy+cy^2+dx+ey+f=0$$ pode ser reescrita em forma matricial como $$\begin{pmatrix}x&y \end{pmatrix}\begin{pmatrix}a&b/2\\ b/2&c\end{pmatrix}\begin{pmatrix}x\\y \end{pmatrix} +\begin{pmatrix}d&e \end{pmatrix}\begin{pmatrix}x\\y \end{pmatrix}+\begin{pmatrix}f\end{pmatrix}=0.$$
Como a matriz $\begin{pmatrix}f\end{pmatrix}$ é $1\times 1$, em geral não iremos usar a notação matricial e escrever somente $f$:
$$\begin{pmatrix}x&y \end{pmatrix}\begin{pmatrix}a&b/2\\ b/2&c\end{pmatrix}\begin{pmatrix}x\\y \end{pmatrix} +\begin{pmatrix}d&e \end{pmatrix}\begin{pmatrix}x\\y \end{pmatrix}+f=0.$$Denote por $$A=\begin{pmatrix}a&b/2\\ b/2&c\end{pmatrix}.$$ Essa matriz satisfaz $A=A^t$, então pelo Teorema Espectral provado por Cauchy em 1829, existem matrizes $Q$ e $D$, sendo $D$ uma matriz diagonal, de modo que $$QD Q^{-1}=A,$$ o que é equivalente a $$D=Q^{-1}AQ.$$
Podemos construir as matrizes $Q$ e $D$ assim: a matriz $D$ é a matriz diagonal com os autovalores de $A$ e a matriz $Q$ é uma matriz cujas colunas são autovetores normais (de norma 1) de $A$, satifazendo $Q^t=Q^{-1}$. Após calculá-las, realizamos a mudança de coordenadas
$$\begin{pmatrix}x\\y\end{pmatrix}=Q\begin{pmatrix}u\\v\end{pmatrix}.$$Substituindo na equação da cônica, temos $$\left(Q\begin{pmatrix}u\\v\end{pmatrix}\right)^t \begin{pmatrix}a&b/2\\ b/2&c\end{pmatrix}Q\begin{pmatrix}u\\v\end{pmatrix} +\begin{pmatrix}d&e \end{pmatrix}Q\begin{pmatrix}u\\v\end{pmatrix}+f=0,$$ ou seja, $$\begin{pmatrix}u&v \end{pmatrix}Q^t AQ\begin{pmatrix}u\\v\end{pmatrix} +\begin{pmatrix}d&e \end{pmatrix}Q\begin{pmatrix}u\\v\end{pmatrix}+f=0.$$
Usando que $Q^tAQ=D$ a equação acima fica $$\begin{pmatrix}u&v \end{pmatrix}D\begin{pmatrix}u\\v\end{pmatrix} +\begin{pmatrix}d&e \end{pmatrix}Q\begin{pmatrix}u\\v\end{pmatrix}+f=0.$$
Se $$D=\begin{pmatrix}\lambda&0\\0&\mu\end{pmatrix},$$ onde $\lambda,\mu$ são os autovalores de $A$, então escrevendo novamente a equação da cônica temos $$\lambda u^2+\mu v^2+\alpha u+\beta v+f=0,$$ para alguns valores de $\alpha,\beta$. O próximo passo é completar quadrados para deixar a equação mais simples. Aqui é preciso tomar cuidado, e a teoria não pode ser tão geral, pois algumas condições degeneradas podem ocorrer ($\lambda$ ou $\mu$ serem zero, por exemplo). Vamos supor que $\lambda\neq 0$, $\mu\neq 0$ e seguir com as contas.
Reescrevemos a equação anterior como $$\lambda\left( u^2+\dfrac{\alpha u}{\lambda}\right)+\mu \left(v^2+\dfrac{\beta v}{\mu}\right)+f=0.$$
Completando quadrados, temos $$\lambda\left( u^2+\dfrac{\alpha u}{\lambda}\right)+\mu \left(v^2+\dfrac{\beta v}{\mu}\right)+f=0$$ e daí $$\lambda\left( u^2+\dfrac{\alpha u}{\lambda}+ \dfrac{\alpha^2}{4\lambda^2} \right)+\mu \left(v^2+\dfrac{\beta v}{\mu}+\dfrac{\beta^2}{4\mu^2}\right)+f-\dfrac{\alpha^2}{4\lambda}-\dfrac{\beta^2}{4\mu}=0.$$
Agrupando tudo, ficamos com a equação $$\lambda\left( u+\dfrac{\alpha}{2\lambda} \right)^2+\mu \left(v+\dfrac{\beta}{2\mu}\right)^2+\tilde f=0,$$ onde $\tilde f=f-\dfrac{\alpha^2}{4\lambda}-\dfrac{\beta^2}{4\mu}$.
Fazendo uma nova mudança de coordenadas para $w=u+\dfrac{\alpha}{2\lambda}$, $z=v+\dfrac{\beta}{2\mu}$ ficamos com a equacão $$\lambda w^2+\mu z^2+\tilde f=0.$$
Conhecendo os valores de $\lambda,\mu,\tilde f$ poderemos reconhecer a cônica! Vamos agora fazer os cálculos de tudo isso no Python, e depois fazer alguns gráficos.
a=1
b=2
c=-3
d=1
e=2
f=-4
x, y = sp.symbols('x y')
sp.Eq(a*x**2+b*x*y+c*y**2+d*x+e*y+f,0)
Vantagens do computador: vamos usar o Python pra pegar uma dica do que esperar lá no fim, plotando os pontos que satisfazem essa equação. O código abaixo é uma versão simplificada do nosso código para plotar curvas de nível (no caso, a curva de nível 0).
import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
delta = 0.01
x = np.arange(-4.0, 4.0, delta)
y = np.arange(-4.0, 4.0, delta)
x, y = np.meshgrid(x, y)
z = a*x**2+b*x*y+c*y**2+d*x+e*y+f
fig = plt.figure()
ax = fig.add_subplot()
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.contour(x, y, z,0)
<matplotlib.contour.QuadContourSet at 0x7f79b9335280>
Humm.. me parece que teremos uma hipérbole. Isso significa que podemos esperar um autovalor positivo e um negativo. Vamos começar as contas definindo a matriz A.
Aqui vamos usar um truque: queremos levar todos esses cálculos de forma algébrica, sem aproximações numéricas. Só que na definição da matriz $A$, temos um $b/2$: se digitarmos somente $b/2$, o resultado vai ser em ponto flutuante. Então vamos forçar o SymPy a usar o $b/2$ como sendo um número racional, com o comando Rational(b,2)
.
A=sp.Matrix([[a,sp.Rational(b, 2)],[sp.Rational(b, 2),c]])
Agora vamos pedir ao Python para diagonalizar a matriz $A$, salvar a matriz diagonal como $D$ e a matriz mudança de base como $Q$:
Q,D = A.diagonalize()
Conferindo como estão as matrizes:
Q,D
Para que a nossa estratégia de mudança de base para reconhecimento de cônica funcione, precisamos pegar a matriz de mudança de base como sendo uma matriz cujas colunas formam uma base ortonormal de $\mathbb R^2$. No momento temos somente uma base. Vamos então escolher outra matriz $Q$ normalizando as colunas da matriz atual. Isso pode ser feito com o comando abaixo:
n1=sp.simplify(sp.sqrt(Q[0]**2+Q[2]**2))
n2=sp.simplify(sp.sqrt(Q[1]**2+Q[3]**2))
Q2=sp.simplify(sp.Matrix([[Q[0]/n1,Q[1]/n2],[Q[2]/n1,Q[3]/n2]]))
Q=Q2
Q
Note que agora a matriz $Q$ satisfaz o que queríamos: $Q^t=Q^{-1}$..
sp.simplify(Q**(-1)-Q.transpose())
... e ainda vale $Q^{-1}AQ=D$:
sp.simplify(Q**(-1)*A*Q)
Vamos agora ver como ficou a cônica após a primeira mudança de coordenada que fizemos:
# aplicando a mudança de coordenadas; z2 será a nova equacao
x, y = sp.symbols('x y')
z2=sp.simplify(
(sp.Matrix([[x,y]])*(Q**(-1)*A*Q)*sp.Matrix([[x],[y]]))+(sp.Matrix([[d,e]]))*Q*(sp.Matrix([[x],[y]]))+sp.Matrix([[f]])
)[0]
sp.init_printing(use_unicode=False)
# exibindo a nova equacao, e como vamos usá-la para plotar, passando tudo
# pra ponto flutuante. você vai precisar copiar o resultado desse comando e
# colar na próxima linha de código, no z = ....
z3=z2.evalf()
repr(z3)
'1.23606797749979*x**2 + 1.43275483056186*x - 3.23606797749979*y**2 + 1.71674505838791*y - 4.0'
import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
delta = 0.01
x = np.arange(-4.0, 4.0, delta)
y = np.arange(-4.0, 4.0, delta)
x, y = np.meshgrid(x, y)
z3 = 1.23606797749979*x**2 + 1.43275483056186*x - 3.23606797749979*y**2 + 1.71674505838791*y - 4.0
fig = plt.figure()
ax = fig.add_subplot()
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.contour(x, y, z3,0)
<matplotlib.contour.QuadContourSet at 0x7f79a850eaf0>
Olha só, já conseguimos desentortar a hipérbole!! Agora só precisamos da segunda mudança de coordenadas para poder centralizá-la. Vamos precisar calcular $\alpha$, $\beta$ e os autovalores $\lambda$ e $\mu$.
lam = D[0]
mu = D[3]
Note que $\alpha$ é como chamamos o coeficiente de $x$ na expressão pós-primeira mudança de coordenadas. Essa expressão pra gente está guardada na variável z2
. Como vamos recuperá-la? Bom, uma possibilidade é derivar essa expressão em $x$ e depois fazer todas as outras variáveis iguais a zero (pense um pouco nessa estratégia).
x, y = sp.symbols('x y')
termolinear=sp.simplify((sp.Matrix([[d,e]]))*Q*(sp.Matrix([[x],[y]])))[0]
alfa=sp.diff(termolinear,x)
beta=sp.diff(termolinear,y)
Agora já podemos plotar o gráfico final:
# definindo quem é f-til
ftil=f-(alfa**2)/(4*lam)-beta**2/(4*mu)
# resetando as variáveis e escrevendo a equacao final
x, y = sp.symbols('x y')
print((lam*x**2 + mu*y**2 +ftil).evalf())
# plotando
import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
delta = 0.01
x = np.arange(-4.0, 4.0, delta)
y = np.arange(-4.0, 4.0, delta)
x, y = np.meshgrid(x, y)
z4 = 1.23606797749979*x**2 - 3.23606797749979*y**2 - 4.1875
fig = plt.figure()
ax = fig.add_subplot()
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.contour(x, y, z4,0)
1.23606797749979*x**2 - 3.23606797749979*y**2 - 4.1875
<matplotlib.contour.QuadContourSet at 0x7f79a84cd580>
Por fim, vamos plotar todas as curvas num único sistema de eixos, para você ver o que aconteceu. A curva em vermelho é a original, a curva em azul é após a mudança de coordenadas do tipo rotação, e a curva preta é após a segunda mudança de coordenadas, de translação.
# plotando
import numpy as np
import matplotlib.cm as cm
import matplotlib.pyplot as plt
delta = 0.01
x = np.arange(-4.0, 4.0, delta)
y = np.arange(-4.0, 4.0, delta)
x, y = np.meshgrid(x, y)
z = a*x**2+b*x*y+c*y**2+d*x+e*y+f
z3 = 1.23606797749979*x**2 + 1.43275483056186*x - 3.23606797749979*y**2 + 1.71674505838791*y - 4.0
z4 = 1.23606797749979*x**2 - 3.23606797749979*y**2 - 4.1875
fig = plt.figure()
ax = fig.add_subplot()
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.contour(x, y, z,0,colors=['red'])
plt.contour(x, y, z3,0,colors=['blue'])
plt.contour(x, y, z4,0,colors=['black'])
plt.show()
E a equação final é:
x, y = sp.symbols('x y')
sp.Eq(lam*x**2+mu*y**2+ftil,0)
Conhece aquela famosa piadinha? "Tudo tem limites, menos $1/x$ com $x\rightarrow 0$." Matemáticos tem um senso de humor bem peculiar.
O Python sabe muito bem trabalhar com limites, graças ao pacote SymPy. Não é nada recomendável calcular limites fazendo "tabelinhas" ou "aproximações", então o pacote de cálculo simbólico é o ideal.
Para calcular $$\lim_{x\rightarrow a} f(x)$$ o comando é sp.limit(f,x,a)
. A variável $x$ precisa anter ser definida.
import sympy as sp
x = sp.symbols('x')
sp.limit(x**2,x,2)
Bom, não vamos ficar usando o Python para calcular limites que sabemos calcular de cabeça, só substituindo os valores né? Vamos a alguns mais complicados. Por exemplo, que tal calcular $$\lim_{x\rightarrow 0} \dfrac{\sin(x)}{x}?$$
sp.limit(sp.sin(x)/x,x,0)
Isso significa que as funções $f(x)=\sin(x)$ e $g(x)=x$ são muito parecidas, para valores pequenos de $x$. Vamos fazer um gráfico para conferir isso:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-0.5, 0.5, 100)
y = np.sin(x)
plt.plot(x, y)
plt.plot(x, x)
[<matplotlib.lines.Line2D at 0x7f79dab1fdc0>]
E os limites que não existem? Também podem ser calculados!
import sympy as sp
x = sp.symbols('x')
sp.limit(1/(x-2),x,2)
Podemos calcular também limites com $x\rightarrow\infty$. No SymPy, o símbolo para infinito é oo
, lembrando do prefixo sp
. Sugestivo, não?
sp.limit(1/x,x,sp.oo)
Podemos ainda calcular limites laterais no Python, usando os símboos +
ou -
no comando.
sp.limit(1/x, x, 0, '+')
sp.limit(1/x, x, 0, '-')
Já vamos começar a falar sobre derivadas, mas podemos calculá-las pela definição, usando limites. Abaixo faremos um exemplo disso.
import sympy as sp
x, h = sp.symbols('x h', real=True)
f=x**2
fh=f.subs(x,x+h)
sp.limit( (fh-f)/h , h, 0)
O Python trabalha muito bem com funções definidas por partes. Abaixo fazemos um exemplo disso.
import sympy as sp
x = sp.symbols('x', real=True)
g=sp.Piecewise((x**2+1, x<1),(3*x, x>1))
sp.limit(g,x,1,'-')
sp.limit(g,x,1,'+')
Aviso importante: o Python não é muito bom com funções definidas por partes, tome cuidado com os resultados.
Se você já fez um curso de cálculo, deve ter percebido que derivação é um processo extremamente mecânico. Não é difícil implementar um "derivador formal". O Python calcula derivadas de funções de uma ou mais variáveis de forma muito eficiente. Novamente vamos usar o SymPy.
Sem mais delongas vamos calcular a derivada de $f(x)=x^2+x$ com respeito à função $x$.
import sympy as sp
x = sp.symbols('x')
f=x**2+x
sp.diff(f,x)
Foi rápido, né? A função pode ser muito mais complicada, como por exemplo $g(x)=e^{x^3+x}+1/x$.
g=sp.exp(x**3+x)+1/x
sp.diff(g,x)
As funções podem ser de mais que uma variável:
y = sp.symbols('y')
h=sp.cos(x*y)+(3*x+y)/(y**2+1)
# derivada em x
sp.diff(h,x)
# derivada em y
sp.diff(h,x)
# derivada mista, em x depois em y
sp.diff(h,x,y)
Claro que as derivadas de ordem superior também podem ser pedidas de forma iterada:
sp.diff(sp.diff(h,x),y)
Para derivadas de ordem superior, também é fácil. Abaixo calculamos a derivada de terceira ordem de $h(x,y)$ com respeito a $x$
sp.diff(h,x,x,x)
Se você quer a derivada em um ponto, pode usar o subs
para avaliar a derivada nesse ponto:
sp.diff(h,x).subs(x,0).subs(y,0)
Claro que a função precisa estar definida no ponto, ou coisas podem dar errado..
sp.diff(g,x).subs(x,0).subs(y,0)
Uma importante aplicação das derivadas é o cálculo da expansão em série de Taylor. O SymPy tem dois comandos para isso, o fps
e o series
. Vamos testá-los com a função $f(x)=x\cos(x)$.
import sympy as sp
x = sp.symbols('x')
f=x*sp.cos(x)
# calculando a expansao formal de f em serie de potencias
# com respeito aa variavel x, no ponto x=0, e truncando
# em ordem 10 usando o fps
fn=sp.fps(f,x,0).truncate(10)
display(fn)
# o comando series tambem funciona: a sintaxe é
# series(f(x),x,x0,k), onde x0
fm=sp.series(f,x,0,20).as_expr()
display(fm)
Se você não está ligando o nome à pessoa, uma aplicação interessante das séries de potências de uma função é poder calcular aproximações de valores de uma função $f(x)$ que não permite cálculos "diretos".
Por exemplo, não sabemos calcular diretamente $e^2$, mas usando a série da exponencial, podemos aproximar esse valor tão bem quanto quisermos. Vamos usar o sufixo removeO
para remover da série de Taylor a parte que tem os termos em $O(n)$.
import sympy as sp
x = sp.symbols('x')
g = sp.exp(x)
gk = sp.series(g,x,0,10).removeO()
gk.subs(x,2)
Portanto, $20947/2835$ é uma boa aproximação racional para $e^2$. Bom, o que estamos fazendo é uma coisa totalmente sem propósito, já que estamos fazendo isso no computador e um simples comando poderia calcular. Mas é aquele famoso "toy problem". Só por curiosidade, note que:
20947//2835
Portanto, $e^2$ está perto de 7. Como bônus, se você decorar a série abaixo, poderá tirar aquela onda no churrasco do fim de ano e calcular aproximações para $e^x$ para valores de $x$ próximos de $0$. Certamente, vai ser um sucesso.
sp.series(g,x,0,20).as_expr().removeO()
E chegamos nas integrais. O integrador do SymPy é muito rápido e eficiente, e é o companheiro que gostaríamos de ter na hora da prova de cálculo. Resolver uma integral no Python é muito fácil: o comando é integrate(f,x)
ou integrate(f,(x,a,b))
se for uma integral definida, com $x\in[a,b]$.
import sympy as sp
x = sp.symbols('x')
f=x**2
sp.integrate(f,x)
import sympy as sp
x = sp.symbols('x')
g=x*sp.exp(x)
sp.integrate(g,x)
import sympy as sp
x = sp.symbols('x')
h=sp.exp(x)*sp.cos(x)
sp.integrate(h,x)
Bom, como você percebeu, o SymPy teria perdido $0.1$ em cada integral anterior - esqueceu a constante de integração.. coisa feia, Python. Alguns exemplos de integrais definidas:
p=x**3+x
sp.integrate(p,(x,0,1))
# vamos tentar enganar o python?
q=1/(x-2)
sp.integrate(q,(x,0,3))
Garoto esperto..
O Python também consegue calcular integrais impróprias, lembrando que o símbolo oo
é usado para "infinito".
Atenção: se você tem menos que 18 anos, não execute o próximo comando!!
sp.integrate(sp.exp(-x**2), (x, 0, sp.oo))
Mais um exemplo de integral imprópria:
sp.integrate(1/(x**(6)), (x, 1,sp.oo))
A integração de funções de várias variávies, principalmente se o domínio for um retângulo, também pode ser feita sem problemas no Python:
x, y, a, b, c, d = sp.symbols("x y a b c d")
f = x*y
sp.integrate(f, (y, a, b), (x, c, d))
Também podemos integrar sobre regiões um pouco mais gerais, as chamadas regiões de tipo I/tipo II, ou regiões $R_x$ ou $R_y$:
x, y, a, b, c, d = sp.symbols("x y a b c d")
f = x**2+y
sp.integrate(f, (y, 0, x+1), (x, 0, 1))
Na primeira aula de integrais, começamos com integrais definidas, fazendo umas figuras sobre somas de Riemann. Acredite em mim: é complicado fazer isso manualmente, pois os cálculos são complicados e os desenhos são chatos de fazer. Que tal usar o Python pra facilitar nossa vida? Facilitará tanto o trabalho do professor quanto do aluno, que entenderá melhor.
A implementação abaixo foi adaptada do Mathematical Python (nesse site).
import numpy as np
import matplotlib.pyplot as plt
# originamente, a função foi definida com a definicao lambda,
# que de fato é mais simples nesse caso - caso queira saber
# mais sobre ela, leia aqui:
# https://stackabuse.com/lambda-functions-in-python/
# f = lambda x : x**2
# definindo a função
def f(x):
return x**2
# intervalo
a = 0; b = 10;
# numero de retangulos
N = 10
n = 10 # Use n*N+1 points to plot the function smoothly
# discretizando variáveis
x = np.linspace(a,b,N+1)
y = f(x)
X = np.linspace(a,b,n*N+1)
Y = f(X)
# iniciando plot
plt.figure(figsize=(15,5))
plt.subplot(1,3,1)
plt.plot(X,Y,'b')
x_left = x[:-1]
y_left = y[:-1]
plt.plot(x_left,y_left,'b.',markersize=10)
plt.bar(x_left,y_left,width=(b-a)/N,alpha=0.2,align='edge',edgecolor='b')
plt.title('Soma de Riemann com base na esquerda'.format(N))
plt.show()
Chegamos no capítulo que motivou essas notas: vamos usar o Pythonn para resolver equações diferenciais!
Começaremos resolvendo algumas equações usando o SymPy. Ele não é a melhor ferramenta para isso, pois só procura soluções "algébricas" (no sentido de poderem ser explicitadas), mas para começar a brincar, já funciona bem.
Primeiro vamos resolver uma equação simples: encontrar $f(x)$ tal que $$f'(x)+f(x)=0.$$ A notação para $f'(x)$ é um pouco estranha: para representar $f(t)$, devemos digitar f(t).diff(t)
. Se quiser a derivada de segunda ordem, $f''(t)$, entre f(t).diff(t,t)
e por aí vai.
Depois disso, é só usar o comando dsolve
.
import sympy as sp
# definindo a variavel independente
t = sp.symbols('t')
# definindo símbolos para as funcoes que estarão envolvidas
# nas equações diferenciais.
# note que precisamos já declarar esses símbolos como sendo
# da classe "função" com a adição cls=sp.Function
f = sp.symbols('f', cls=sp.Function)
# defina a equacao diferencial. para representar f'(t)
# deve-se usar f(t).diff(t).
# para indicar f''(t) usamos f(t).diff(t,t).
# a equacao sera na forma F=0. defina eq=F.
eq=f(t).diff(t) + f(t)
# resolvendo a equacao
sp.dsolve(eq, f(t))
Pronto! Está aí a solução da equação diferencial, já com a constante de integração. Vamos resolver agora uma equação de segunda ordem, $$g''(t)-2g'(t)+g(t)=0.$$
import sympy as sp
t = sp.symbols('t')
g = sp.symbols('g', cls=sp.Function)
eq=g(t).diff(t,t) -2*g(t).diff(t) +g(t)
sp.dsolve(eq, g(t))
Vamos piorar um pouco a equação diferencial e tentar resolver $$q''(t)-6q'(t)+2q(t)-t\cos(t)=0.$$
import sympy as sp
t = sp.symbols('t')
q = sp.symbols('q', cls=sp.Function)
eq=q(t).diff(t,t) -6*q(t).diff(t) +2*q(t)-t*sp.cos(t)
sp.dsolve(eq, q(t))
Para resolver adicionando condições iniciais, podemos complementar o comando. Vamos fazer isso, só para poder plotar um gráfico - adoramos gráficos. A condição inicial $q(0)=1$, $q'(0)=1$ é entrada como a opção ics={q(0): 1, q(t).diff(t).subs(t, 0): 1}
dentro do dsolve
.
import sympy as sp
t = sp.symbols('t')
q = sp.symbols('q', cls=sp.Function)
eq=q(t).diff(t,t) -6*q(t).diff(t) +2*q(t)-t*sp.cos(t)
# encontrando a solucao e armazenando no nome "solucao"
solucao=sp.dsolve(eq, q(t), ics={q(0): 1, q(t).diff(t).subs(t, 0): 1})
sp.plot(solucao.rhs,(t,-2,2),ylim=[-5,5])
<sympy.plotting.plot.Plot at 0x7f79b994c370>
Para resolver/plotar sistemas de equações diferenciais, até podemos usar o SymPy, mas é muito mais eficiente usar o NumPy (ou uma mistura dele com o SciPy). Parte do código abaixo foi inspirado desse site. Vamos usar como exemplo o que é, talvez, o sistema de equações diferenciais cujo retrato de fase ilustra mais livros em todo o mundo!
O sistema de Lorenz é definido como $$\left\{ \begin{array}{lcl} \dot x&=&\sigma (y-x),\\ \dot y&=& x(\rho-z)-y,\\ \dot z&=&xy-\beta z, \end{array} \right. $$ onde $\sigma,\rho,\beta$ são parâmetros. O sistema de Lorenz é uma simplificação de um modelo atmosférico. Os valores mais utilizados desses parâmetros, como feito pelo próprio Lorenz, são $\sigma=10$, $\beta=8/3$ e $\rho=28$.
O comando base para resolver sistemas de equações é o odeint
, mas ele tem alguns detalhes.
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
def lorenz(state, t, sigma, beta, rho):
x, y, z = state
dx = sigma * (y - x)
dy = x * (rho - z) - y
dz = x * y - beta * z
return [dx, dy, dz]
# valores dos parametros
sigma = 10.0
beta = 8.0 / 3.0
rho = 28.0
# parametros
p = (sigma, beta, rho)
# condicao inicial
ic = [1.0, 1.0, 1.0]
# tempo inicial, tempo final, tamanho da partição.
# se você colocar uma partição muito grande, então serão
# usados menos pontos para avaliar a função, e com isso
# terá a impressão de que são vários segmentos de reta.
# experimente um pouco com isso.
t = np.arange(0.0, 60.0, 0.001)
# integrando
result = odeint(lorenz, ic, t, p)
# plotando
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.plot(result[:, 0], result[:, 1], result[:, 2])
fig2 = plt.figure()
plt.plot(t,result[:, 0])
plt.plot(t,result[:, 1])
plt.plot(t,result[:, 2])
[<matplotlib.lines.Line2D at 0x7f79dab948e0>]
Soluções para sistemas bidimensionais também podem ser plotadas de forma muito similar. Como antes, vamos plotar a curva solução $(x(t),y(t))$ e depois, num mesmo gráfico, $x(t)$ e $y(t)$. Nosso eleito para o exemplo bidimensional é o oscilador de van der Pol.
A equação de segunda ordem $$x''-\mu(1-x^2)x'+x=0$$ é conhecida como oscilador de van der Pol, em homenagem ao matemático holandês Balthasar van der Pol (ok, ele era engenheiro e físico, mas acho que não ficaria triste em ser chamado de matemático) que a deduziu na década de 20, enquanto trabalhava na Philips com circuitos elétricos (veja detalhes aqui na Wikipedia). Essa equação de segunda ordem pode ser escrita como um sistema de equações de primeira ordem: $$\left\{ \begin{array}{lcl} \dot x&=&\mu(1-y^2)x-y,\\ \dot y&=& x.\\ \end{array} \right. $$
É fato conhecido que o oscilador de van der Pol admite um ciclo limite (uma órbita periódica atratora) para certos valores do parâmetro $\mu$. Vejamos como isso acontece ao fazer o gráfico: a solução começa em um certo ponto inicial e depois tende a uma curva fechada. Essa curva não pode ser expressada em termos algébricos, ela é uma curva transcendente. Vamos ao código:
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
# define o sistema de van der pol
def sistema(variaveis, t):
x, y = variaveis
dx = 3*(1-y**2)*x-y
dy = x
return [dx, dy]
# define a condicao inicial, onde
# ic=[ x(0), y(0)]
ic = [0.1,0.2]
# define o tempo - será usado pelo integrador e também
# pelo plot
t = np.arange(0, 40, 0.0001)
# integrando
result = odeint(sistema, ic, t)
# primeiro plot
fig = plt.figure()
ax = plt.axes()
# plotando a curva-solucao
ax.plot(result[:, 0], result[:, 1])
# agora plotando x(t) e y(t) separadamente num segundo plot
fig2=plt.figure()
plt.plot(t,result[:, 0])
plt.plot(t,result[:, 1])
[<matplotlib.lines.Line2D at 0x7f79db019c10>]
Note como no sistema de van der Pol, o plot das soluções $x(t),y(t)$ a partir de um certo $t$ parece o de uma função periódica, o que não acontece no sistema de Lorenz. Isso é justificado pela presença de um ciclo limite atrator no sistema de van der Pol, enquanto o sistema de Lorenz apresenta comportamento caótico.
Acima usamos o odeint
para encontrar a solução de forma numérica antes de plotá-la, e o plot era sempre com $t\in[0,t_0]$, com $t_0>0$. O motivo para isso é que o odeint
não funciona bem com tempos negativos. Uma possibilidade para resolver isso é usar o solve_ivp
. A página com manual dele pode ser vista nesse link, e é bem detalhada. Vamos resolver novamente o oscilador de van der Pol, agora com o solve_ivp
:
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
# note que agora que queremos usar o solve_ivp precisamos
# colocar o t como primeira variável, ao contrário de quando
# queríamos usar o odeint.
def sistema(t, variaveis):
x, y = variaveis
dx = 3*(1-y**2)*x-y
dy = x
return [dx, dy]
# condicao inicial
ic=[0.1,0.2]
# basicamente a sintaxe eh solve_ivp(sistema [t0,t1], ic). adicionamos
# a opcao dense_output para ficar mais facil recuperar as solucoes para
# que sejam plotadas.
solucao = solve_ivp(sistema, [-40, 40], ic,dense_output=True)
# agora discretizamos o tempo para plotar. é preciso ser compatível com
# o intervalo do tempo que foi passado para o comando solve_ivp
t = np.arange(-40, 40, 0.0001)
# a propriedade sol(t) da solucao carrega em suas colunas os valores
# de x(t) e y(t). para usar no plot, precisamos que isso esteja nas linhas,
# por isso usamos o .T no final, para calcular a transposta. isso vai plotar
# as solucoes x(t), y(t)
plt.plot(t,solucao.sol(t).T)
# agora criamos outra janela gráfica e plotamos a curva parametrizada.
# usamos [:,0] para acessar a primeira linha da matriz solucao.sol(t)
# e [:,1] para acessar a segunda linha. isso vai produzir um plot de
# curva parametrizada, como já sabemos fazer.
fig=plt.figure()
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
[<matplotlib.lines.Line2D at 0x7f79db1dcca0>]
Compare os gráficos e as curvas: são bem parecidas! (Claro!!)
Quando estamos estudando o comportamento de uma função $y=f(x)$, analisar o gráfico da função é um bom ponto de partida - e vimos como fazer gráficos no Python lá no começo desse tutorial.
No caso de equações diferenciais e PVIs, podemos encontrar e plotar a solução, seja na forma de curva parametrizada, seja em termos das coordenadas.
Já para sistemas de equações diferenciais, duas representações gráficas são muito eficientes:
Para o retrato de fase, já sabemos o que fazer: basta colocar, num mesmo sistema de eixos, várias soluções (ou seja, soluções passando por várias condições iniciais). Isso pode ser feito com um for
para pegar várias condições iniciais e ir plotando, como fazemos no comando abaixo. Confira esse site aqui para mais alguns exemplos.
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import odeint
from scipy.integrate import solve_ivp
fig = plt.figure(num=1)
ax=fig.add_subplot(111)
def sistema(t, variaveis):
x, y = variaveis
dx = (1-y**2)*x-y
dy = x
return [dx, dy]
# trajetórias em tempo positivo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,10], [P,Q],dense_output=True)
t = np.linspace(0, 10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# trajetórias em tempo negativo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,-10], [P,Q],dense_output=True)
t = np.linspace(0, -10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# limita a janela de visualização e mostra o plot
plt.xlim(-5,5)
plt.ylim(-5,5)
plt.show()
Vamos a mais dois exemplo, agora considerando um sistema de equações lineares: primeiro o sistema $$\left\{ \begin{array}{lcl} \dot x&=&-y,\\ \dot y&=&x, \end{array} \right. $$ que só tem soluções periódicas, e depois o sistema e depois o sistema $$\left\{ \begin{array}{lcl} \dot x&=&y,\\ \dot y&=&x, \end{array} \right. $$ que tem um ponto de sela.
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import odeint
from scipy.integrate import solve_ivp
fig = plt.figure(num=1)
ax=fig.add_subplot(111)
# um sistema que só tem órbitas periódicas
def sistema(t, variaveis):
x, y = variaveis
dx = -y
dy = x
return [dx, dy]
# trajetórias em tempo positivo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,10], [P,Q],dense_output=True)
t = np.linspace(0, 10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# trajetórias em tempo negativo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,-10], [P,Q],dense_output=True)
t = np.linspace(0, -10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# limita a janela de visualização e mostra o plot
plt.xlim(-5,5)
plt.ylim(-5,5)
plt.show()
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import odeint
fig = plt.figure(num=1)
ax=fig.add_subplot(111)
# um sistema que só tem órbitas periódicas
def sistema(t, variaveis):
x, y = variaveis
dx = y
dy = x
return [dx, dy]
# trajetórias em tempo positivo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,10], [P,Q],dense_output=True)
t = np.linspace(0, 10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# trajetórias em tempo negativo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,-10], [P,Q],dense_output=True)
t = np.linspace(0, -10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# limita a janela de visualização e mostra o plot
plt.xlim(-5,5)
plt.ylim(-5,5)
plt.show()
Por fim, um exemplo com MUITAS órbitas traçadas, e que possui tanto uma órbita homoclínica quanto algumas órbitas periódicas.
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import odeint
fig = plt.figure(num=1)
ax=fig.add_subplot(111)
# um sistema hamiltoniano com órbitas periódicas e uma órbita homoclínica
def sistema(t, variaveis):
x, y = variaveis
dx = -y
dy = -x-x**2
return [dx, dy]
# trajetórias em tempo positivo
for P in np.linspace(-2, 2, 20):
for Q in np.linspace(-2, 2, 20):
solucao = solve_ivp(sistema, [0,10], [P,Q],dense_output=True)
t = np.linspace(0, 10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# trajetórias em tempo negativo
for P in np.linspace(-2, 2, 20):
for Q in np.linspace(-2, 2, 20):
solucao = solve_ivp(sistema, [0,-10], [P,Q],dense_output=True)
t = np.linspace(0, -10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# limita a janela de visualização e mostra o plot
plt.xlim(-2,2)
plt.ylim(-2,2)
plt.show()
Agora que já sabemos plotar retratos de fase, vamos plotar campos de direções, e depois os dois juntos. Vamos usar como base a sela linear. O comando para plotar o campo vetorial é o quiver
, dentro do matplotlib. A sintaxe é bem simples.
import matplotlib.pyplot as plt
import numpy as np
# criamos uma malha em R2, com x e y indo de -5 a 5, contendo
# um total de 10 pontos em cada coordenada
x,y = np.meshgrid( np.linspace(-5,5,10),np.linspace(-5,5,10) )
# calculamos o campo vetorial X(x,y)=(y,x) na malha
u = y
v = x
#N = np.sqrt(u**2+v**2)
#U2, V2 = u/N, v/N
# plotando
plt.quiver( x,y,u, v)
<matplotlib.quiver.Quiver at 0x7f79ba40d880>
Em alguns casos, pode ser uma boa estratégia normalizar o campo vetorial para que a figura fique melhor. Isso obviamente deforma o campo vetorial, mas para visualização inicial, pode ser útil:
import matplotlib.pyplot as plt
import numpy as np
# criamos uma malha em R2, com x e y indo de -5 a 5, contendo
# um total de 10 pontos em cada coordenada
x,y = np.meshgrid( np.linspace(-5,5,10),np.linspace(-5,5,10) )
# calculamos o campo vetorial X(x,y)=(y,x) na malha
u = y
v = x
# normalizando o campo
N = np.sqrt(u**2+v**2)
U, V = u/N, v/N
# plotando
plt.quiver( x,y,U, V)
<matplotlib.quiver.Quiver at 0x7f79cb89b940>
Agora nosso gran finale: vamos plotar o campo de direções e o retrato de fase num só gráfico. Isso nos dá muita informação sobre o sistema dinâmico.
O código abaixo é o melhor que eu sei fazer, mas provavelmente deve existir uma forma mais eficiente: em particular, eu preciso definir separadamente tanto o sistema de equações diferenciais quanto o campo vetorial e isso não é a forma mais inteligente de fazer isso.
import matplotlib.pyplot as plt
import numpy as np
from scipy.integrate import odeint
fig = plt.figure(num=1)
ax=fig.add_subplot(111)
# um sistema que só tem órbitas periódicas
def sistema(t, variaveis):
x, y = variaveis
dx = y
dy = x
return [dx, dy]
# calculando e plotando trajetórias em tempo positivo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,10], [P,Q],dense_output=True)
t = np.linspace(0, 10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# calculando e plotando trajetórias em tempo negativo
for P in range(-5,5,1):
for Q in range(-5,5,1):
solucao = solve_ivp(sistema, [0,-10], [P,Q],dense_output=True)
t = np.linspace(0, -10, 500)
plt.plot(solucao.sol(t).T[:, 0],solucao.sol(t).T[:, 1])
# grid para os vetores
x,y = np.meshgrid( np.linspace(-5,5,20),np.linspace(-5,5,20) )
# calculando o campo vetorial X(x,y)=(y,x) na malha
u = y
v = x
# normalizando o campo (opcional)
N = np.sqrt(u**2+v**2)
u, v = u/N, v/N
# plotando o campo de direcoes
plt.quiver( x,y,u, v)
# limita a janela de visualização e mostra o plot
plt.xlim(-5,5)
plt.ylim(-5,5)
plt.show()
Se obter de forma explícita soluções para EDOs não é uma coisa simples, para EDPs se torna uma tarefa impossível. Então já partiremos para soluções numéricas desde o começo. Vamos estudar como obter a solução da equação do calor, e nosso objetivo será obter uma representação gráfica animada da evolução da equação (ou seja, como o "calor" se espalha numa região).
Os códigos abaixo foram adaptados dos códigos originais daqui e daqui.
Vamos começar com a equação do calor em dimensão 1. A ideia é simples: considere uma barra de ferro (ou outro material) que está fixada pelos dois extremos. Suponha que existe uma distribuição inicial de temperatura. Com o passar do tempo, como a temperatura evolui?
A solução do código abaixo é um pouco fake: nós já pegamos uma animação da solução com o passar do tempo e plotamos.
%matplotlib inline
%matplotlib tk
import numpy as np
from numpy import pi
import matplotlib.pyplot as plt
import matplotlib.animation as animation
fig = plt.figure()
fig.set_dpi(100)
ax1 = fig.add_subplot(1,1,1)
#Diffusion constant
k = 2
#Scaling factor (for visualisation purposes)
scale = 5
#Length of the rod (0,L) on the x axis
L = pi
#Initial contitions u(0,t) = u(L,t) = 0. Temperature at x=0 and x=L is fixed
x0 = np.linspace(0,L+1,10000)
t0 = 0
temp0 = 5 #Temperature of the rod at rest (before heating)
#Increment
dt = 0.01
# Solução da equação do calor
def u(x,t):
return temp0 + scale*np.exp(-k*t)*np.sin(x)
#Gradient of u
def grad_u(x,t):
#du/dx #du/dt
return scale*np.array([np.exp(-k*t)*np.cos(x),-k*np.exp(-k*t)*np.sin(x)])
a = []
t = []
for i in range(500):
value = u(x0,t0) + grad_u(x0,t0)[1]*dt
t.append(t0)
t0 = t0 + dt
a.append(value)
k = 0
def animate(i): #The plot shows the temperature evolving with time
global k #at each point x in the rod
x = a[k] #The ends of the rod are kept at temperature temp0
k += 1 #The rod is heated in one spot, then it cools down
ax1.clear()
plt.plot(x0,x,color='red',label='Temperatura em cada ponto x')
plt.plot(0,0,color='red',label='Tempo decorrido '+str(round(t[k],2)))
plt.grid(True)
plt.ylim([temp0-2,2.5*scale])
plt.xlim([0,L])
plt.title('Evolução da equação do calor')
plt.legend()
anim = animation.FuncAnimation(fig,animate,frames=360,interval=10)
plt.show()
É muito mais complicado resolver a equação do calor em umd domínio bidimensional. O código abaixo resolve numericamente a equação e plota o resultado numa animação.
%matplotlib inline
%matplotlib tk
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.animation import FuncAnimation
print("2D heat equation solver")
plate_length = 50
max_iter_time = 750
alpha = 2
delta_x = 1
delta_t = (delta_x ** 2)/(4 * alpha)
gamma = (alpha * delta_t) / (delta_x ** 2)
# Initialize solution: the grid of u(k, i, j)
u = np.empty((max_iter_time, plate_length, plate_length))
# Initial condition everywhere inside the grid
u_initial = 0
# Boundary conditions
u_top = 100.0
u_left = 0.0
u_bottom = 0.0
u_right = 0.0
# Set the initial condition
u.fill(u_initial)
# Set the boundary conditions
u[:, (plate_length-1):, :] = u_top
u[:, :, :1] = u_left
u[:, :1, 1:] = u_bottom
u[:, :, (plate_length-1):] = u_right
def calculate(u):
for k in range(0, max_iter_time-1, 1):
for i in range(1, plate_length-1, delta_x):
for j in range(1, plate_length-1, delta_x):
u[k + 1, i, j] = gamma * (u[k][i+1][j] + u[k][i-1][j] + u[k][i][j+1] + u[k][i][j-1] - 4*u[k][i][j]) + u[k][i][j]
return u
def plotheatmap(u_k, k):
plt.clf()
plt.title(f"Gráfico da temperatura no instante t = {k*delta_t:.3f} unit time")
plt.xlabel("x")
plt.ylabel("y")
# This is to plot u_k (u at time-step k)
plt.pcolormesh(u_k, cmap=plt.cm.jet, vmin=0, vmax=100)
plt.colorbar()
return plt
# Do the calculation here
u = calculate(u)
def animate(k):
plotheatmap(u[k], k)
anim = animation.FuncAnimation(plt.figure(), animate, interval=1, frames=max_iter_time, repeat=False)
2D heat equation solver