← Python · estudo medido

O sensor que enche a memória

Quanto dá pra encolher um sinal de vibração sem mudar o diagnóstico do motor — e o atalho que economiza igual, troca o defeito e não deixa rastro.

De onde veio a pergunta

O painel de vibração do Neon grava 199 amostras por segundo. Parece pouco até você deixar rodando: uma noite de máquina são 6,7 milhões de amostras — 9 horas e 21 minutos de gravação. Em float64, o padrão de quem só abre o arquivo e não pensa, isso são 51,1 MB na memória. Salvo em CSV, 127 MB em disco.

A pergunta que eu quis responder medindo, não achando: até onde dá pra encolher esse sinal sem mudar o que o diagnóstico diz? Porque economizar memória é fácil; o difícil é economizar sem trocar o defeito do motor.

Todo o código abaixo roda de ponta a ponta num notebook — no Google Colab ele roda sem instalar nada. O próprio notebook gera o sinal, então não depende de arquivo meu nenhum.

Em português claro, antes das contas

Se as palavras abaixo forem novas pra você, leia esta parte primeiro. O resto do estudo fica fácil depois dela.

Com isso na mão, o estudo inteiro cabe em três frases: trocar o formato do número é de graça e economiza 75%; jogar amostra fora é traiçoeiro, porque troca o defeito do motor e o número de conferência não acusa; e um minuto de gravação responde o mesmo que nove horas quando o que você quer é achar a frequência.

O campo de testes

Um sinal que imita o que o acelerômetro vê num motor de 3600 RPM (60 voltas por segundo, ou seja, pico em 60 Hz), com dois harmônicos, um defeito fraco de rolamento em 91 Hz — de propósito, bem no limite de aparecer — e o chiado do próprio sensor:

import numpy as np

fs, N = 199.0, 6_700_000          # 199 amostras por segundo, 9,35 horas
t = np.arange(N, dtype=np.float64) / fs
rng = np.random.default_rng(42)   # pra você medir os MESMOS números

x = (0.55*np.sin(2*np.pi*60.0*t)   # a rotação: 3600 RPM
   + 0.20*np.sin(2*np.pi*20.0*t)   # uma frequência baixa da estrutura
   + 0.08*np.sin(2*np.pi*80.0*t)   # um harmônico
   + 0.004*np.sin(2*np.pi*91.0*t)  # o defeito FRACO de rolamento
   + rng.normal(0, 0.02, N))       # o chiado do sensor

E a "máquina de diagnosticar", que é o que a gente compara em cada teste: o pico do espectro (em que frequência a máquina vibra mais), o RMS (o quanto ela vibra, que é o número que a ISO 10816 usa) e a altura do defeito fraco:

def diagnostica(sinal, fs):
    y = np.asarray(sinal, dtype=np.float64)
    esp = np.abs(np.fft.rfft(y - y.mean()))
    f = np.fft.rfftfreq(len(y), 1/fs)
    janela = (f > 90) & (f < 92)
    return f[esp.argmax()], np.sqrt(np.mean(y**2)), esp[janela].max()/len(y)*2

Teste 1 — trocar o formato do número

Mesmo sinal, guardado de quatro jeitos. O int16 é o formato cru, do jeito que o conversor do sensor entrega: a faixa de ±2 g repartida em 65.536 degraus.

formatomemóriapicoRMSdefeito de 91 Hz
float64 (padrão)51,12 MB60,000 Hz0,4182 g0,00393 g
float3225,56 MB60,000 Hz0,4182 g100%
float1612,78 MB60,000 Hz0,4182 g100%
int16 (cru, ±2 g)12,78 MB60,000 Hz0,4182 g100%
Achado 1: dá pra cortar 75% da memória — de 51,12 para 12,78 MB — sem mexer em nada do diagnóstico. Mesmo pico, mesmo RMS, e o defeito fraco preservado inteiro.

Por que o int16 não estraga: o degrau dele é 0,000061 g, quase cinquenta vezes menor que o ruído do próprio sensor parado, que no painel do Neon mede 0,00294 g. Guardar mais casas decimais do que o ruído do sensor é guardar chiado com precisão de laboratório.

Teste 2 — o atalho que parece esperto

Aqui mora o motivo deste estudo existir. A economia mais óbvia do mundo é pegar uma amostra a cada duas: metade do arquivo, metade da memória, e "o sinal continua lá".

pico, rms, defeito = diagnostica(x[::2], fs/2)   # 1 amostra a cada 2
o que eu fizmemóriapico medidopico realRMS
tudo51,12 MB60,000 Hz60 Hz0,4182 g
1 a cada 225,56 MB39,500 Hz60 Hz0,4182 g
1 a cada 317,04 MB6,333 Hz60 Hz0,4182 g
Achado 2 — o perigoso: jogar amostra fora economiza o mesmo que trocar o formato, mas troca o defeito do motor. O pico de 60 Hz vira 39,5 Hz, e um motor de 3600 RPM aparece como um problema de 2370 RPM que não existe. Pior: o RMS continua idêntico, 0,4182 g — o número que você olharia pra conferir não acusa nada.

O nome disso é aliasing, e a conta é simples: quando você fica com fs' = 99,5 amostras por segundo, tudo que passa de fs'/2 é rebatido pra baixo, em |f − fs'|. Daí 99,5 − 60 = 39,5 Hz. Com 1 a cada 3, 66,33 − 60 = 6,33 Hz.

Regra prática que sai daí: você só pode jogar amostra fora se a taxa que sobrar for mais que o dobro da maior frequência que te interessa. Pra enxergar 91 Hz, nada abaixo de 182 amostras por segundo — ou seja, com 199 não sobra folga nenhuma.

Teste 3 — de quanto tempo o diagnóstico precisa?

Se o problema é a memória, talvez o erro seja carregar a noite inteira. Medi o mesmo diagnóstico em pedaços cada vez maiores do sinal:

gravaçãoamostrasmemóriapicoRMSdefeito
10 segundos1.9900,02 MB60,000 Hz0,4185 g0,00447 g
1 minuto11.9400,09 MB60,000 Hz0,4181 g0,00421 g
10 minutos119.4000,91 MB60,000 Hz0,4182 g0,00403 g
1 hora716.4005,47 MB60,000 Hz0,4182 g0,00396 g
tudo (9,35 h)6.700.00051,12 MB60,000 Hz0,4182 g0,00393 g
Achado 3: o diagnóstico cabe em um minuto de gravação — 0,09 MB, contra 51 MB. É 0,2% da memória pro mesmo pico e o mesmo RMS. A gravação longa serve pra outra coisa: acompanhar a máquina ao longo do tempo, e não pra achar a frequência.

O que melhora com mais tempo é a resolução: com 10 segundos, cada raia do espectro vale 0,1 Hz; com 10 minutos, 0,002 Hz. Isso importa quando você precisa separar duas frequências quase coladas — não pra achar um pico que está sozinho.

Teste 4 — o que o pandas cobra, e o que você paga sem ver

Eu esperava encontrar um "imposto do pandas". Medi: um DataFrame com a mesma coluna float32 gastou 132 bytes a mais que o array puro do numpy. Nada.

O que pesa mesmo é o que você decide guardar. Guardar a coluna de tempo junto do sinal dobra a memória, de 25,56 para 51,12 MB — e essa coluna não precisa existir: ela se recria com uma conta, porque as amostras são igualmente espaçadas.

t = np.arange(len(sinal)) / fs   # o tempo se recria; não precisa ser guardado

E, para somar sem carregar tudo, o velho truque dos lotes: acumulei a soma dos quadrados em 67 lotes de 100 mil amostras. O RMS deu 0,418152 g, exatamente o mesmo do sinal inteiro — diferença de 0,00. A memória passa a depender do lote, não do arquivo.

E em disco

como salvei as mesmas 6,7 milhões de amostrastamanho
CSV (o jeito que o painel salva)127 MB
.npy em float6451,1 MB
.npy em int1612,8 MB

O CSV custa dez vezes o arquivo binário em int16 — porque ele guarda cada número como texto, com vírgula, sinal e quebra de linha. Pra guardar sinal de sensor, o CSV é o formato mais caro que existe.

O que eu levo deste estudo

Vale pra qualquer sinal de sensor: vibração, corrente, temperatura, pressão. Antes de pedir uma máquina maior na nuvem, é isto que eu faço — e, nos meus testes, o arquivo da noite inteira virou um problema de 0,09 MB.

Estudo do Método Neon · sinal gerado pelo próprio notebook, medições refeitas com numpy 2.5.3 e pandas 3.0.5 · o mesmo código roda no Google Colab, sem instalar nada · os números do sensor parado (0,00294 g) e da taxa de 199 amostras por segundo vêm do painel de vibração do Neon