Pular para conteúdo

17. Frequências e a FFT

No capítulo 16, os ciclos do consumo de energia eram conhecidos de antemão: um dia tem 24 horas, e uma semana, 168. Mas e quando não se sabe o período? Um motor vibra, e a vibração é uma mistura da rotação, de folgas e de um rolamento gasto. Uma corda de violão soa com a nota principal e vários harmônicos. Um sinal de rádio carrega várias estações ao mesmo tempo.

A pergunta passa a ser "de quais frequências este sinal é feito?". A ferramenta que responde é a transformada de Fourier, e o algoritmo rápido que a calcula, a FFT (Fast Fourier Transform), é possivelmente o método numérico mais usado do mundo: ele está no seu celular, no Wi-Fi, no MP3, no JPEG e no 5G.

Frequências escondidas

Um sinal simples, feito de dois senos: um de 5 Hz (5 voltas por segundo) com amplitude 1 e um de 12 Hz com amplitude 0,5. O computador guarda o sinal em amostras: 100 por segundo. Essa é a taxa de amostragem, \(f_s = 100\) Hz.

# Um sinal feito de dois senos: 5 Hz com amplitude 1 e 12 Hz com amplitude 0,5,
# amostrado 100 vezes por segundo durante 1 s. No grafico do tempo, da para ver
# que ha "coisa misturada", mas nao quais frequencias.
import numpy as np
import matplotlib.pyplot as plt

fs = 100                               # amostras por segundo
N = 100                                # 1 s de sinal
t = np.linspace(0, (N - 1) / fs, N)    # 0, 0.01, 0.02, ..., 0.99
sinal = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 12 * t)

plt.figure()
plt.plot(t, sinal, ".-")
plt.xlabel("tempo (s)")
plt.ylabel("sinal")
plt.grid()
plt.show()
print(f"{N} amostras, de {t[0]} s a {t[-1]:.2f} s")
100 amostras, de 0.0 s a 0.99 s

Um sinal que sobe e desce 5 vezes, com uma ondulação mais rápida por cima

Olhando o gráfico, dá para desconfiar de 5 ondas grandes, mas a de 12 Hz fica escondida nelas. Com ruído e com 5 ou 10 componentes, olhar o gráfico no tempo não adianta. É preciso um jeito de medir quanto de cada frequência existe no sinal.

No quadro: a transformada de Fourier

🧑‍🏫 No quadro — a transformada discreta de Fourier

1. Para saber se o sinal tem a frequência \(k\), multiplique-o, amostra por amostra, por um cosseno de frequência \(k\) e some: \(\sum_n x_n \cos(2\pi k n / N)\).

2. Se o sinal tem essa frequência, ele sobe quando o cosseno sobe e desce quando o cosseno desce: os produtos são quase todos positivos, e a soma é grande. Se não tem, os produtos ficam ora positivos, ora negativos, e a soma dá quase zero. É uma "sonda" para cada frequência.

3. Um problema: se a componente do sinal estiver atrasada um quarto de volta, ela é um seno, e a sonda de cosseno dá zero. A solução é usar duas sondas, um cosseno e um seno, e combinar as duas somas como os lados de um triângulo retângulo:

\[ C_k = \sum_{n=0}^{N-1} x_n \cos\!\left(\frac{2\pi k n}{N}\right), \qquad S_k = \sum_{n=0}^{N-1} x_n \sin\!\left(\frac{2\pi k n}{N}\right), \qquad A_k = \frac{2}{N}\sqrt{C_k^2 + S_k^2}. \]

4. O fator \(2/N\) faz \(A_k\) ser a amplitude do seno de frequência \(k\): um seno de amplitude 1 dá \(A_k = 1\).

5. Com \(N\) amostras em \(T\) segundos, as frequências testadas são \(0, \frac{1}{T}, \frac{2}{T}, \ldots\) até \(\frac{f_s}{2}\). O espaço entre elas, \(1/T\), é a resolução: para distinguir frequências mais próximas, é preciso gravar por mais tempo.

Em código, são dois laços: um para cada frequência \(k\), outro para cada amostra \(n\).

# A transformada discreta de Fourier com lacos: para cada frequencia k (em Hz,
# porque o sinal tem 1 s), multiplica o sinal por um cosseno e por um seno dessa
# frequencia e soma. Se o sinal "contem" a frequencia k, a soma e grande.
import numpy as np

fs = 100
N = 100
t = np.linspace(0, (N - 1) / fs, N)
sinal = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 12 * t)

amplitude = np.zeros(N // 2 + 1)
for k in range(N // 2 + 1):
    soma_cos = 0.0
    soma_sen = 0.0
    for n in range(N):
        soma_cos = soma_cos + sinal[n] * np.cos(2 * np.pi * k * n / N)
        soma_sen = soma_sen + sinal[n] * np.sin(2 * np.pi * k * n / N)
    amplitude[k] = 2 * np.sqrt(soma_cos ** 2 + soma_sen ** 2) / N

for k in range(N // 2 + 1):
    if amplitude[k] > 0.1:
        print(f"frequencia {k:2d} Hz: amplitude {amplitude[k]:.3f}")
print(f"maior amplitude acima de 12 Hz: {np.max(amplitude[13:]):.1e}")
frequencia  5 Hz: amplitude 1.000
frequencia 12 Hz: amplitude 0.500
maior amplitude acima de 12 Hz: 3.1e-15

As sondas acharam exatamente os dois senos, com as amplitudes certas, e zero (até o arredondamento, \(10^{-15}\)) em todas as outras frequências.

A FFT

Os dois laços fazem \(N \times N/2\) multiplicações. Com 1 segundo de áudio de CD (\(N = 44\,100\)), são quase um bilhão. Em 1965, Cooley e Tukey publicaram um jeito de reaproveitar as contas, dividindo o sinal ao meio repetidas vezes, que faz o mesmo com cerca de \(N \log_2 N\) operações: uns 700 mil. Essa é a FFT. Ela dá o mesmo resultado da transformada com laços, só que milhares de vezes mais depressa.

🧰 Comando novo: np.fft.rfft

np.fft.rfft(sinal) calcula as somas \(C_k\) e \(S_k\) para todas as frequências, de \(0\) até \(f_s/2\), com a FFT. Cada resultado é um número complexo, que guarda as duas somas juntas. Não é preciso saber mais do que isso sobre números complexos neste curso: o que interessa é o tamanho de cada um, que é o próximo comando.

espectro = np.fft.rfft(sinal)    # N // 2 + 1 resultados

🧰 Comando novo: np.abs

np.abs(a) dá o valor absoluto de cada elemento: np.abs(np.array([-2, 3])) dá [2 3]. Num resultado da FFT, dá o tamanho \(\sqrt{C_k^2 + S_k^2}\) de cada número, e então a amplitude é 2 * np.abs(espectro) / N.

🧰 Comando novo: np.fft.rfftfreq

np.fft.rfftfreq(N, 1 / fs) dá a frequência, em Hz, de cada posição do resultado da rfft: \(N\) é o número de amostras, e 1 / fs, o tempo entre elas. Com 4 amostras a 4 por segundo, np.fft.rfftfreq(4, 1 / 4) dá [0. 1. 2.].

# A mesma conta com a FFT do numpy: rfft faz as somas, rfftfreq diz a que
# frequencia corresponde cada posicao e abs tira o "tamanho" de cada resultado.
import numpy as np
import matplotlib.pyplot as plt

fs = 100
N = 100
t = np.linspace(0, (N - 1) / fs, N)
sinal = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 12 * t)

espectro = np.fft.rfft(sinal)
freq = np.fft.rfftfreq(N, 1 / fs)
amplitude = 2 * np.abs(espectro) / N

plt.figure()
plt.plot(freq, amplitude, "o-")
plt.xlabel("frequência (Hz)")
plt.ylabel("amplitude")
plt.grid()
plt.show()
print(f"{len(freq)} frequencias, de {freq[0]} a {freq[-1]} Hz, de {freq[1]} em {freq[1]} Hz")
print(f"amplitude em  5 Hz: {amplitude[5]:.3f}")
print(f"amplitude em 12 Hz: {amplitude[12]:.3f}")
51 frequencias, de 0.0 a 50.0 Hz, de 1.0 em 1.0 Hz
amplitude em  5 Hz: 1.000
amplitude em 12 Hz: 0.500

O espectro: zero em quase tudo, um pico de altura 1 em 5 Hz e um de altura 0,5 em 12 Hz

Esse gráfico, amplitude contra frequência, é o espectro do sinal. O mesmo sinal, visto do outro lado: no tempo, uma mistura; na frequência, dois picos limpos.

Um afinador

🌍 Contexto: afinar um violão

A quinta corda do violão, solta, deve soar na nota lá de 110 Hz. Os afinadores de celular gravam um pedaço do som, calculam a FFT e mostram se o pico mais forte está acima ou abaixo da nota certa. O arquivo nota_violao.csv tem meio segundo dessa corda, gravado a 8000 amostras por segundo, com o barulho de uma sala.

# Um afinador: a quinta corda do violao deveria soar em 110 Hz (a nota la).
# Meio segundo de gravacao, 8000 amostras por segundo, com muito ruido.
import numpy as np
import matplotlib.pyplot as plt

dados = np.loadtxt("nota_violao.csv", delimiter=",", skiprows=1)
t = dados[:, 0]
som = dados[:, 1]
fs = 8000
N = len(som)

amplitude = 2 * np.abs(np.fft.rfft(som)) / N
freq = np.fft.rfftfreq(N, 1 / fs)

plt.figure()
plt.plot(t[:400], som[:400])
plt.xlabel("tempo (s)")
plt.ylabel("amplitude")
plt.grid()
plt.show()

plt.figure()
plt.plot(freq[:400], amplitude[:400])
plt.xlabel("frequência (Hz)")
plt.ylabel("amplitude")
plt.grid()
plt.show()

k = np.argmax(amplitude)
print(f"resolucao: {freq[1]} Hz")
print(f"frequencia mais forte: {freq[k]} Hz")
if freq[k] < 110:
    print("a corda esta baixa: aperte a tarraxa")
else:
    print("a corda esta alta ou afinada")
resolucao: 2.0 Hz
frequencia mais forte: 108.0 Hz
a corda esta baixa: aperte a tarraxa

O som no tempo: uma onda irregular e ruidosa, sem período visível

O espectro do som: um pico forte perto de 108 Hz e picos menores perto de 217, 325 e 434 Hz

No tempo, o ruído esconde tudo. No espectro, a nota aparece com clareza, e também os harmônicos: picos em 2, 3 e 4 vezes a frequência principal, que dão ao violão o som de violão (um piano tocando a mesma nota tem os mesmos picos, com outras alturas).

O afinador diz 108 Hz: a corda está baixa. Repare na resolução: com meio segundo de som, as frequências testadas vão de 2 em 2 Hz (106, 108, 110...), e o afinador não distingue 108 de 108,5. Para ver o meio hertz, seria preciso gravar pelo menos 2 segundos. Os afinadores de verdade refinam o pico com uma interpolação entre as frequências vizinhas (o capítulo 10 de novo).

Amostrar rápido o bastante

# Aliasing: um seno de 12 Hz amostrado so 15 vezes por segundo. As amostras
# caem exatamente sobre um seno de 3 Hz, e a FFT nao tem como saber a diferenca.
import numpy as np
import matplotlib.pyplot as plt

f = 12
fs = 15
N = 15                                   # 1 s
t = np.linspace(0, (N - 1) / fs, N)
amostras = np.sin(2 * np.pi * f * t)

t_fino = np.linspace(0, 1, 1000)
plt.figure()
plt.plot(t_fino, np.sin(2 * np.pi * f * t_fino), label="seno de 12 Hz")
plt.plot(t_fino, -np.sin(2 * np.pi * 3 * t_fino), label="seno de 3 Hz")
plt.plot(t, amostras, "o", label="amostras (15 por segundo)")
plt.xlabel("tempo (s)")
plt.grid()
plt.legend()
plt.show()

amplitude = 2 * np.abs(np.fft.rfft(amostras)) / N
freq = np.fft.rfftfreq(N, 1 / fs)
k = np.argmax(amplitude)
print(f"a FFT diz: {freq[k]} Hz")
print(f"frequencia mais alta que 15 amostras/s enxergam: {fs / 2} Hz")
a FFT diz: 3.0 Hz
frequencia mais alta que 15 amostras/s enxergam: 7.5 Hz

Um seno rápido de 12 Hz e um lento de 3 Hz; as 15 amostras caem sobre os dois

💥 Quebre o método

Um seno de 12 Hz, amostrado 15 vezes por segundo, e a FFT diz, com toda a convicção, 3 Hz. Não é erro de conta: as 15 amostras caem exatamente sobre um seno de 3 Hz também, e nenhum método tem como saber qual dos dois foi gravado. É o aliasing (do inglês alias, "outro nome"): uma frequência alta se disfarça de frequência baixa.

A regra é o teorema da amostragem (de Nyquist e Shannon): só se enxergam frequências até a metade da taxa de amostragem, \(f_s/2\). Por isso o áudio de CD usa 44 100 amostras por segundo: o ouvido escuta até uns 20 kHz, e \(44\,100/2 = 22\,050\). É também por isso que as rodas de um carro, num vídeo, parecem girar devagar ou para trás.

Aumentar a taxa depois de gravar não resolve: a informação já se perdeu. Os conversores de verdade põem um filtro antes da amostragem, que corta tudo o que passa de \(f_s/2\).

Mesmo método, outra área

🌍 Contexto: os ciclos do consumo de energia, sem saber quais são

No capítulo 16, os ciclos do consumo de energia foram usados porque se sabia que um dia tem 24 horas. Com a FFT, a própria série diz quais são os seus ciclos. Com uma amostra por hora, as frequências saem em ciclos por hora, e o período é o inverso: \(1/f\).

# Mesmo metodo, outra area: os ciclos do consumo de energia (capitulo 16), sem
# saber de antemao quais sao. Uma amostra por hora: as frequencias saem em
# "ciclos por hora", e o periodo e 1 / frequencia.
import numpy as np
import matplotlib.pyplot as plt

dados = np.loadtxt("consumo_energia.csv", delimiter=",", skiprows=1)
consumo = dados[:, 1]
N = len(consumo)

variacao = consumo - np.mean(consumo)          # sem a media, que so daria um pico em 0
amplitude = 2 * np.abs(np.fft.rfft(variacao)) / N
freq = np.fft.rfftfreq(N, 1)                    # uma amostra por hora

plt.figure()
plt.plot(freq[:120], amplitude[:120], ".-")
plt.xlabel("frequência (ciclos por hora)")
plt.ylabel("amplitude (MW)")
plt.grid()
plt.show()

print("os tres picos mais fortes:")
copia = amplitude.copy()
for i in range(3):
    k = np.argmax(copia)
    print(f"  periodo {1 / freq[k]:6.1f} h   amplitude {amplitude[k]:.2f} MW")
    copia[k] = 0
os tres picos mais fortes:
  periodo   24.0 h   amplitude 5.72 MW
  periodo   12.0 h   amplitude 3.33 MW
  periodo  168.0 h   amplitude 2.84 MW

Espectro do consumo: picos em 1/168, 1/24 e 1/12 ciclo por hora

Os três picos mais fortes são o ciclo de 24 horas, o de 168 horas (a semana) e um de 12 horas. Este último não é um ciclo novo: o dia típico do consumo não é um seno perfeito (tem um vale de madrugada e um pico estreito à noite), e a forma dele precisa de um "harmônico" de 12 horas, como a corda do violão. Antes da FFT, a média foi tirada: ela é a frequência zero, e o pico dela esconderia os outros.

A mesma análise encontra o ciclo das manchas solares (11 anos), os ritmos do coração num eletrocardiograma e a sazonalidade de vendas numa loja.

Erros comuns deste capítulo

Sintoma Causa provável
um pico enorme na frequência 0 esconde o resto o sinal tem média diferente de zero: subtraia np.mean(sinal) antes da FFT
as frequências no eixo x estão erradas o segundo argumento de rfftfreq é o tempo entre amostras (1 / fs), e não fs
as amplitudes não batem com o sinal faltou o 2 * ... / N
o pico está numa frequência que o sinal não pode ter aliasing: a frequência real passa de \(f_s/2\)
não dá para separar duas frequências próximas a resolução é \(1/T\): grave por mais tempo
plt.plot reclama de números complexos faltou o np.abs

Onde isso é usado de verdade

  • Telecomunicações: o Wi-Fi, o 4G e o 5G (OFDM) enviam os dados em centenas de frequências ao mesmo tempo, montadas e separadas por uma FFT em cada símbolo.
  • Áudio e música: afinadores, equalizadores, reconhecimento de músicas (Shazam) e a compressão do MP3, que descarta as frequências que o ouvido não percebe.
  • Manutenção preditiva: sensores de vibração em motores, turbinas e bombas detectam defeitos pela frequência (é a aula 17).
  • Imagens: o JPEG usa uma prima da transformada de Fourier, em blocos de 8×8 pixels; a ressonância magnética monta a imagem com uma FFT em duas dimensões.
  • Ciências: os astrônomos acham planetas fora do sistema solar pelo ciclo do brilho da estrela; os sismólogos, a assinatura de um terremoto.

Resumo

  • Um sinal pode ser visto no tempo ou na frequência; o espectro mostra quanto de cada frequência ele tem.
  • A transformada discreta de Fourier mede cada frequência com uma "sonda": multiplica o sinal por um cosseno e por um seno dela e soma.
  • A FFT calcula o mesmo, com \(N \log_2 N\) operações em vez de \(N^2\): 2 * np.abs(np.fft.rfft(sinal)) / N dá as amplitudes, e np.fft.rfftfreq(N, 1 / fs), as frequências.
  • A resolução é \(1/T\): mais tempo de gravação separa frequências mais próximas.
  • Só se enxergam frequências até \(f_s/2\); acima disso, o aliasing as disfarça de frequências baixas.

Para praticar

A Lista 17 tem uma transformada à mão (✏️), a amplitude e a frequência dominante de um sinal, a resolução e o limite de Nyquist, e os ciclos de uma série.