enDAQ Custom Analysis

Introduction

This notebook and accompanying webinar was developed and released by the enDAQ team. This is the fifth “chapter” of our series on Python for Mechanical Engineers: 1. Get Started with Python * Blog: Get Started with Python: Why and How Mechanical Engineers Should Make the Switch 2. Introduction to Numpy & Pandas for Data Analysis 3. Introduction to Plotly for Plotting Data 4. Introduction of the enDAQ Library - There are lots of examples in this! 5. More Examples! (Today’s Webinar) - Frequency Analysis (FFTs and PSDs) - Simple Shock Response Spectrums - Peak Analysis - Preview endaq.batch

To sign up for future webinars and watch previous ones, visit our webinars page.

Installation

Available on PyPi via: > pip install endaq

For the most recent features that are still under development, you can also use pip to install endaq directly from GitHub: > pip install git+https://github.com/MideTechnology/endaq-python.git@development

[ ]:
!pip install -q endaq
!pip install -q kaleido #this is for rendering images with plotly
exit() #needed in Colab because they pre-load some libraries, wouldn't be neccessary if running locally
     |████████████████████████████████| 71 kB 3.0 MB/s
     |████████████████████████████████| 11.3 MB 13.0 MB/s
     |████████████████████████████████| 62 kB 749 kB/s
     |████████████████████████████████| 38.1 MB 1.3 MB/s
     |████████████████████████████████| 93 kB 1.0 MB/s
     |████████████████████████████████| 83 kB 1.0 MB/s
     |████████████████████████████████| 25.3 MB 51.8 MB/s
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
google-colab 1.0.0 requires pandas~=1.1.0; python_version >= "3.0", but you have pandas 1.3.4 which is incompatible.
google-colab 1.0.0 requires requests~=2.23.0, but you have requests 2.26.0 which is incompatible.
datascience 0.10.6 requires folium==0.2.1, but you have folium 0.8.3 which is incompatible.
albumentations 0.1.12 requires imgaug<0.2.7,>=0.2.5, but you have imgaug 0.2.9 which is incompatible.
     |████████████████████████████████| 79.9 MB 1.2 MB/s

[ ]:
import endaq

endaq.plot.utilities.set_theme(theme='endaq')

import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio; pio.renderers.default = "iframe"
import pandas as pd
import numpy as np
import scipy

PSDs & FFTs

Simple Sine Wave

[ ]:
time = np.linspace(0,2,200,endpoint=False)

sine_waves = pd.DataFrame(index=time)
sine_waves['0.8g @ 2 Hz'] = 0.8*np.sin(2*np.pi*2 * time)
sine_waves['1g @ 3 Hz'] = np.sin(2*np.pi*3 * time)
sine_waves['0.6g @ 5 Hz'] = 0.6*np.sin(2*np.pi*5 * time)
sine_waves['0.5g @ 4 & 6 Hz'] = 0.5*np.sin(2*np.pi*4 * time) + 0.5*np.sin(2*np.pi*6 * time)
sine_waves['0.3g @ 7 Hz'] = 0.3*np.sin(2*np.pi*7 * time)

sine_waves
0.8g @ 2 Hz 1g @ 3 Hz 0.6g @ 5 Hz 0.5g @ 4 & 6 Hz 0.3g @ 7 Hz
0.00 0.000000 0.000000 0.000000 0.000000 0.000000
0.01 0.100267 0.187381 0.185410 0.308407 0.127734
0.02 0.198952 0.368125 0.352671 0.583150 0.231154
0.03 0.294500 0.535827 0.485410 0.794687 0.290575
0.04 0.385403 0.684547 0.570634 0.921177 0.294686
... ... ... ... ... ...
1.95 -0.470228 -0.809017 -0.600000 -0.951057 -0.242705
1.96 -0.385403 -0.684547 -0.570634 -0.921177 -0.294686
1.97 -0.294500 -0.535827 -0.485410 -0.794687 -0.290575
1.98 -0.198952 -0.368125 -0.352671 -0.583150 -0.231154
1.99 -0.100267 -0.187381 -0.185410 -0.308407 -0.127734

200 rows × 5 columns

[ ]:
fig = px.line(sine_waves)
fig.update_layout(
    title_text='Comparison of Fabricated Sine Waves',
    yaxis_title_text='Acceleration (g)',
    xaxis_title_text='Time (s)',
    legend_title_text=''
)

Now to compute the PSD on this using endaq.calc.psd.welch(), see docs

[ ]:
psd = endaq.calc.psd.welch(sine_waves, bin_width=0.5)
[ ]:
fig = px.line(psd)
fig.update_layout(
    title_text='Comparison of the PSD of Two Fabricated Sine Waves',
    yaxis_title_text='Acceleration (g^2/Hz)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    legend_title_text=''
)

PSDs are the more appropriate way to analyze vibration data for a number of reasons (see blog) but we typically see people want to see the FFT because it’s units are easier to intuitively understand.

In my experience the best way to do this is to compute a PSD using our function with a few modifications: - Use scaling as parseval which means it is g^2 instead of g^2/Hz - Use a boxcar window so in effect there is no windowing - Define a very fine bin width - Set the overlap between FFTs to 0 (not necessary if the bin width is small enough) - Scale from g^2 as RMS to g-peak via **2 * (2**0.5)

[ ]:
fft = endaq.calc.psd.welch(
    sine_waves,
    scaling='parseval',
    window='boxcar',
    noverlap=0,
    bin_width=0.1,
)
fft = fft**0.5 * (2**0.5) #scale from g^2 as RMS to g-peak

fig = px.line(fft)
fig.update_layout(
    title_text='Comparison of the FFT (from PSD) of Two Fabricated Sine Waves',
    yaxis_title_text='Acceleration (g)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    legend_title_text='',
    template='endaq_light'
)
/usr/local/lib/python3.7/dist-packages/scipy/signal/spectral.py:1966: UserWarning:

nperseg = 1000 is greater than input length  = 200, using nperseg = 200

Real Sine Wave

Let’s load a IDE file with get_doc docs.

[ ]:
doc = endaq.ide.get_doc('https://info.endaq.com/hubfs/100Hz_shake_cal.ide')
endaq.ide.get_channel_table(doc)
  channel name type units start end duration samples rate
0 8.0 X (100g) Acceleration g 00:00.0218 00:25.0532 00:25.0314 126575 5000.07 Hz
1 8.1 Y (100g) Acceleration g 00:00.0218 00:25.0532 00:25.0314 126575 5000.07 Hz
2 8.2 Z (100g) Acceleration g 00:00.0218 00:25.0532 00:25.0314 126575 5000.07 Hz
3 80.0 X (40g) Acceleration g 00:00.0218 00:25.0547 00:25.0328 100934 3985.00 Hz
4 80.1 Y (40g) Acceleration g 00:00.0218 00:25.0547 00:25.0328 100934 3985.00 Hz
5 80.2 Z (40g) Acceleration g 00:00.0218 00:25.0547 00:25.0328 100934 3985.00 Hz
6 36.0 Pressure/Temperature:00 Pressure Pa 00:00.0217 00:25.0702 00:25.0485 27 1.06 Hz
7 36.1 Pressure/Temperature:01 Temperature °C 00:00.0217 00:25.0702 00:25.0485 27 1.06 Hz
8 70.0 X Quaternion q 00:00.0284 00:24.0682 00:24.0397 2424 99.35 Hz
9 70.1 Y Quaternion q 00:00.0284 00:24.0682 00:24.0397 2424 99.35 Hz
10 70.2 Z Quaternion q 00:00.0284 00:24.0682 00:24.0397 2424 99.35 Hz
11 70.3 W Quaternion q 00:00.0284 00:24.0682 00:24.0397 2424 99.35 Hz
12 59.0 Control Pad Pressure Pressure Pa 00:00.0251 00:25.0399 00:25.0148 252 10.02 Hz
13 59.1 Control Pad Temperature Temperature °C 00:00.0251 00:25.0399 00:25.0148 252 10.02 Hz
14 59.2 Relative Humidity Relative Humidity RH 00:00.0251 00:25.0399 00:25.0148 252 10.02 Hz
15 76.0 Lux Light Ill 00:00.0000 00:25.0278 00:25.0278 100 3.96 Hz
16 76.1 UV Light Index 00:00.0000 00:25.0278 00:25.0278 100 3.96 Hz

Now get the actual data out of one channel with to_pandas() docs.

Then generate a plot with rolling_min_max_envelope() (docs) that will instead of plotting all data points make a shaded plot which will look identical to plotting all points but be more responsive and faster (not entirely necessary on this dataset, but becomes so with larger ones).

[ ]:
accel = endaq.ide.to_pandas(doc.channels[80], time_mode='seconds')
accel = accel-accel.median() #remove DC offset
endaq.plot.rolling_min_max_envelope(
    accel,
    plot_as_bars=True,
    desired_num_points=1000,
    opacity=0.7
)

Here we’ll plot the raw data around the peak event with around_peak() docs.

[ ]:
endaq.plot.around_peak(accel, num=500)
[ ]:
fft = endaq.calc.psd.welch(
    accel[4.5:9.5], #just do it in time of first dwell
    scaling='parseval',
    window='boxcar',
    noverlap=0,
    bin_width=0.5,
)
fft = fft**0.5 * (2**0.5) #scale from g^2 as RMS to g-peak

fig = px.line(fft[:500])
fig.update_layout(
    title_text='FFT (from PSD) of Real World 10.3g Sine Wave',
    yaxis_title_text='Acceleration (g)',
    xaxis_title_text='Frequency (Hz)',
    legend_title_text='',
)

Comparing FFT Options

FFT Option Overview

There are a lot of different ways to compute a FFT in Python. I’ve been using Welch’s method so far but I want to compare that to some other more “direct” methods. So let’s compare them! First let’s conveniently wrap our manipulation of Welch’s method PSD into a FFT inside a function.

[ ]:
def welch_fft(df, bin_width=0.5):
  fft = endaq.calc.psd.welch(df, bin_width=bin_width, scaling='parseval', window='boxcar', noverlap=0)
  return fft**0.5 * (2**0.5) #scale from g^2 as RMS to g-peak

Now let’s wrap something around Numpy’s FFT functions for a real discrete Fourier transform.

Notice here we actually have phase information! So we’ll return that too.

[ ]:
def numpy_fft(df):
  """
  Using Numpy's rfft functions compute a discrete Fourier Transform
  """
  freq = np.fft.rfftfreq(len(df), d=endaq.calc.utils.sample_spacing(df))
  df_fft = pd.DataFrame(
    np.fft.rfft(df.to_numpy(), axis=0),
    index=pd.Series(freq, name="frequency (Hz)"),
    columns=df.columns
  )

  df_mag = df_fft.apply(np.abs, raw=True) / len(df_fft)
  df_phase = df_fft.apply(np.angle, raw=True)

  return df_mag, df_phase

Now we will use the FFTW algorithm which is available in the pyFFTW library under a GPL license (which makes it potentially difficult for us to use because we use the more premissive MIT license).

First let’s download it.

[ ]:
!pip install -q pyfftw
     |████████████████████████████████| 2.6 MB 5.3 MB/s

Now let’s use it in a function which allows for a drop-in replacement to the Numpy code. This algorithm is generally regarded as the fatest for computing discrete Fourier transforms - so we’ll put it to the test!

[ ]:
import pyfftw

def fftw_fft(df):
  """
  Using the FFTW algorithm, compute a discrete Fourier Transform
  """
  freq = pyfftw.interfaces.numpy_fft.rfftfreq(len(df), d=endaq.calc.utils.sample_spacing(df))
  df_fft = pd.DataFrame(
    pyfftw.interfaces.numpy_fft.rfft(df.to_numpy(), axis=0),
    index=pd.Series(freq, name="frequency (Hz)"),
    columns=df.columns
  )

  df_mag = df_fft.apply(np.abs, raw=True) / len(df_fft)
  df_phase = df_fft.apply(np.angle, raw=True)

  return df_mag, df_phase

Now let’s see a FFT result with this library. Note though this will be relatively large to plot…

[ ]:
fft, phase = fftw_fft(accel[4.5:9.5])

fig = px.line(fft[80:120])
fig.update_layout(
    title_text='FFT using FFTW of Real World 10.3g Sine Wave',
    yaxis_title_text='Acceleration (g)',
    xaxis_title_text='Frequency (Hz)',
    legend_title_text='',
)

Remember one of the benefits of the DFT is to get phase. Although not particularly useful for this dataset let’s plot it, notice the shift for the Z axis at the drive frequency.

[ ]:
fig = px.line(phase[80:120])
fig.update_layout(
    title_text='Phase using FFTW of Real World 10.3g Sine Wave',
    yaxis_title_text='Phase Angle (radians)',
    xaxis_title_text='Frequency (Hz)',
    legend_title_text='',
)

FFT Option Comparison

Now let’s do the fun part to compare the three approaches! First let’s make a sine wave with a bit over 1M points.

[ ]:
time = np.linspace(0,200,2**20,endpoint=False)

sine_waves = pd.DataFrame(index=time)
sine_waves['10g @ 100 Hz'] = 10*np.sin(2*np.pi*100 * time)
sine_waves['8g @ 99 Hz'] = 8*np.sin(2*np.pi*99 * time)
sine_waves['6g @ 100.25 Hz'] = 6*np.sin(2*np.pi*100.25 * time)
[ ]:
fig = px.line(sine_waves[:0.01])
fig.update_layout(
    title_text='Comparison of Fabricated Sine Waves',
    yaxis_title_text='Acceleration (g)',
    xaxis_title_text='Time (s)',
    legend_title_text=''
)
[ ]:
from time import process_time
import timeit
pyfftw.forget_wisdom() #FFTW will be faster on subsequent runs

lengths = [2**14, 16411, 2**17, 131101, 2**20, 1000003]

ffts = pd.DataFrame()
times = pd.DataFrame()
def melt_fft(fft,name,L):
  fft =  fft.reset_index().melt(id_vars='frequency (Hz)')
  fft['Algo'] = name
  fft['Length'] = L
  return fft

def time_fft(func):
    return np.array(timeit.repeat(func, number=1, repeat=10)).mean()

def time_pyfftw(x, threads=1):
    pyfftw.forget_wisdom()
    rfft = pyfftw.builders.rfft(x, threads=threads, auto_align_input=True)
    rfft()
    return np.array(timeit.repeat(lambda: rfft(), number=1, repeat=10)).mean()

for L in lengths:
  x = sine_waves.iloc[:L, 0].to_numpy()

  t1 = process_time()
  wfft_bin = welch_fft(sine_waves.iloc[:L], bin_width=0.5)

  t2 = process_time()
  wfft = welch_fft(sine_waves.iloc[:L], bin_width=1/(L*endaq.calc.utils.sample_spacing(sine_waves)))

  t3 = process_time()
  nfft, phase = numpy_fft(sine_waves.iloc[:L])

  t4 = process_time()
  fftw, phase = fftw_fft(sine_waves.iloc[:L])

  t5 = process_time()

  times_t = pd.DataFrame(
      {'Welch w/ 0.5 Hz Bin':t2-t1,
       'Welch': t3-t2,
       'Numpy':t4-t3,
       'FFTW':t5-t4,
       'Numpy - No OH': time_fft(lambda: np.fft.rfft(x)),
       'FFTW - No OH': time_pyfftw(x, threads=1)
      },
      index=pd.Series([L],name='Length')
  )
  times = pd.concat([times,times_t.reset_index().melt(id_vars='Length')])

  wfft_bin = melt_fft(wfft_bin.copy(),'Welch w/ 0.5 Hz Bin',L)
  wfft = melt_fft(wfft.copy(),'Welch',L)
  nfft = melt_fft(nfft.copy(),'Numpy',L)
  fftw = melt_fft(fftw.copy(),'FFTW',L)

  ffts = pd.concat([ffts,wfft_bin,wfft,nfft,fftw])
[ ]:
times['str_len'] = times['Length'].astype('str')
fig = px.bar(
    times,
    x='str_len',
    y='value',
    color='variable',
    log_y=True,
    barmode='group',
    labels={'value':'Computation Time (s)','str_len':'Array Length','variable':''}
)
fig.show()
[ ]:
fig = px.scatter(
    times,
    x='str_len',
    y='value',
    color='variable',
    log_y=True,
    log_x=True,
    labels={'value':'Computation Time (s)','str_len':'Array Length','variable':''}
)
fig.show()

So what does this mean!? FFTW is the fastest as expected, but only if we first structure the data in a more efficient way. But typically you will not have the data structured in this “optimal” way for FFTW which means the time it takes to restucture it is real.

Long story short for this audience, using Welch’s method is fastest!

[ ]:
fig = px.line(
    ffts[(ffts['frequency (Hz)']>95) & (ffts['frequency (Hz)']<105)],
    x='frequency (Hz)',
    y='value',
    color='Algo',
    facet_col='Length',
    facet_row='variable',
    title='FFTs: Acceleration (g) vs Frequency (Hz)',
    labels={'value':'','frequency (Hz)':''}

)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
fig.update_layout(width=1200,height=1000)
fig.show()
[ ]:
fig.write_html('FFT-compare.html',include_plotlyjs='cdn')

When comparing the actual FFT results we notice that Welch’s with a fixed frequency bin width gives us identical results regardless of the time duration we use - and these are the “right” or at least expected result.

Random Vibration (NAVMAT PSD)

Let’s look at the NAVMAT PSD which is as follows: image0

[ ]:
navmat = pd.read_csv('https://info.endaq.com/hubfs/navmat-p-9492.csv',delimiter='\t',header=None,index_col=0,names=['Random'])
navmat.index.name='Time (s)'

navmat['Add 2g @ 100 Hz'] = navmat.Random + 2*np.sin(2*np.pi*100 * navmat.index)
navmat['Add 2g @ 200 Hz'] = navmat.Random + 2*np.sin(2*np.pi*200 * navmat.index)
navmat['Add 2g @ 400 Hz'] = navmat.Random + 2*np.sin(2*np.pi*400 * navmat.index)
navmat
Random Add 2g @ 100 Hz Add 2g @ 200 Hz Add 2g @ 400 Hz
Time (s)
0.000000 -8.698490e-07 -8.698490e-07 -8.698490e-07 -8.698490e-07
0.000038 8.459350e-07 4.793334e-02 9.583830e-02 1.914556e-01
0.000076 1.871620e-06 9.583932e-02 1.914566e-01 3.811528e-01
0.000114 3.444110e-06 1.436908e-01 2.866355e-01 5.673498e-01
0.000153 6.051080e-06 1.914608e-01 3.811570e-01 7.483369e-01
... ... ... ... ...
9.999847 3.435810e-02 -1.571110e-01 -3.468213e-01 -7.140265e-01
9.999886 3.562460e-02 -1.080109e-01 -2.509046e-01 -5.315224e-01
9.999924 1.458970e-02 -8.125498e-02 -1.768794e-01 -3.665897e-01
9.999962 1.604150e-02 -3.183180e-02 -7.967766e-02 -1.751775e-01
10.000000 -2.424250e-04 -2.424250e-04 -2.424250e-04 -2.424250e-04

262144 rows × 4 columns

[ ]:
endaq.plot.rolling_min_max_envelope(navmat, plot_as_bars=True, desired_num_points=500, opacity=0.8)
[ ]:
endaq.plot.around_peak(navmat, num=1000)

FFTs

[ ]:
fft = welch_fft(navmat, bin_width=1)

fig = px.line(fft[20:2000])
fig.update_layout(
    title_text='NAVMAT P-9492 FFT (1 Hz Bin Width)',
    yaxis_title_text='Acceleration (g)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    yaxis_type='log',
    legend_title_text=''
)

Now let’s demonstrate the trouble with FFTs by varying the length of time we’ll use and the bin width/resolution in the FFT.

[ ]:
bins = ['1/T', 1, 10]
times = [1.0, 10]

ffts = pd.DataFrame()
for bin in bins:
  for time in times:
    bin_t = bin
    if bin=='1/T':
      bin_t = 1/time
    fft = welch_fft(navmat[:time], bin_width=bin_t)
    fft = fft[20:2000].reset_index().melt(id_vars='frequency (Hz)')
    fft['Time'] = time
    fft['Bin Width'] = bin

    ffts = pd.concat([ffts,fft])

ffts
frequency (Hz) variable value Time Bin Width
0 20.000229 Random 0.288549 1.0 1/T
1 21.000240 Random 0.169284 1.0 1/T
2 22.000252 Random 0.099324 1.0 1/T
3 23.000263 Random 0.101153 1.0 1/T
4 24.000275 Random 0.100844 1.0 1/T
... ... ... ... ... ...
787 1950.319916 Add 2g @ 400 Hz 0.388329 10.0 10
788 1960.321557 Add 2g @ 400 Hz 0.369708 10.0 10
789 1970.323197 Add 2g @ 400 Hz 0.335365 10.0 10
790 1980.324838 Add 2g @ 400 Hz 0.378497 10.0 10
791 1990.326478 Add 2g @ 400 Hz 0.346982 10.0 10

104548 rows × 5 columns

[ ]:
fig = px.line(
    ffts,
    x='frequency (Hz)',
    y='value',
    color='variable',
    log_y=True,
    log_x=True,
    facet_row='Time',
    facet_col='Bin Width',
    title='FFTs: Acceleration (g) vs Frequency (Hz)',
    labels={'value':'',
            'frequency (Hz)':'',
            'variable':''}

)
fig.update_layout(width=800,height=600)
fig.show(renderer='svg')
../_images/webinars_Webinar_enDAQ_Custom_Analysis_51_0.svg

Our peaks stay consistent around the 2g we’d expect… but what is happening with the other frequency content!?

PSDs

[ ]:
psd = endaq.calc.psd.welch(navmat,bin_width=1)

fig = px.line(psd[20:2000])
fig.update_layout(
    title_text='NAVMAT P-9492 PSD (1 Hz Bin Width)',
    yaxis_title_text='Acceleration (g^2/Hz)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    yaxis_type='log',
    legend_title_text=''
)

Now let’s do the same comparison of PSDs

[ ]:
psds = pd.DataFrame()
for bin in bins:
  for time in times:
    bin_t = bin
    if bin=='1/T':
      bin_t = 1/time
    psd = endaq.calc.psd.welch(
        navmat[:time], bin_width=bin_t)
    psd = psd[20:2000].reset_index().melt(id_vars='frequency (Hz)')
    psd['Time'] = time
    psd['Bin Width'] = bin

    psds = pd.concat([psds,psd])
[ ]:
fig = px.line(
    psds,
    x='frequency (Hz)',
    y='value',
    color='variable',
    log_y=True,
    log_x=True,
    facet_row='Time',
    facet_col='Bin Width',
    title='PSDs: Acceleration (g^2/Hz) vs Frequency (Hz)',
    labels={'value':'',
            'frequency (Hz)':'',
            'variable':''}

)
fig.update_layout(width=800,height=600)
fig.show(renderer='svg')
../_images/webinars_Webinar_enDAQ_Custom_Analysis_57_0.svg

The peak at those sine tones decrease yet the bin width was wider and that sine tone was at one single frequency… so it is less dense. But then the other random/broadband levels are consistent regardless of our length of time or frequency resolution.

PSD Frequency Resolution and Octave Spacing

[ ]:
psd_coarse = endaq.calc.psd.welch(navmat,bin_width=10)

fig = px.line(psd_coarse[20:2000])
fig.update_layout(
    title_text='NAVMAT P-9492 PSD (10 Hz Bin Width)',
    yaxis_title_text='Acceleration (g^2/Hz)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    yaxis_type='log',
    legend_title_text=''
)

Convert to an octave spaced PSD with to_octave() docs.

[ ]:
psd = endaq.calc.psd.welch(navmat,bin_width=1)
oct_psd = endaq.calc.psd.to_octave(psd,fstart=20,octave_bins=3)

fig = px.line(oct_psd[:2000])
fig.update_layout(
    title_text='NAVMAT P-9492 PSD 1/3 Octave',
    yaxis_title_text='Acceleration (g^2/Hz)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    yaxis_type='log',
    legend_title_text='',
    template='endaq_light'
)
/usr/local/lib/python3.7/dist-packages/endaq/calc/psd.py:161: RuntimeWarning:

empty frequency bins in re-binned PSD; original PSD's frequency spacing is too coarse

Cumulative Sum from PSD

[ ]:
cum_rms = endaq.calc.psd.welch(navmat,bin_width=1, scaling='parseval').cumsum()**0.5

fig = px.line(cum_rms[10:2000])
fig.update_layout(
    title_text='NAVMAT P-9492 PSD with Added Sine Tone',
    yaxis_title_text='Cumulative Acceleration RMS (g)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    legend_title_text=''
)

Random Vibration Examples

Bearing

The first example was the topic of our blog on top 12 vibration metrics.

[ ]:
bearing = pd.read_csv('https://info.endaq.com/hubfs/Plots/bearing_data.csv', index_col=0)
[ ]:
endaq.plot.rolling_min_max_envelope(bearing, plot_as_bars=True, desired_num_points=500, opacity=0.8)
[ ]:
psd = endaq.calc.psd.welch(bearing,bin_width=10)

fig = px.line(psd)
fig.update_layout(
    title_text='Bearing Vibration',
    yaxis_title_text='Acceleration (g^2/Hz)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    yaxis_type='log',
    legend_title_text='',
    template='endaq_light'
)

Car Engine

During a morning commute (many years ago) I mounted an enDAQ sensor to the car’s engine.

[ ]:
engine = endaq.ide.to_pandas(endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Commute.ide').channels[8])
engine = endaq.calc.filters.butterworth(engine,low_cutoff=1)
[ ]:
endaq.plot.rolling_min_max_envelope(engine, plot_as_bars=True, desired_num_points=500, opacity=0.8)
[ ]:
psd = endaq.calc.psd.welch(engine,bin_width=1)

fig = px.line(psd)
fig.update_layout(
    title_text='VIbration of an Engine',
    yaxis_title_text='Acceleration (g^2/Hz)',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    yaxis_type='log',
    legend_title_text='',
    template='endaq_light'
)

Here we’ll use the octave_spectrogram() (see docs) to generate a spectrogram with log spaced frequency bins.

[ ]:
data, fig = endaq.plot.octave_spectrogram(engine[['Z']], window=2, bins_per_octave=24, freq_start= 20, max_freq=100)
fig.show()
/usr/local/lib/python3.7/dist-packages/endaq/calc/psd.py:161: RuntimeWarning:

empty frequency bins in re-binned PSD; original PSD's frequency spacing is too coarse

[ ]:
data = 10 ** (data/10)

fig = px.line(data[data.index<500].idxmax())
fig.update_layout(
    title_text="Moving Peak Frequency",
    xaxis_title_text="",
    yaxis_title_text="Peak Frequency (Hz)",
    showlegend=False
)
fig.show()

Quick Poll - What FFT Support Should We Add?

Remember this is an open source library you can view, comment and “react” to feature requests and bug reports. Here is an “issue” created to document this need to add some FFT support.

Simple Shock Response Spectrums

We’ll look at two datasets in our blog on pseudo velocity.

Punching Bag

[ ]:
punch = endaq.ide.to_pandas(endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Punching-Bag.ide').channels[8], time_mode='seconds')
punch = punch - punch.median()
[ ]:
endaq.plot.rolling_min_max_envelope(punch, plot_as_bars=True, desired_num_points=500, opacity=0.8)
[ ]:
fig = endaq.plot.around_peak(punch, num=1000, leading_ratio=0.2)
fig.update_layout(
    xaxis_title_text='',
    yaxis_title_text='Acceleration (g)',
    legend_title_text=''
)

First determine the frequency bins to calculate it at.

[ ]:
freqs = endaq.calc.utils.logfreqs(punch[29:30], bins_per_octave=12)

Now perform the shock response spectrum calculation for those frequencies.

[ ]:
srs_punch = endaq.calc.shock.shock_spectrum(punch[29:30], freqs=freqs, damp=0.05, mode='srs')

Now plot!

[ ]:
fig = px.line(srs_punch)
fig.update_layout(
    title_text='Shock Response Spectrum (SRS) of Punching Bag',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Peak Acceleration (g)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

Repeat for the PVSS.

[ ]:
pvss_punch = endaq.calc.shock.shock_spectrum(punch[29:30], freqs=freqs, damp=0.05, mode='pvss')
pvss_punch = pvss_punch*9.81*39.37 #convert to in/s
[ ]:
fig = px.line(pvss_punch)
fig.update_layout(
    title_text='Psuedo Velocity Shock Spectrum (PVSS) of Punching Bag',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Psuedo Velocity (in/s)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

MIL-S-901D Barge Shock

[ ]:
barge = pd.read_csv('https://info.endaq.com/hubfs/data/mil-s-901d-barge.csv', names=['Time (s)','Accel (g)']).set_index('Time (s)')
barge = barge - barge.median()
[ ]:
endaq.plot.rolling_min_max_envelope(barge, plot_as_bars=True, desired_num_points=500, opacity=0.8)
[ ]:
fig = endaq.plot.around_peak(barge, num=1000, leading_ratio=0.2)
fig.update_layout(
    xaxis_title_text='',
    yaxis_title_text='Acceleration (g)',
    showlegend=False
)

First determine the frequency bins to calculate it at. Here though we will specify an initial frequency of one lower than the duration.

[ ]:
freqs = endaq.calc.utils.logfreqs(barge, init_freq=1, bins_per_octave=12)
/usr/local/lib/python3.7/dist-packages/endaq/calc/utils.py:54: RuntimeWarning:

the data's duration is too short to accurately represent an initial frequency of 1.000 Hz

Now perform the shock response spectrum calculation for those frequencies.

[ ]:
srs_barge = endaq.calc.shock.shock_spectrum(barge, freqs=freqs, damp=0.05, mode='srs')

Now plot!

[ ]:
fig = px.line(srs_barge)
fig.update_layout(
    title_text='Shock Response Spectrum (SRS) of MIL-S-901 Barge',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Peak Acceleration (g)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

Repeat for the PVSS.

[ ]:
pvss_barge = endaq.calc.shock.shock_spectrum(barge, freqs=freqs, damp=0.05, mode='pvss')
pvss_barge = pvss_barge*9.81*39.37 #convert to in/s
[ ]:
fig = px.line(pvss_barge)
fig.update_layout(
    title_text='Psuedo Velocity Shock Spectrum (PVSS) of MIL-S-901 Barge',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Psuedo Velocity (in/s)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

Compare

[ ]:
srs_punch = srs_punch[['Z (100g)']].reset_index().melt(id_vars='frequency (Hz)')
srs_punch['variable'] = 'Punching Bag (80g Peak)'

srs_barge = srs_barge.reset_index().melt(id_vars='frequency (Hz)')
srs_barge['variable'] = 'MIL-S-901 (600g Peak)'

srs_combined = pd.concat([srs_punch, srs_barge])
[ ]:
fig = px.line(
    srs_combined,
    x='frequency (Hz)',
    y='value',
    color='variable'
)
fig.update_layout(
    title_text='Shock Response Spectrum (SRS) Comparison',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Peak Acceleration (g)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )
[ ]:
pvss_punch = pvss_punch[['Z (100g)']].reset_index().melt(id_vars='frequency (Hz)')
pvss_punch['variable'] = 'Punching Bag (80g Peak)'

pvss_barge = pvss_barge.reset_index().melt(id_vars='frequency (Hz)')
pvss_barge['variable'] = 'MIL-S-901 (600g Peak)'

pvss_combined = pd.concat([pvss_punch, pvss_barge])
[ ]:
fig = px.line(
    pvss_combined,
    x='frequency (Hz)',
    y='value',
    color='variable'
)
fig.update_layout(
    title_text='Psuedo Velocity Shock Spectrum (PVSS) Comparison',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Psuedo Velocity (in/s)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

Peak Finding

[ ]:
bumps = endaq.ide.to_pandas(endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Robotic-Bumps.ide').channels[80], time_mode='seconds')
bumps = bumps - bumps.median()
[ ]:
fig = endaq.plot.rolling_min_max_envelope(bumps, plot_as_bars=True, desired_num_points=500, opacity=0.8)
fig.show()

Now we’ll use SciPy’s find_peaks function to isolate the major events.

[ ]:
peak_times, _ = scipy.signal.find_peaks(
    x = bumps['Y (40g)'].abs(),
    distance = 5 / endaq.calc.utils.sample_spacing(bumps), #spaced 5 seconds apart
    height = 8
)

Let’s add them to the previous plot!

[ ]:
fig.add_trace(go.Scatter(
    x=bumps.iloc[peak_times].index,
    y=bumps.iloc[peak_times]['Y (40g)'],
    name='Peaks',
    mode='markers',
    marker_size=10,
    marker_color='#D72D2D'
))
fig.show()

Now let’s plot the peak event with the identified peak.

[ ]:
fig = endaq.plot.around_peak(bumps, num=1000, leading_ratio=0.2)
fig.update_layout(
    xaxis_title_text='',
    yaxis_title_text='Acceleration (g)',
    legend_title_text=''
)
fig.add_trace(go.Scatter(
    x=[bumps.iloc[peak_times[2]].name],
    y=[bumps.iloc[peak_times[2]]['Y (40g)']],
    name='Peak',
    mode='markers',
    marker_size=10,
    marker_color='#D72D2D'
))
fig.show()

PVSS of Full Time History

[ ]:
freqs = endaq.calc.utils.logfreqs(bumps, init_freq=1, bins_per_octave=12)
pvss_bumps = endaq.calc.shock.shock_spectrum(bumps, freqs=freqs, damp=0.05, mode='pvss')
pvss_bumps = pvss_bumps*9.81*39.37 #convert to in/s
[ ]:
fig = px.line(pvss_bumps)
fig.update_layout(
    title_text='Psuedo Velocity Shock Spectrum (PVSS) of Robotic "Bumps"',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Psuedo Velocity (in/s)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

You may be surprised to see the X axis actually had higher peak velocities than the Y axis because of where the frequency content lies!

Loop Through All Peaks

[ ]:
pvss_all = pd.DataFrame()

for peak in peak_times:
  freqs = endaq.calc.utils.logfreqs(bumps.iloc[peak-1000:peak+4000], init_freq=1, bins_per_octave=12)
  pvss = endaq.calc.shock.shock_spectrum(bumps.iloc[peak-1000:peak+4000], freqs=freqs, damp=0.05, mode='pvss')*9.81*39.37 #convert to in/s
  pvss = pvss.reset_index().melt(id_vars='frequency (Hz)')
  pvss['Peak'] = np.abs(bumps.iloc[peak]['Y (40g)'])
  pvss['Time'] = bumps.index[peak]

  pvss_all = pd.concat([pvss_all,pvss])
[ ]:
pvss_all['Peak'] = np.round(pvss_all['Peak'],1)
fig = px.line(
    pvss_all,
    x='frequency (Hz)',
    y='value',
    facet_col='variable',
    color='Peak',
    hover_data=['value','Time'],
    log_x=True,
    log_y=True,
    title='Comparison of PVSS for Each Event',
    labels={'value':'Peak Pseudo Velocity (in/s)','frequency (Hz)':''}
)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
fig.show()

Notice how the peak at 23.5g has a pseudo velocity about 1/2 that compared to the peak at virtually all the other events, including the one at 12.8g.

Preview of Batch

This is currently available, see docs, but we are working on a few bug fixes and improved functionality. This module allows you to batch process many IDE (only works for our sensors for now).

In a seperate document I first executed the following code to gather all the .IDE files I wanted to analyze (I hide the actual folder name). ~~~python import glob directory = r“C:\Users\shanly\enDAQ-Notebooks…”+“\”

files = glob.glob(directory+“.ide”) #get all the files in that directory with a .ide extension ~~~

Then with this list of files, I performed the batch operation with the following. ~[STRIKEOUT:python calc_output = ( endaq.batch.GetDataBuilder(accel_highpass_cutoff=1) .add_psd(freq_bin_width=1) .add_metrics() .aggregate_data(files) ) file_data = calc_output.dataframes]~

Then I saved the output dataframes of interest to pickles that I will load next! ~[STRIKEOUT:python file_data[‘psd’].to_pickle(‘batch_psd.pkl’, protocol=4) file_data[‘metrics’].to_pickle(‘batch_metrics.pkl’, protocol=4)]~

Note that I obscured the actual filenames (that would have contained the path) with these lines prior to saving. ~[STRIKEOUT:python file_data[‘metrics’].filename = file_data[‘metrics’].filename.str.split(‘\’).str[-1].str.split(‘.IDE’).str[0] file_data[‘psd’].filename = file_data[‘psd’].filename.str.split(‘\’).str[-1].str.split(‘.IDE’).str[0]]~

Metrics

[ ]:
metrics = pd.read_pickle('https://info.endaq.com/hubfs/data/batch_metrics.pkl')
metrics
filename calculation axis value serial number start time
0 DAQ11409_000061 RMS Acceleration X (40g) 0.050146 11409 2021-10-27 17:33:19.722259
1 DAQ11409_000061 RMS Velocity X (40g) 44.938216 11409 2021-10-27 17:33:19.722259
2 DAQ11409_000061 RMS Displacement X (40g) 5.836992 11409 2021-10-27 17:33:19.722259
3 DAQ11409_000061 Peak Absolute Acceleration X (40g) 0.906830 11409 2021-10-27 17:33:19.722259
4 DAQ11409_000061 Peak Pseudo Velocity Shock Spectrum X (40g) 0.002361 11409 2021-10-27 17:33:19.722259
... ... ... ... ... ... ...
148258 DAQ11409_005795 RMS Angular Velocity X 1.541428 11409 2021-11-16 19:52:53.617858
148259 DAQ11409_005795 RMS Angular Velocity Y 0.344340 11409 2021-11-16 19:52:53.617858
148260 DAQ11409_005795 RMS Angular Velocity Z 0.057123 11409 2021-11-16 19:52:53.617858
148261 DAQ11409_005795 Average Temperature Pressure/Temperature:00 15.580078 11409 2021-11-16 19:52:53.617858
148262 DAQ11409_005795 Average Temperature Control Pad Pressure 89.527103 11409 2021-11-16 19:52:53.617858

148263 rows × 6 columns

[ ]:
metrics.calculation.unique()
array(['RMS Acceleration', 'RMS Velocity', 'RMS Displacement',
       'Peak Absolute Acceleration',
       'Peak Pseudo Velocity Shock Spectrum', 'RMS Angular Velocity',
       'Average Temperature'], dtype=object)
[ ]:
fig = px.scatter(
    metrics[metrics.calculation.isin(['RMS Acceleration',
                                      'RMS Displacement',
                                      'Peak Absolute Acceleration'])],
    x='start time',
    y='value',
    facet_col='calculation',
    facet_col_wrap=1,
    color='axis',
    labels={'value':'',
            'start time':'',
            'axis':''
           },
    hover_data=['filename']
)
fig.update_layout(height=600)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
fig.update_yaxes(matches=None)
fig.show()

PSDs

[ ]:
psd = pd.read_pickle('https://info.endaq.com/hubfs/data/batch_psd.pkl')
psd
filename axis frequency (Hz) value serial number start time
0 DAQ11409_000061 X (40g) 0.000000 7.402487e-10 11409 2021-10-27 17:33:19.722259
1 DAQ11409_000061 Y (40g) 0.000000 2.419061e-08 11409 2021-10-27 17:33:19.722259
2 DAQ11409_000061 Z (40g) 0.000000 4.245836e-09 11409 2021-10-27 17:33:19.722259
3 DAQ11409_000061 Resultant 0.000000 2.917670e-08 11409 2021-10-27 17:33:19.722259
4 DAQ11409_000061 X (40g) 1.003505 4.808626e-09 11409 2021-10-27 17:33:19.722259
... ... ... ... ... ... ...
2876195 DAQ11409_005795 Resultant 124.448957 7.143454e-08 11409 2021-11-16 19:52:53.617858
2876196 DAQ11409_005795 X (40g) 125.452578 1.768300e-08 11409 2021-11-16 19:52:53.617858
2876197 DAQ11409_005795 Y (40g) 125.452578 3.348997e-09 11409 2021-11-16 19:52:53.617858
2876198 DAQ11409_005795 Z (40g) 125.452578 5.949278e-08 11409 2021-11-16 19:52:53.617858
2876199 DAQ11409_005795 Resultant 125.452578 8.052478e-08 11409 2021-11-16 19:52:53.617858

2876200 rows × 6 columns

Now we want to aggregate this see how things changed over time. So we’ll round the frequency values and focus on one axis. For readability we’ll also make a new column that will display time into the test in days.

[ ]:
psd['frequency (Hz)'] = np.round(psd['frequency (Hz)'] ,0)
psd['start time'] = pd.to_datetime(psd['start time'])
psd['Test Day'] = psd['start time']-psd['start time'].iloc[0]
psd['Test Day'] = np.round(psd['Test Day'].dt.total_seconds()/60/60/24,3)

psd_y = psd[psd.axis=='Y (40g)'].copy()
psd_y
filename axis frequency (Hz) value serial number start time Test Day
1 DAQ11409_000061 Y (40g) 0.0 2.419061e-08 11409 2021-10-27 17:33:19.722259 0.000
5 DAQ11409_000061 Y (40g) 1.0 2.557175e-07 11409 2021-10-27 17:33:19.722259 0.000
9 DAQ11409_000061 Y (40g) 2.0 2.522567e-06 11409 2021-10-27 17:33:19.722259 0.000
13 DAQ11409_000061 Y (40g) 3.0 1.247001e-06 11409 2021-10-27 17:33:19.722259 0.000
17 DAQ11409_000061 Y (40g) 4.0 7.596711e-08 11409 2021-10-27 17:33:19.722259 0.000
... ... ... ... ... ... ... ...
2876181 DAQ11409_005795 Y (40g) 121.0 1.037411e-08 11409 2021-11-16 19:52:53.617858 20.097
2876185 DAQ11409_005795 Y (40g) 122.0 6.082880e-09 11409 2021-11-16 19:52:53.617858 20.097
2876189 DAQ11409_005795 Y (40g) 123.0 5.461760e-09 11409 2021-11-16 19:52:53.617858 20.097
2876193 DAQ11409_005795 Y (40g) 124.0 4.885745e-09 11409 2021-11-16 19:52:53.617858 20.097
2876197 DAQ11409_005795 Y (40g) 125.0 3.348997e-09 11409 2021-11-16 19:52:53.617858 20.097

719050 rows × 7 columns

2D Waterfall

Let’s see how the PSD changes over time in a 2D plot with a bunch of lines (one per time). To help with the visualization we first need to get a bunch of colors to map to.

[ ]:
from plotly import colors
num_colors = len(psd_y[psd_y['Test Day']<1]['Test Day'].unique())
color_steps = colors.sample_colorscale(px.colors.sequential.Turbo, num_colors)

Now we can make the plot!

[ ]:
fig = px.line(
    psd_y[psd_y['Test Day']<1],
    x='frequency (Hz)',
    y='value',
    color='Test Day',
    hover_data=['filename','start time'],
    color_discrete_sequence = color_steps
)
fig.update_layout(
    xaxis_title_text='Frequency (Hz)',
    yaxis_title_text='Acceleration (g^2/Hz)',
    legend_title_text='',
    legend_y=-0.7,
    height=800,
    xaxis_type='log',
    yaxis_type='log',
    template='endaq_light'
)

3D Waterfall

I know everyone wants to see the 3D view…

[ ]:
fig = px.line_3d(
    psd_y[psd_y['Test Day']<1],
    x='frequency (Hz)',
    z='value',
    y='Test Day',
    color='Test Day',
    hover_data=['filename','start time'],
    color_discrete_sequence = color_steps,
    log_x=True,
    log_z=True,
    labels={'value':'Acceleration (g^2/Hz)','frequency (Hz)':'Frequency (Hz)'}
)
fig.update_layout(
    title_text='3D Waterfall Plot',
    showlegend = False,
    margin=dict(l=20, r=20, t=20, b=20),
    template='endaq_light'
)
[ ]:
fig.write_html('3d-waterfall.html',include_plotlyjs='cdn')

Animation

This let’s us create animations which I’ll first do on the first day’s worth of data.

[ ]:
fig = px.line(
    psd_y[psd_y['Test Day']<1],
    x='frequency (Hz)',
    y='value',
    animation_frame='Test Day',
    hover_data=['filename','start time']
)
fig.update_layout(
    xaxis_title_text='Frequency (Hz)',
    yaxis_title_text='Acceleration (g^2/Hz)',
    legend_title_text='',
    xaxis_type='log',
    yaxis_type='log',
)

That’s pretty cool! But we’ll notice that the animation moves outside the inital bounds pretty quickly. So let’s first find an easy way to calculate these metrics of max/min/median per frequency bin.

[ ]:
psd_pivot = psd_y.pivot(index='frequency (Hz)', columns='start time', values='value')
psd_pivot
start time 2021-10-27 17:33:19.722259 2021-10-27 17:38:21.101715 2021-10-27 17:43:22.482452 2021-10-27 17:48:23.870605 2021-10-27 17:53:25.254699 2021-10-27 17:58:26.638977 2021-10-27 18:03:28.009857 2021-10-27 18:08:29.389068 2021-10-27 18:13:30.775756 2021-10-27 18:18:32.146697 2021-10-27 18:23:33.514129 2021-10-27 18:28:34.893768 2021-10-27 18:33:36.268646 2021-10-27 18:38:37.657501 2021-10-27 18:43:39.036804 2021-10-27 18:48:40.431610 2021-10-27 18:53:41.812469 2021-10-27 18:58:43.190490 2021-10-27 19:03:44.574890 2021-10-27 19:08:45.954772 2021-10-27 19:13:47.342895 2021-10-27 19:18:48.719848 2021-10-27 19:23:50.096343 2021-10-27 19:28:51.476501 2021-10-27 19:33:52.856201 2021-10-27 19:38:54.239196 2021-10-27 19:43:55.622436 2021-10-27 19:48:57.017181 2021-10-27 19:53:58.401855 2021-10-27 19:58:59.786193 2021-10-27 20:04:01.182495 2021-10-27 20:09:02.573852 2021-10-27 20:14:03.964660 2021-10-27 20:19:05.560089 2021-10-27 20:24:06.949493 2021-10-27 20:29:08.341552 2021-10-27 20:34:09.737091 2021-10-27 20:39:11.129425 2021-10-27 20:44:12.713470 2021-10-27 20:49:14.100280 ... 2021-11-16 16:35:08.246185 2021-11-16 16:40:12.454956 2021-11-16 16:45:16.662902 2021-11-16 16:50:20.865539 2021-11-16 16:55:25.078521 2021-11-16 17:00:29.311737 2021-11-16 17:05:33.518707 2021-11-16 17:10:37.732543 2021-11-16 17:15:41.967681 2021-11-16 17:20:46.173339 2021-11-16 17:25:50.585174 2021-11-16 17:30:54.814361 2021-11-16 17:35:59.042205 2021-11-16 17:41:03.259735 2021-11-16 17:46:07.486053 2021-11-16 17:51:11.697174 2021-11-16 17:56:15.934448 2021-11-16 18:01:20.156829 2021-11-16 18:06:24.380767 2021-11-16 18:11:28.603668 2021-11-16 18:16:32.835784 2021-11-16 18:21:37.054992 2021-11-16 18:26:41.778625 2021-11-16 18:31:45.985595 2021-11-16 18:36:50.203430 2021-11-16 18:41:54.423553 2021-11-16 18:46:58.631286 2021-11-16 18:52:02.839935 2021-11-16 18:57:07.076232 2021-11-16 19:02:11.296142 2021-11-16 19:07:15.528411 2021-11-16 19:12:19.742004 2021-11-16 19:17:23.986328 2021-11-16 19:22:28.219085 2021-11-16 19:27:32.438873 2021-11-16 19:32:36.662109 2021-11-16 19:37:40.912109 2021-11-16 19:42:45.152252 2021-11-16 19:47:49.379699 2021-11-16 19:52:53.617858
frequency (Hz)
0.0 2.419061e-08 6.961451e-09 2.694621e-08 0.000004 0.000008 0.000003 0.000003 0.000003 0.000001 0.000002 0.000004 0.000005 0.000008 0.000003 0.000002 0.000003 0.000002 0.000002 0.000005 0.000002 0.000001 0.000003 9.199357e-07 0.000003 1.518771e-06 0.000002 0.000002 2.163578e-06 0.000008 0.000002 0.000004 3.760117e-06 8.331635e-07 0.000002 2.212025e-06 1.235489e-06 2.958984e-06 1.301514e-06 1.982634e-06 0.000005 ... 0.000003 3.022666e-06 4.218103e-06 0.000008 3.078788e-06 0.000002 2.032640e-06 7.642987e-06 2.100572e-06 3.379650e-06 2.492302e-06 9.384487e-06 2.791448e-06 2.543345e-06 0.000002 2.078568e-06 8.132484e-07 2.838021e-06 0.000004 4.413651e-06 0.000002 6.958460e-06 0.000005 2.259085e-06 2.582262e-06 8.285179e-06 0.000004 7.115846e-06 1.923488e-06 1.178528e-06 0.000005 1.850663e-07 5.758527e-08 4.356333e-08 1.326678e-08 3.523284e-08 3.401198e-08 9.321506e-08 2.072132e-07 5.329716e-08
1.0 2.557175e-07 8.999609e-08 1.926265e-07 0.000034 0.000055 0.000041 0.000037 0.000020 0.000022 0.000016 0.000045 0.000052 0.000049 0.000039 0.000026 0.000032 0.000011 0.000027 0.000051 0.000038 0.000007 0.000015 1.105532e-05 0.000036 1.671067e-05 0.000023 0.000026 1.953543e-05 0.000087 0.000031 0.000041 4.049195e-05 1.907499e-05 0.000032 2.385972e-05 1.026427e-05 2.749858e-05 1.615311e-05 2.693152e-05 0.000080 ... 0.000057 2.867848e-05 5.494686e-05 0.000059 1.791955e-05 0.000025 3.726021e-05 7.125737e-05 2.376242e-05 3.463165e-05 2.879028e-05 4.655712e-05 7.107180e-05 4.406466e-05 0.000065 4.128956e-05 1.860609e-05 4.077474e-05 0.000033 8.258510e-05 0.000014 7.395578e-05 0.000057 2.943697e-05 3.235613e-05 1.141953e-04 0.000103 5.089320e-05 3.296615e-05 2.210161e-05 0.000058 1.788990e-06 6.666895e-07 4.819046e-07 6.939725e-07 3.583268e-07 8.923644e-07 1.693514e-06 5.189828e-06 3.352921e-07
2.0 2.522567e-06 6.889837e-07 2.332143e-06 0.000208 0.000298 0.000202 0.000104 0.000118 0.000102 0.000077 0.000193 0.000266 0.000251 0.000146 0.000130 0.000145 0.000073 0.000150 0.000230 0.000155 0.000044 0.000097 4.346807e-05 0.000199 6.920640e-05 0.000099 0.000104 9.560500e-05 0.000549 0.000116 0.000247 3.122200e-04 1.225986e-04 0.000119 1.170403e-04 4.953305e-05 1.380503e-04 1.002347e-04 1.661748e-04 0.000339 ... 0.000234 1.086385e-04 3.531844e-04 0.000271 1.440112e-04 0.000148 2.247540e-04 3.794856e-04 1.091585e-04 1.625495e-04 2.139703e-04 2.784456e-04 3.102676e-04 2.197761e-04 0.000248 2.685322e-04 7.470985e-05 2.238129e-04 0.000208 5.037352e-04 0.000094 5.531540e-04 0.000472 1.668488e-04 1.696425e-04 5.673354e-04 0.000513 3.158666e-04 1.340000e-04 1.496420e-04 0.000383 1.414038e-05 4.116737e-06 3.061537e-06 3.137081e-06 2.238157e-06 3.732002e-06 8.260310e-06 2.382195e-05 3.364408e-06
3.0 1.247001e-06 3.708911e-07 1.238390e-06 0.000122 0.000155 0.000151 0.000053 0.000066 0.000059 0.000041 0.000116 0.000133 0.000148 0.000081 0.000059 0.000073 0.000037 0.000056 0.000124 0.000073 0.000026 0.000069 3.026884e-05 0.000104 4.267918e-05 0.000057 0.000043 6.355741e-05 0.000216 0.000054 0.000132 1.861895e-04 6.389375e-05 0.000079 7.598246e-05 4.548199e-05 6.775841e-05 6.932025e-05 8.357926e-05 0.000180 ... 0.000137 6.661549e-05 2.033306e-04 0.000178 1.262726e-04 0.000082 1.100094e-04 1.730692e-04 5.929123e-05 1.030920e-04 1.313220e-04 2.379084e-04 1.730239e-04 1.114687e-04 0.000104 1.285181e-04 3.582072e-05 1.165774e-04 0.000116 1.988762e-04 0.000067 2.372616e-04 0.000252 1.024006e-04 1.111846e-04 2.714036e-04 0.000222 2.389278e-04 9.763413e-05 8.946194e-05 0.000149 8.732022e-06 2.575371e-06 1.443267e-06 1.056115e-06 1.322230e-06 2.146535e-06 4.270818e-06 1.430278e-05 2.720061e-06
4.0 7.596711e-08 3.332906e-08 2.693014e-07 0.000026 0.000026 0.000032 0.000023 0.000020 0.000027 0.000012 0.000033 0.000023 0.000034 0.000020 0.000014 0.000018 0.000009 0.000019 0.000036 0.000024 0.000008 0.000022 9.036514e-06 0.000025 1.038904e-05 0.000017 0.000013 1.145624e-05 0.000039 0.000013 0.000038 3.526670e-05 1.801536e-05 0.000019 1.389114e-05 6.302247e-06 2.129335e-05 1.779139e-05 1.426981e-05 0.000043 ... 0.000059 1.420008e-05 2.916908e-05 0.000033 1.532318e-05 0.000016 1.920946e-05 5.402916e-05 1.619829e-05 4.107995e-05 2.308721e-05 6.647677e-05 5.163262e-05 2.292783e-05 0.000037 3.134771e-05 8.408034e-06 2.837890e-05 0.000028 4.000348e-05 0.000018 5.138282e-05 0.000051 2.366792e-05 2.534139e-05 7.182962e-05 0.000081 5.186834e-05 2.924370e-05 2.360597e-05 0.000056 1.866866e-06 1.077818e-06 6.921128e-07 2.991976e-07 1.100801e-07 4.083279e-07 3.597081e-07 1.828241e-06 4.625317e-07
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
122.0 4.794718e-08 1.403580e-09 1.555955e-09 0.000002 0.000013 0.000017 0.000002 0.000003 0.000003 0.000002 0.000002 0.000006 0.000004 0.000002 0.000004 0.000009 0.000021 0.000020 0.000019 0.000007 0.000008 0.000006 6.404928e-06 0.000012 1.515504e-05 0.000012 0.000003 1.431891e-05 0.000008 0.000004 0.000003 2.263224e-06 1.028560e-05 0.000002 4.241913e-06 4.873168e-06 1.225941e-06 3.019433e-06 1.951890e-06 0.000002 ... 0.000012 2.195231e-05 2.106803e-05 0.000012 1.830445e-05 0.000010 1.882487e-05 2.267166e-05 5.221777e-06 2.167978e-05 1.289279e-05 1.664051e-05 1.671418e-05 5.891856e-06 0.000015 1.482488e-05 1.265719e-05 1.252918e-05 0.000015 2.233010e-05 0.000008 1.701158e-05 0.000007 4.759324e-06 1.749558e-05 6.335562e-06 0.000006 2.069402e-05 1.063007e-05 7.831063e-06 0.000011 6.039101e-09 4.662815e-09 3.121494e-09 5.535294e-09 9.558629e-09 5.412999e-09 1.458804e-08 1.983171e-08 6.082880e-09
123.0 1.851752e-08 1.599072e-09 2.228003e-09 0.000002 0.000003 0.000003 0.000002 0.000002 0.000001 0.000001 0.000002 0.000002 0.000002 0.000001 0.000001 0.000003 0.000002 0.000003 0.000003 0.000001 0.000001 0.000003 2.084756e-06 0.000004 4.710479e-06 0.000002 0.000003 6.170386e-06 0.000003 0.000002 0.000003 2.033750e-06 2.567449e-06 0.000002 1.504151e-06 1.682195e-06 1.000708e-06 1.408385e-06 1.328178e-06 0.000002 ... 0.000003 4.974716e-06 4.804490e-06 0.000006 4.564517e-06 0.000003 4.870139e-06 8.099777e-06 2.389564e-06 4.807188e-06 2.969390e-06 4.548254e-06 5.288451e-06 2.222343e-06 0.000004 3.728426e-06 3.989563e-06 4.845033e-06 0.000005 6.873294e-06 0.000002 5.049589e-06 0.000005 2.686271e-06 5.196398e-06 4.537091e-06 0.000005 6.886131e-06 3.704323e-06 2.606997e-06 0.000003 5.848638e-09 2.652969e-09 3.802013e-09 3.680023e-09 7.588366e-09 3.352630e-09 1.334295e-08 1.356523e-08 5.461760e-09
124.0 1.526794e-08 1.904395e-09 3.012694e-09 0.000003 0.000002 0.000004 0.000004 0.000002 0.000001 0.000002 0.000005 0.000003 0.000002 0.000001 0.000001 0.000002 0.000001 0.000003 0.000005 0.000008 0.000001 0.000003 5.319471e-07 0.000002 7.423440e-07 0.000001 0.000002 1.331505e-06 0.000003 0.000001 0.000002 1.107081e-06 1.698859e-06 0.000001 7.515144e-07 5.368396e-07 7.445584e-07 8.629774e-07 1.665465e-06 0.000002 ... 0.000003 1.273982e-06 1.744378e-06 0.000003 2.006455e-06 0.000001 1.867671e-06 3.270818e-06 1.444862e-06 1.950836e-06 1.375152e-06 3.053077e-06 3.156415e-06 1.976571e-06 0.000003 1.392365e-06 9.572028e-07 1.487826e-06 0.000003 2.841553e-06 0.000002 2.921201e-06 0.000005 1.717473e-06 3.548734e-06 3.188935e-06 0.000002 3.984916e-06 1.677965e-06 2.226348e-06 0.000003 6.599701e-09 3.184131e-09 4.878153e-09 4.554283e-09 4.333788e-09 4.143943e-09 6.197127e-09 1.149195e-08 4.885745e-09
125.0 1.554448e-08 2.240702e-09 2.165436e-09 0.000003 0.000003 0.000004 0.000003 0.000002 0.000002 0.000003 NaN 0.000009 0.000005 0.000005 0.000002 0.000006 0.000002 0.000008 0.000019 0.000008 0.000003 0.000004 1.340032e-06 0.000005 1.828251e-06 0.000002 0.000002 1.030046e-06 0.000003 0.000003 0.000001 1.376330e-06 1.444470e-06 NaN 1.055394e-06 3.712440e-07 NaN 1.261323e-06 9.890634e-07 0.000003 ... 0.000002 1.289565e-06 1.452126e-06 0.000002 1.357979e-06 0.000001 1.266668e-06 2.451592e-06 3.529851e-07 1.686731e-06 7.846670e-07 2.801990e-06 2.365513e-06 8.005016e-07 0.000002 1.181413e-06 1.090871e-06 2.218299e-06 0.000003 2.916531e-06 0.000001 2.456790e-06 0.000001 2.894886e-07 1.862269e-06 9.910348e-07 NaN 2.436740e-06 9.076316e-07 7.792794e-07 0.000002 4.828801e-09 3.057908e-09 2.959562e-09 2.461398e-09 5.281825e-09 4.169273e-09 6.557761e-09 5.647898e-09 3.348997e-09
126.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 0.000004 1.284312e-06 0.000004 1.629573e-06 0.000002 0.000001 8.839285e-07 0.000002 0.000003 NaN 8.535060e-07 6.206068e-07 NaN 8.059833e-07 1.717251e-07 NaN 6.507064e-07 5.962763e-07 0.000002 ... NaN 3.593515e-07 4.812013e-07 0.000001 3.284124e-07 NaN 2.777564e-07 6.396569e-07 NaN 6.206701e-07 NaN 6.462563e-07 6.047255e-07 NaN NaN 3.570359e-07 3.670580e-07 6.375746e-07 0.000002 9.608136e-07 NaN 5.880341e-07 NaN NaN 4.747024e-07 NaN NaN 8.688151e-07 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

127 rows × 5703 columns

In this format computing the metric per frequency bin is pretty easy:

[ ]:
psd_pivot.max(axis=1)
frequency (Hz)
0.0      0.000018
1.0      0.000155
2.0      0.000933
3.0      0.000521
4.0      0.000139
           ...
122.0    0.000059
123.0    0.000016
124.0    0.000011
125.0    0.000019
126.0    0.000005
Length: 127, dtype: float64

Now we can loop through and add these lines to our animation and update it.

[ ]:
def add_line(df_stat,name,dash,color):
  fig.add_trace(go.Scatter(
    x=df_stat.index,
    y=df_stat.values,
    name=name,
    line_width=3,
    line_dash=dash,
    line_color=color
))

#Add max, min, median
for stat,dash,quant in zip(['Max','Min','Median'],
                           [None,None,'dash'],
                           [1.0,0.0,0.5]):
  df_stat = psd_pivot.quantile(quant, axis=1)
  add_line(df_stat,stat,dash,'#6914F0')
[ ]:
fig.update_layout(
    legend_y=-0.7
)
fig.show()

That’s a Wrap!

Hopefully you have enough to get started but if not remember we do offer services! And we will be working on more and more documentation and examples!

[ ]: