Pular para conteúdo

0. Modelos, numpy e matplotlib

Engenharia é, em boa parte, prever. Com que velocidade um paraquedista chega ao chão? Em quanto tempo uma vacina fora da geladeira passa do limite seguro? Quanto tempo um capacitor leva para carregar? Para prever, a ciência escreve um modelo matemático: uma equação que diz como uma grandeza muda. Às vezes essa equação tem solução exata no papel. Quase sempre, na vida real, não tem.

Este capítulo mostra as duas coisas lado a lado: um problema com fórmula exata e o mesmo problema resolvido um passo de cada vez, só com contas simples. Esse segundo jeito é o que o curso inteiro faz. No caminho, você conhece as duas ferramentas que vão aparecer em toda aula: numpy, para fazer contas com tabelas de números, e matplotlib, para desenhar gráficos.

O que é um modelo matemático

🌍 Contexto: o paraquedista e o arrasto do ar

Um paraquedista que salta de um avião cai cada vez mais depressa, mas não para sempre: o ar empurra para cima com uma força que cresce com a velocidade (o arrasto). Quando o arrasto fica igual ao peso, a velocidade para de aumentar. Essa é a velocidade terminal. O modelo diz que a aceleração é \(\dfrac{dv}{dt} = g - \dfrac{c}{m}v^2\): a gravidade menos o arrasto, que depende da massa \(m\) e de um coeficiente \(c\) que mede o quanto o corpo "segura" o ar.

Para esse modelo, o cálculo sabe achar a fórmula exata:

\[ v(t) = \sqrt{\frac{gm}{c}}\,\tanh\!\left(\sqrt{\frac{gc}{m}}\;t\right). \]

Com a fórmula, prever é só substituir valores. O código usa duas ferramentas novas: as funções matemáticas do numpy e a formatação de números no print.

🧰 Comando novo: import numpy as np, np.sqrt, np.exp e np.tanh

O Python sozinho não tem raiz quadrada nem exponencial prontas. A biblioteca numpy tem, e é a ferramenta de cálculo de todo o curso. A primeira linha do programa a carrega com o apelido np; daí em diante, tudo o que vem dela é chamado com np. na frente:

import numpy as np

print(np.sqrt(9))        # raiz quadrada
print(np.exp(1))         # e elevado a 1
print(np.tanh(0))        # tangente hiperbolica: comeca em 0...
print(np.tanh(3))        # ...e fica perto de 1 para valores grandes
3.0
2.718281828459045
0.0
0.9950547536867305

np.sqrt é a raiz quadrada, np.exp(x) é \(e^x\) e np.tanh é a tangente hiperbólica, uma curva em S que sai de 0 e se aproxima de 1. Você não precisa saber calcular a tanh à mão: ela aparece na fórmula do modelo, e o numpy faz a conta.

🧰 Python: formatando números no print

Dentro de uma f-string, o que vem depois dos dois-pontos diz como mostrar o número. O curso usa só estas formas:

x = 3.14159
print(f"{x:.2f}")        # 2 casas decimais
print(f"[{x:8.2f}]")     # 8 caracteres de largura, 2 casas (os colchetes mostram a largura)
print(f"[{7:5d}]")       # inteiro, 5 caracteres de largura
print(f"{0.0045:.1e}")   # notacao cientifica: 4.5 vezes 10 elevado a -3
3.14
[    3.14]
[    7]
4.5e-03

.2f quer dizer "duas casas decimais". O número antes do ponto (8.2f, 5d) é a largura: o número ocupa aquele tanto de espaço, o que alinha as colunas de uma tabela. d é para inteiros, e .1e é a notação científica (\(4{,}5 \times 10^{-3}\)).

# Modelo do paraquedista em queda livre (antes de abrir o paraquedas).
# A fisica da a formula exata da velocidade v(t), com arrasto do ar.
import numpy as np

g = 9.81         # gravidade (m/s²)
m = 68.1         # massa (kg)
c = 0.25         # coeficiente de arrasto (kg/m)


def v(t):
    return np.sqrt(g * m / c) * np.tanh(np.sqrt(g * c / m) * t)


print(" t (s)   v (m/s)")
for t in [0, 2, 4, 6, 8, 10, 12]:
    print(f"{t:5d}   {v(t):7.2f}")
print(f"velocidade terminal: {np.sqrt(g * m / c):.2f} m/s")
 t (s)   v (m/s)
    0      0.00
    2     18.73
    4     33.11
    6     42.08
    8     46.96
   10     49.42
   12     50.62
velocidade terminal: 51.69 m/s

Um modelo não é a realidade: ele ignora o vento, a posição do corpo e a densidade do ar mudando com a altitude. Ele é bom o bastante para a pergunta que se quer responder. Todo resultado deste curso vem com essa ressalva: o método numérico resolve o modelo, e o modelo aproxima o mundo.

Resolver sem fórmula: um passo de cada vez

A fórmula exata do paraquedista existe porque o modelo é simples. Mude um detalhe (o arrasto variando com a altitude, o paraquedas abrindo aos poucos) e ela some. O que não some é a equação da taxa, \(\dfrac{dv}{dt} = g - \dfrac{c}{m}v^2\): ela diz, a cada instante, quanto a velocidade está mudando. E isso basta.

🧑‍🏫 No quadro — o passo de Euler

1. A derivada é a taxa de variação. Para um intervalo de tempo \(\Delta t\) pequeno (o passo), a taxa é quase igual à inclinação da reta entre dois instantes:

\[ \frac{dv}{dt} \approx \frac{v_{\text{próximo}} - v_{\text{agora}}}{\Delta t}. \]

2. Isole o valor desconhecido, \(v_{\text{próximo}}\):

\[ v_{\text{próximo}} = v_{\text{agora}} + \frac{dv}{dt}\,\Delta t. \]

3. Em palavras: o próximo valor é o valor de agora mais a taxa vezes o passo. A taxa vem do modelo, \(g - \frac{c}{m}v_{\text{agora}}^2\), e só depende do que já se conhece.

4. Começando do valor inicial (\(v = 0\), parado), repita: cada passo usa o resultado do anterior. É o método de Euler.

A aproximação do passo 1 é a diferença progressiva do capítulo 2, lida ao contrário: lá, conhecemos os valores e queremos a taxa; aqui, conhecemos a taxa e queremos os valores.

# O mesmo paraquedista, SEM usar a formula de v(t): so a taxa de variacao,
# dv/dt = g - (c/m) v², e um passo de cada vez (metodo de Euler).
import numpy as np

g = 9.81
m = 68.1
c = 0.25
dt = 2.0                  # tamanho do passo (s)

t = 0.0
v = 0.0                   # comeca parado
print(" t (s)   v Euler   v exata")
for passo in range(7):
    exata = np.sqrt(g * m / c) * np.tanh(np.sqrt(g * c / m) * t)
    print(f"{t:5.0f}   {v:7.2f}   {exata:7.2f}")
    taxa = g - (c / m) * v**2     # quanto v muda por segundo, agora
    v = v + taxa * dt             # o proximo valor
    t = t + dt
 t (s)   v Euler   v exata
    0      0.00      0.00
    2     19.62     18.73
    4     36.41     33.11
    6     46.30     42.08
    8     50.18     46.96
   10     51.31     49.42
   12     51.60     50.62

Sem nenhuma fórmula de \(v(t)\), só com multiplicação e soma dentro de um laço, a coluna de Euler segue a exata. Ela erra, e sempre para cima: em cada passo a taxa usada é a do começo do intervalo, quando o arrasto ainda era menor. Mas as duas convergem para a mesma velocidade terminal.

💡 Pense assim: dirigir olhando só o velocímetro

Você sabe onde está e a que velocidade anda agora. Se mantiver essa velocidade por um minuto, sabe onde estará daqui a um minuto. Não é exato (a velocidade muda durante o minuto), mas, olhando o velocímetro de novo a cada minuto e corrigindo a conta, você chega perto. Com olhadas mais frequentes, chega mais perto ainda.

Quanto o passo importa

O erro de Euler vem de fingir que a taxa fica parada durante todo o passo. Com um passo menor, ela tem menos tempo para mudar:

# Quanto o tamanho do passo importa? Velocidade em t = 12 s com passos
# cada vez menores, comparada com a exata.
import numpy as np

g = 9.81
m = 68.1
c = 0.25
exata = np.sqrt(g * m / c) * np.tanh(np.sqrt(g * c / m) * 12)

print("  dt     passos   v(12)    erro")
for dt in [2.0, 1.0, 0.5, 0.25]:
    v = 0.0
    n = int(12 / dt)
    for passo in range(n):
        v = v + (g - (c / m) * v**2) * dt
    print(f"{dt:5.2f}   {n:5d}   {v:6.3f}   {abs(v - exata):.3f}")
  dt     passos   v(12)    erro
 2.00       6   51.601   0.983
 1.00      12   51.201   0.583
 0.50      24   50.926   0.308
 0.25      48   50.775   0.158

Com o passo pela metade, o erro cai mais ou menos pela metade, e o número de contas dobra. É o primeiro exemplo de uma troca que atravessa o curso inteiro: precisão custa trabalho, e cada método tem uma "taxa de câmbio" entre os dois. No capítulo 2 essa taxa de câmbio ganha um nome, a ordem do método.

numpy em poucos minutos

🌍 Contexto: por que numpy?

Uma lista do Python guarda qualquer coisa: números, textos, outras listas. Por isso ela não sabe fazer conta. O numpy tem a sua própria tabela de números, o array, que sabe. Ele é a ferramenta padrão de cálculo científico em Python, e o Colab já o traz instalado.

🧰 Comando novo: np.array

np.array(lista) transforma uma lista de números num array. A diferença aparece na primeira conta:

import numpy as np

lista = [1, 2, 3]
tabela = np.array([1, 2, 3])
print("lista * 2:", lista * 2)      # a lista do Python REPETE
print("array * 2:", tabela * 2)     # o array MULTIPLICA cada elemento
lista * 2: [1, 2, 3, 1, 2, 3]
array * 2: [2 4 6]

Com a lista, * 2 repete; com o array, multiplica cada elemento. Repare também que o array é impresso sem vírgulas: é assim que se reconhece um array na saída.

🧰 Comando novo: np.linspace

np.linspace(inicio, fim, quantos) cria um array com quantos números igualmente espaçados de inicio a fim, incluindo os dois. É o jeito de criar os instantes de tempo de uma tabela ou os pontos de um gráfico:

import numpy as np

t = np.linspace(0, 12, 7)    # 7 numeros igualmente espacados, de 0 a 12
print(t)
[ 0.  2.  4.  6.  8. 10. 12.]

Sete números de 0 a 12 dão um espaçamento de 2. O ponto depois de cada número (0., 2.) quer dizer que eles são float.

🧰 Conta com array: elemento a elemento

Toda conta entre um array e um número, e toda função do numpy, é feita em cada elemento, e o resultado é outro array do mesmo tamanho:

import numpy as np

t = np.linspace(0, 12, 7)
print(2 * t)             # cada elemento vezes 2
print(t + 1)             # cada elemento mais 1
print(np.sqrt(t))        # a raiz de cada elemento
[ 0.  4.  8. 12. 16. 20. 24.]
[ 1.  3.  5.  7.  9. 11. 13.]
[0.         1.41421356 2.         2.44948974 2.82842712 3.16227766
 3.46410162]

É por isso que a função v(t) do paraquedista, escrita pensando num número, funciona também com um array de tempos inteiro: np.sqrt e np.tanh trabalham elemento a elemento.

🧰 Índice e fatia no array

Funcionam igual à lista do Python:

import numpy as np

t = np.linspace(0, 12, 7)
print(t[0])              # o primeiro
print(t[-1])             # o ultimo
print(t[2:5])            # das posicoes 2, 3 e 4 (a 5 fica de fora)
print(len(t))            # quantos elementos
0.0
12.0
[4. 6. 8.]
7

A contagem começa em 0, t[-1] é o último e, na fatia t[2:5], a posição 5 fica de fora. len(t) diz quantos elementos há.

🧰 Comando novo: np.sum, np.max, np.argmax e np.round

Resumos de um array inteiro:

import numpy as np

medidas = np.array([2.5, 7.1, 3.8, 9.4, 6.0])
print(np.sum(medidas))         # soma de todos
print(np.max(medidas))         # o maior
print(np.argmax(medidas))      # a POSICAO do maior (a contagem comeca em 0)
print(np.round(3.14159, 2))    # arredonda com 2 casas
print(np.round(medidas / 3, 1))  # arredonda cada elemento do array
28.799999999999997
9.4
3
3.14
[0.8 2.4 1.3 3.1 2. ]

np.argmax devolve a posição do maior, e não o valor: o 9,4 está na posição 3 (contando do zero). np.round(x, n) arredonda com n casas, e funciona com o array inteiro. E a soma deu 28.799999999999997 em vez de 28,8. Não é defeito do numpy: é o assunto do capítulo 1.

Gráficos com matplotlib

Um número isolado esconde muita coisa; um gráfico mostra a forma. No curso, todo resultado importante vira gráfico, principalmente para comparar o modelo com as medições.

🧰 Comando novo: a receita de um gráfico com matplotlib

A biblioteca de gráficos é carregada com import matplotlib.pyplot as plt, e tudo dela é chamado com plt. na frente. Todo gráfico do curso segue a mesma receita:

Linha O que faz
plt.figure() começa uma figura nova, em branco
plt.plot(x, y, label="modelo") desenha y contra x como uma linha; label= dá o nome que aparece na legenda
plt.plot(x, y, "o") o mesmo, mas com bolinhas em cada ponto; "." desenha pontinhos e "o-", bolinhas ligadas por linha
plt.scatter(x, y, color="red") desenha pontos soltos, sem linha; color= escolhe a cor
plt.xlabel("tempo (min)"), plt.ylabel(...) o nome de cada eixo, com a unidade
plt.title("...") o título
plt.grid() a grade de fundo, que ajuda a ler os valores
plt.legend() a legenda, com os nomes dados em label=
plt.show() mostra a figura: é sempre o último comando

x e y podem ser listas ou arrays, mas precisam ter o mesmo tamanho. Para desenhar uma curva lisa, use muitos pontos, como np.linspace(0, 30, 100).

Um café esfriando: medições de um termômetro de cozinha (pontos) contra o modelo da lei de resfriamento de Newton (linha):

# Cafe esfriando: medicoes com um termometro de cozinha contra o modelo
# da lei de resfriamento de Newton, T(t) = 25 + 65*exp(-0.045 t).
import numpy as np
import matplotlib.pyplot as plt

t_medido = np.array([0, 5, 10, 15, 20, 25, 30])                # minutos
T_medido = np.array([90, 77.4, 66.8, 58.2, 51.4, 46.0, 41.6])  # °C

t = np.linspace(0, 30, 100)
T_modelo = 25 + 65 * np.exp(-0.045 * t)

plt.figure()
plt.plot(t, T_modelo, label="modelo de Newton")
plt.scatter(t_medido, T_medido, color="red", label="medições")
plt.xlabel("tempo (min)")
plt.ylabel("temperatura (°C)")
plt.title("Café esfriando numa sala a 25 °C")
plt.grid()
plt.legend()
plt.show()

O modelo de resfriamento de Newton como uma curva, e sete medições como pontos vermelhos sobre ela

O modelo passa perto de todas as medições: é assim que um gráfico mostra, de relance, se um modelo serve. Gráfico sem nome nos eixos e sem unidade é gráfico que ninguém consegue ler.

Mesmo método, outra área

🌍 Contexto: o capacitor carregando

Um capacitor é um "tanque" de carga elétrica. Ligado a uma fonte por um resistor, ele carrega depressa no começo e cada vez mais devagar, porque a corrente depende da diferença entre a tensão da fonte e a dele. A taxa é \(\dfrac{dV}{dt} = \dfrac{V_f - V}{RC}\). O produto \(RC\) (em segundos) é a constante de tempo: em \(RC\) segundos, o capacitor chega a 63 % da tensão final. Todo filtro de sinal, temporizador e circuito de reset usa isso.

O laço é o mesmo do paraquedista. Só muda a linha da taxa:

# Mesmo metodo, outra area: um capacitor carregando por um resistor.
# A tensao muda a uma taxa dV/dt = (Vf - V) / (R*C). E o mesmo laco do
# paraquedista, trocando so a linha da taxa.
import numpy as np
import matplotlib.pyplot as plt

Vf = 5.0          # tensao da fonte (V)
R = 10e3          # resistencia (ohm)
C = 100e-6        # capacitancia (F): R*C = 1 segundo
dt = 0.1

t = 0.0
V = 0.0
tempos = [t]
tensoes = [V]
for passo in range(50):
    taxa = (Vf - V) / (R * C)
    V = V + taxa * dt
    t = t + dt
    tempos.append(t)
    tensoes.append(V)

print(f"Euler: V(1 s) = {tensoes[10]:.3f} V   V(5 s) = {tensoes[50]:.3f} V")
print(f"exata: V(1 s) = {Vf * (1 - np.exp(-1)):.3f} V   V(5 s) = {Vf * (1 - np.exp(-5)):.3f} V")

t_exato = np.linspace(0, 5, 100)
plt.figure()
plt.plot(t_exato, Vf * (1 - np.exp(-t_exato)), label="exata")
plt.plot(tempos, tensoes, "o", label="Euler, dt = 0,1 s")
plt.xlabel("tempo (s)")
plt.ylabel("tensão no capacitor (V)")
plt.grid()
plt.legend()
plt.show()
Euler: V(1 s) = 3.257 V   V(5 s) = 4.974 V
exata: V(1 s) = 3.161 V   V(5 s) = 4.966 V

Curva exata de carga do capacitor e os pontos do método de Euler quase sobre ela

E não para aí. A taxa do capacitor, \(\frac{V_f - V}{RC}\), tem a mesma forma da taxa do café esfriando, \(k\,(T_{\text{sala}} - T)\): "a mudança é proporcional à distância até o equilíbrio". O mesmo código carrega o capacitor, esfria o café e esquenta uma vacina fora da geladeira (o problema desta aula). É essa a ideia do curso: o método não sabe de que área é o problema.

Erros comuns deste capítulo

Sintoma Causa provável
NameError: name 'np' is not defined faltou import numpy as np, ou a célula do import não foi rodada
2 * lista repete a lista em vez de dobrar os números é uma lista do Python: use np.array(lista)
o laço de Euler dá sempre o mesmo valor esqueceu de guardar o resultado: v + taxa * dt sem o v = na frente
Euler "explode" para números enormes passo grande demais para o problema: diminua o dt
o gráfico não aparece faltou plt.show() (ou ele ficou antes do plt.plot)
a curva sai "quebrada", em segmentos retos poucos pontos no np.linspace: use 100 ou mais para desenhar um modelo

Onde isso é usado de verdade

  • Previsão do tempo: os modelos meteorológicos avançam a atmosfera no tempo passo a passo, com métodos da mesma família de Euler (bem mais precisos), sobre milhões de pontos do globo.
  • Jogos e animação: a física de um jogo (pulos, colisões, carros) é atualizada a cada quadro, 60 vezes por segundo, com "posição nova = posição + velocidade × passo".
  • Simuladores de circuitos (SPICE, o do Tinkercad, o do Multisim): cada capacitor e indutor do circuito é avançado no tempo passo a passo.
  • Epidemiologia: os modelos de contágio usados na pandemia de COVID-19 avançam suscetíveis, infectados e recuperados dia a dia com essa mesma ideia.
  • Espaço: a trajetória de uma sonda é calculada integrando a gravidade de todos os corpos do sistema solar, passo a passo.

Resumo

  • Um modelo matemático descreve como uma grandeza muda. Às vezes tem fórmula exata; quase sempre, não.
  • O método de Euler resolve o modelo sem fórmula: próximo = agora + taxa × passo, repetido num laço. É a diferença progressiva usada ao contrário.
  • Passo menor, erro menor e mais contas: precisão custa trabalho.
  • numpy faz conta com a tabela inteira de uma vez; matplotlib mostra a forma do resultado.
  • O mesmo laço serve para paraquedista, café, vacina e capacitor. Só muda a taxa.

Para praticar

A Lista 00 começa com três passos de Euler à mão (✏️) e segue com o paraquedista, o café, a vacina fora da geladeira e o capacitor.