← 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