Introduction to the enDAQ library

Introduction

This notebook and accompanying webinar was developed and released by the enDAQ team. This is the fourth “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 * Watch Recording of This 5. More Custom Examples

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

Why Did We Develop This?

When analyzing data you’ll need: - Customize a bit to meet your specific need - Share the results - Share the methodology - Reproduce the analysis for future/other data sets

Writing scripts that produce highly interactive and custom plots addresses all of these needs. And that’s why we created the open source enDAQ library - to make analysis more convenient, adaptable and reliable!

Docs

All of our functions are documented at www.docs.endaq.com image0

The code itself lives on github at: https://github.com/MideTechnology/endaq-python

Installation

Installing is as easy as pip install endaq that is needed once when running locally, but everytime in Google Colab.

[1]:
!pip install -q endaq
exit() #needed in Colab because they pre-load some libraries
WARNING: You are using pip version 21.1.2; however, version 22.0.3 is available.
You should consider upgrading via the 'c:\users\cflanigan\documents\endaq\endaq-python\endaq-python\venv\scripts\python.exe -m pip install --upgrade pip' command.
[ ]:
import endaq

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

Story 1: PSD of Large Time Series

Accessing Data

This first uses a function endaq.ide.get_doc() (see docs) to load in an IDE file. This can accept locations locally, or hosted online.

Please note that Python won’t accept backslashes and there are a number of ways around this (libraries!). I typically add an ‘r’ before the string of the folder like so r"C:\Users\shanly"+"\\" which ends up becoming: 'C:\\Users\\shanly\\'. For more see Python’s docs.

[ ]:
doc = endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Sys034_1.ide')

There are a lot of elements to this object that you can explore with doc.__dict__ but my favorite is to grab the serial number and part number.

[ ]:
print(doc.recorderInfo['PartNumber'])
print(doc.recorderInfo['RecorderSerial'])

Once the file is loaded, we are using the endaq.ide.get_channel_table() (see docs) which will present the contents of the file.

This file is modestly sized at 140 MB.

[ ]:
table = endaq.ide.get_channel_table(doc)
table

If you need to parse this table dataframe to sort it, find channels of interest, etc., you’ll need to grab the data like so.

[ ]:
table.data

Ok, now that we have IDE file, let’s get the data out! This is using the endaq.ide.to_pandas() (see docs) to pull out every channel into a dictionary with keys of the channel name.

[ ]:
data = {doc.channels[ch].name: endaq.ide.to_pandas(doc.channels[ch], time_mode='datetime') for ch in doc.channels}
data

Dashboard of an IDE File

We can pass this into a plot function to display a dashboard! Here are the docs, this accepts a dictionary of dataframes to display a bunch of sub plots for every channel/column.

[ ]:
fig = endaq.plot.dashboards.rolling_enveloped_dashboard(
    data,
    desired_num_points=100,
    min_points_to_plot=1,
    plot_as_bars=True,
)
fig.show()

New Plotting Theme

We’ve developed four Plotly themes (although really just two): 1. endaq 2. endaq_light 3. endaq_cloud (Open Sans Font) 4. endaq_cloud_light (Open Sans Font)

The set_theme() function creates the above four themes and makes one the default.

This uses a helper function, define_theme() which we recommend to those that may want to develop their own theme. Here are the docs.

[ ]:
endaq.plot.utilities.set_theme()

fig.update_layout(
    template='endaq'
)

Remember these are plotly figures which can be saved as interactive HTML files.

[ ]:
fig.write_html('dashboard.html',include_plotlyjs ='cdn')

This function allows a lot of customization with how the dashboard is displayed.

[ ]:
endaq.plot.dashboards.rolling_enveloped_dashboard(
    data,
    desired_num_points=100,
    min_points_to_plot=1,
    plot_as_bars=False,
    num_rows=1,
    num_cols=None
)

This dashboard can be used on any collection of dataframes, not just from an IDE file.

[ ]:
csv_data = {
    'crash':pd.read_csv('https://info.endaq.com/hubfs/data/Motorcycle-Car-Crash.csv',index_col=0),
    'moto':pd.read_csv('https://info.endaq.com/hubfs/data/motorcycle-vibration-moving-frequency.csv',index_col=0),
    'instrument':pd.read_csv('https://info.endaq.com/hubfs/data/surgical-instrument.csv',index_col=0),
    'rocket':pd.read_csv('https://info.endaq.com/hubfs/data/blushift.csv',index_col=0),
    'calibration':pd.read_csv('https://info.endaq.com/hubfs/data/Calibration-Shake.csv',index_col=0),
    'baseball':pd.read_csv('https://info.endaq.com/hubfs/data/baseball-throw-acceleration.csv',index_col=0, header=None, names=['X','Y','Z']),
    'volleyball':pd.read_csv('https://info.endaq.com/hubfs/data/volleyball-hit-acceleration.csv',index_col=0, header=None, names=['X','Y','Z']),
    'football':pd.read_csv('https://info.endaq.com/hubfs/data/football-catch-acceleration.csv',index_col=0, header=None, names=['X','Y','Z']),
}
[ ]:
endaq.plot.dashboards.rolling_enveloped_dashboard(
    csv_data,
    desired_num_points=100,
    min_points_to_plot=1,
    plot_as_bars=True,
)

We also have a related function to compute some rolling metrics, not just the envelope. Here I will plot the rolling peak and standard deviation (effectively the RMS). See the docs.

[ ]:
endaq.plot.dashboards.rolling_metric_dashboard(
    csv_data,
    desired_num_points=100,
    rolling_metrics_to_plot = ('absolute max', 'std')
)

No let’s go back to that big dataset and look at our dataframes within it.

[ ]:
data.keys()
[ ]:
data['100g PE Acceleration']

If you want to skip this step we just went through (load all data, present dashboard) and you know which channel is of interest, you can directly load in that data with endaq.ide.to_pandas() (see docs).

This defaults to present the time with datetime objects which is helpful for synchronization. You can pass in time_mode='seconds' to just get the time as seconds if preferred.

[ ]:
accel = endaq.ide.to_pandas(doc.channels[8])
accel

High Pass Filter

If you noticed, the DC offset on the piezoelectric accelerometer was a bit wonky which should be filtered away. Even if there isn’t an egregious DC offset like this example, it is still recommended to apply this filter when doing vibration analysis.

This uses our endaq.calc.filters.butterworth() (see docs) to filter intuitively.

[ ]:
filtered = endaq.calc.filters.butterworth(accel, low_cutoff=2)
filtered

Plotting Large Time Series

Now here’s a function similar to the dashboard above that was based off a dictionary of dataframes with each subchannel/column of data getting its own plot.

Here in rolling_min_max_envelope() (see docs) though we plot a single dataframe’s data on one plot. When using plot_as_bars this view will appear identical to loading ALL of the data and plotting it, yet this operation is completed in <10 seconds and will be highly responsive.

We are going to be making a lot of acceleration vs time plots, so I am going to simplify the labeling.

[ ]:
accel_time_labels = dict(
    xaxis_title_text='',
    yaxis_title_text='Acceleration (g)',
    legend_title_text=''
)
[ ]:
fig = endaq.plot.plots.rolling_min_max_envelope(
    filtered,
    desired_num_points=1000,
    plot_as_bars=True,
    opacity=0.7
    )
fig.update_layout(
    accel_time_labels,
    title_text='Filtered Time Series with 13M Points',
)

Linear PSD

Now we have the data, let’s generate a PSD on the whole thing with psd.welch()(see docs), and add in the resultant (PSDs are squared, so the resultant is simply the sum).

[ ]:
psd = endaq.calc.psd.welch(filtered, bin_width=1)
psd['Resultant'] = psd.sum(axis=1)
psd

Remember this is a pandas dataframe, saving to csv is easy with to_csv() (or other file type, see docs)

We’re going to make a lot of PSDs so let’s make the labeling easy.

[ ]:
psd_labels = dict(
    xaxis_title_text='Frequency (Hz)',
    yaxis_title_text='Acceleration (g^2/Hz)',
    legend_title_text='',
    xaxis_type='log',
    yaxis_type='log',
)

Now let’s plot it in Plotly!

[ ]:
fig = px.line(psd)
fig.update_layout(
    psd_labels,
    title_text='Power Spectral Density',
)

Log PSD

Once a linear PSD is computed, we have a function to convert it to octave spacing, see docs.

[ ]:
oct_psd = endaq.calc.psd.to_octave(psd, fstart=4, octave_bins=3)
oct_psd.head()

Using Plotly graph_objects, I’ll add these lines to the existing plot.

[ ]:
for c in oct_psd.columns:
  fig.add_trace(go.Scattergl(
      x=oct_psd.index,
      y=oct_psd[c],
      name=c+' Octave',
      line_width=6,
      line_dash='dash'
  ))

fig.show()

Spectrogram

Our spectrogram function (see docs) allows for octave spaced frequency bins which drastically reduces the heatmap resolution needed and is arguably a better way to represent the data anyways. This is a spectrogram generated off 13M points and completed in 3 seconds.

[ ]:
freqs, bins, Pxx, fig = endaq.plot.octave_spectrogram(filtered[['X (100g)']], window=12, bins_per_octave=6)
fig.show()

Extracting a Subsection of IDE File

With long files there may be subsections we’d like to pull out to save and share. This function does just that, (see docs).

[ ]:
endaq.ide.extract_time(doc,
                       out='extracted.ide',
                       start=pd.to_datetime('2021-10-22 09:17:15'),
                       end=pd.to_datetime('2021-10-22 09:17:25'))

Within Python and with dataframes, remember we can easily “slice” the data to focus on areas of interest. Here I’ll generate a PSD on a 10 second period of a fixed operating state.

[ ]:
psd = endaq.calc.psd.welch(filtered['2021-10-22 09:17:15':'2021-10-22 09:17:25'], bin_width=1)
psd['Resultant'] = psd.sum(axis=1)

fig = px.line(psd)
fig.update_layout(
    psd_labels,
    title_text='Power Spectral Density from 9:17:15 to 9:17:25',
)

I knew that to be a particularly interesting time in the file because of the light data this user utilized… clever!

[ ]:
fig = px.line(data['Light Sensor'][['Lux']][::4])
fig.update_layout(
    showlegend=False,
    xaxis_title_text='',
    yaxis_title_text="Light (Lux)"
)

Story 2: Multiple Files

We did a quick test with 3 devices on a shaker. image0

I know I only care about the secondary accelerometer, so I can load that channel (80) directly on these THREE files.

[ ]:
data = {
    'Shaker' : endaq.ide.to_pandas(endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Transfer_Shaker.ide').channels[80]),
    'Long Beam' : endaq.ide.to_pandas(endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Transfer_Long_Beam.ide').channels[80]),
    'Short Beam' : endaq.ide.to_pandas(endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Transfer_Short_Beam.ide').channels[80])
}
data

Dashboard

With these three files, let’s generate that dashboard again and compare them all in one figure (with shared x axes).

[ ]:
fig = endaq.plot.dashboards.rolling_enveloped_dashboard(
    data,
    desired_num_points=500,
    min_points_to_plot=1,
    plot_as_bars=True,
)
fig.update_xaxes(matches='x')
fig.show()

Single Plot Comparison

We may want to compare them all in one plot, here I’ll combine just the Z axis from each file.

[ ]:
time_overall = pd.DataFrame()
for device in data.keys():
  #Get Y axis and filter
  accel = endaq.calc.filters.butterworth(data[device]['Z (40g)'].to_frame(), low_cutoff=2)

  #Rename the column as our test/device name
  accel.columns = [device]

  #Combine Times
  time_overall = pd.concat([time_overall,accel])

time_overall

There is still 40,000 data points, so I’ll use the rolling_envelope plot to simplify the plot.

[ ]:
fig = endaq.plot.plots.rolling_min_max_envelope(
    time_overall,
    desired_num_points=1000,
    plot_as_bars=True,
    opacity=0.7
    )
fig.update_layout(
    accel_time_labels,
    title_text='Comparison of Acceleration in Time Domain',
)
fig.show()

PSD Comparison

Now let’s compute a PSD for all of these, and combine into one dataframe by rounding to a shared frequency bin.

[ ]:
psd_overall = pd.DataFrame()
for device in data.keys():
  #Get Z axis and filter
  accel = endaq.calc.filters.butterworth(data[device]['Z (40g)'].to_frame(), low_cutoff=2)

  #Get PSD
  psd = endaq.calc.psd.welch(accel, bin_width = 0.5)

  #Round to the nearest 0.5 Hz
  psd.index = np.round(psd.index*2,0)/2

  #Rename the PSD column as our test/device name
  psd.columns = [device]

  #Combine PSDs
  psd_overall = pd.concat([psd_overall,psd], axis=1)

psd_overall
[ ]:
fig = px.line(psd_overall[4:500])
fig.update_layout(
    psd_labels,
    title_text='PSD Comparison',
)

Octave PSD

[ ]:
oct_psd = endaq.calc.psd.to_octave(psd_overall, fstart=4, octave_bins = 0.5)

fig = px.line(oct_psd['Shaker'][:256])
fig.update_layout(
    psd_labels,
    title_text='PSD Comparison',
    template='endaq_light'
)

Transfer Function

I can use the shaker data to compute a transfer function for the other devices on different length beams. This is accomplished by taking the square root of the ratio from the beam PSD to the shaker.

[ ]:
transfer = psd_overall[5:240].copy().drop('Shaker',axis=1)
for col in transfer.columns:
    transfer[col] = (psd_overall[col]/psd_overall['Shaker']) ** 0.5
transfer

Now we can plot it to see the natural frequencies of these two beams.

[ ]:
fig = px.line(transfer)
fig.update_layout(
    title_text='Transfer Function',
    xaxis_title_text='Frequency (Hz)',
    yaxis_title_text='Transfer (g/g)',
    legend_title_text='',
    xaxis_type='log',
    yaxis_type='log',
)

Integrate to Displacement

We also have a function to integrate to velocity and displacement which is done here (see docs).

[ ]:
displacements = {}
for device in data.keys():
  #Get Z axis and filter
  accel = data[device]['Z (40g)'].to_frame()

  #Double Integrate to Displacement
  [df_accel, df_vel] = endaq.calc.integrate.integrals(accel, n=1, highpass_cutoff=2, tukey_percent=0.2)
  [df_vel, df_disp] = endaq.calc.integrate.integrals(df_vel, n=1, highpass_cutoff=2, tukey_percent=0.2)
  df_disp = df_disp*9.81*39.37 #convert to in

  #Rename the column as our test/device name
  df_disp.columns = [device]

  #Combine Times
  displacements[device] = df_disp

displacements

Resample at Slower Rate

Displacement is dominated by lower frequency content, so we can reduce the amount of data by resampling at 200 Hz with another function.

[ ]:
displacement = pd.DataFrame()
for device in displacements.keys():
  #Resample at 100 Hz
  disp_resampled = endaq.calc.utils.resample(displacements[device], sample_rate = 200)

  #Rename the index name to help with "melting"
  disp_resampled.index.name = 'index'

  #Combine Into One DataFrame
  displacement = pd.concat([displacement, disp_resampled.reset_index().melt(id_vars='index').dropna()])

displacement
[ ]:
fig = px.line(displacement,
             x='index',
             y='value',
             color='variable')
fig.update_layout(
    title_text = 'Z Axis Displacement Resampled at 200 Hz',
    yaxis_title_text = 'Displacement (in)',
    xaxis_title_text = '',
    legend_title_text = '',
)

Synchronization is quite impressive if I do say so myself!! Maybe off by about 0.003 seconds?

Story 3: Shock Event

Now let’s analyze some data that was discussed in our blog on the shock response spectrum.

[ ]:
doc = endaq.ide.get_doc('https://info.endaq.com/hubfs/data/Motorcycle-Car-Crash.ide')
accel = endaq.ide.to_pandas(doc.channels[8])
accel

Plot at Peak Time

The around_peak() function (see docs) takes in a dataframe and plots around the peak value across all columns.

[ ]:
fig = endaq.plot.plots.around_peak(
    accel,
    num=1500,
    leading_ratio=0.4
    )
fig.update_layout(
    accel_time_labels,
    title_text='Acceleration Around Peak',
)

Low Pass Filter

Now let’s apply a bunch of low-pass filters (see docs).

[ ]:
accel = accel['Y (500g)'].to_frame() #keep as a dataframe
accel = (accel - accel.median()) * -1 #apply a high pass and make the shock positive acceleration

accel.columns = ['No Low-Pass']
freqs = [1600, 800, 400, 200, 100, 50]
for freq in freqs:
  name = 'Filtered at: '+str(freq)+' Hz'
  accel[name] = endaq.calc.filters.butterworth(accel['No Low-Pass'].to_frame(), high_cutoff=freq)

accel = accel['2019-07-03 17:05:08.4':'2019-07-03 17:05:08.55'] #isolate the time of interest
accel = accel - accel.iloc[0] #force start to 0 to remove any filtering artifact
accel
[ ]:
fig = px.line(accel)
fig.update_layout(
    accel_time_labels,
    title_text='Motorcycle Car Crash, Effect of Filtering'
)

Shock Response Spectrum

First we need to define which frequencies we want to calculate and plot the SRS for.

[ ]:
freqs = endaq.calc.utils.logfreqs(accel, init_freq=1, bins_per_octave=12)

Now we can calculate the shock response spectrum (see docs).

[ ]:
srs = endaq.calc.shock.shock_spectrum(accel, freqs=freqs, damp=0.05, mode='srs')
[ ]:
fig = px.line(srs)
fig.update_layout(
    title_text='Shock Response Spectrum (SRS)',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Peak Acceleration (g)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

Pseudo Velocity Shock Spectrum

That function also allows us to calculate the PVSS.

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

Enveloped Half Sine

Once a PVSS is calculated we have a function to find a half-sine pulse that would envelop that PVSS (this can make reproducing the event easy with a drop test). See the docs for more.

[ ]:
t = np.linspace(0,0.1,num=1000)  # NOTE: if there aren't enough samples, low-frequency artifacts will appear!

def half_sine_pulse(t, T):
    result = np.zeros_like(t)
    result[(t > 0) & (t < T)] = np.sin(np.pi*t[(t > 0) & (t < T)] / T)
    df_result = pd.DataFrame({'Time':t,
                              'Pulse':result}).set_index('Time')
    return df_result
[ ]:
for c in ['No Low-Pass', 'Filtered at: 200 Hz']:

  half_sine_params = endaq.calc.shock.enveloping_half_sine(pvss[c]/9.81/39.37, damp=0.05)
  pvss_pulse = endaq.calc.shock.shock_spectrum(half_sine_params[0] * half_sine_pulse(t, T=half_sine_params[1]),
                                              freqs=freqs, damp=0.05, mode='pvss')*9.81*39.37

  fig.add_trace(go.Scattergl(
      x=pvss_pulse.index,
      y=pvss_pulse[pvss_pulse.columns[0]].values,
      name='Half Sine of '+c+': '+str(np.round(half_sine_params[0],1))+'g, '+ str(np.round(half_sine_params[1],5)) +'s',
      line_width=6,
      line_dash='dot'
  ))

fig.show()

Impact of Damping

[ ]:
pvss_damping = pd.DataFrame()
damps = [0, 0.025, 0.05, 0.10]
for damp in damps:
  pvss = endaq.calc.shock.shock_spectrum(accel['No Low-Pass'].to_frame(), freqs=freqs, damp=damp, mode='pvss')
  name = 'Damping Ratio = '+str(damp)
  pvss_damping[name] = pvss[pvss.columns[0]]*9.81*39.37 #convert to in/s
[ ]:
fig = px.line(pvss_damping)
fig.update_layout(
    title_text='Impact of Damping on PVSS',
    xaxis_title_text="Natural Frequency (Hz)",
    yaxis_title_text="Psuedo Velocity (in/s)",
    legend_title_text='',
    xaxis_type="log",
    yaxis_type="log",
  )

Integration to Velocity

[ ]:
[df_accel, df_vel] = endaq.calc.integrate.integrals(accel, n=1, highpass_cutoff=None, tukey_percent=0)

df_vel = df_vel-df_vel.iloc[0] #forced the starting velocity to 0
df_vel = df_vel*9.81*39.37 #convert to in/s
[ ]:
fig = px.line(df_vel)
fig.update_layout(
    title_text="Integrated Velocity Time History",
    xaxis_title_text="",
    yaxis_title_text="Velocity (in/s)",
    legend_title_text=''
)
fig.show()

Story 4: Moving Frequency

[ ]:
df_vibe = pd.read_csv('https://info.endaq.com/hubfs/data/motorcycle-vibration-moving-frequency.csv',index_col=0)
df_vibe = df_vibe - df_vibe.median()
df_vibe

Shaded Plot in Time

[ ]:
fig = endaq.plot.plots.rolling_min_max_envelope(
    df_vibe,
    desired_num_points=1000,
    plot_as_bars=True,
    opacity=1.0
    )
fig.update_layout(
    accel_time_labels,
    title_text='Engine Reving of Motorcycle',
)
fig.show()

Spectrogram

[ ]:
freqs, bins, Pxx, fig = endaq.plot.octave_spectrogram(df_vibe[['Z (40g)']],
                                                      window=0.5,
                                                      bins_per_octave=12,
                                                      max_freq=1000,
                                                      freq_start=40)
fig.show()

Plot of Frequency vs Time

[ ]:
df_Pxx = pd.DataFrame(Pxx, index= freqs, columns = bins)
df_Pxx = 10 ** (df_Pxx/10)
[ ]:
fig = px.line(df_Pxx[df_Pxx.index<500].idxmax())
fig.update_layout(
    title_text="Moving Peak Frequency",
    xaxis_title_text="",
    yaxis_title_text="Peak Frequency (Hz)",
    showlegend=False
)
fig.show()

PSD Animation over Time

Now we can prepare for creating an animation of the PSD over time.

[ ]:
df_Pxx.index.name='Frequency (Hz)'
df_Pxx.columns.name = 'Time (s)'
df_Pxx.columns = np.round(df_Pxx.columns,2)

Create base figure with animation, this will render fine but we’ll want to add the other lines for scaling and reference.

[ ]:
fig = px.line(
    df_Pxx.reset_index().melt(id_vars='Frequency (Hz)'),
    x='Frequency (Hz)',
    y='value',
    animation_frame='Time (s)',
    log_x=True,
    log_y=True,
)

Add in the max, min, median, and mean lines.

[ ]:
def add_line(df_stat,name,dash,color):
  fig.add_trace(go.Scattergl(
    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'],
                           ['dash','dash','dot'],
                           [1.0,0.0,0.5]):
  df_stat = df_Pxx.quantile(quant, axis=1)
  add_line(df_stat,stat,dash,'#6914F0')

#Add in mean
df_stat = df_Pxx.mean(axis=1)
add_line(df_stat,'Mean','dot','#2DB473')
[ ]:
fig.update_layout(
    legend_y=-0.7,
    yaxis_title_text='Acceleration (g^2/Hz)'
)
fig.show()

Compare to Overall PSD

[ ]:
psd = endaq.calc.psd.welch(df_vibe[['Z (40g)']], bin_width=0.25)
oct_psd = endaq.calc.psd.to_octave(psd,octave_bins=12,fstart=40)
[ ]:
fig = px.line(oct_psd[oct_psd.index<=1000])
fig.update_layout(
    psd_labels
)

Story 5: Sound Data

[ ]:
mic = endaq.ide.to_pandas(endaq.ide.get_doc('https://info.endaq.com/hubfs/data/sound/gangnam-style.ide').channels[8],time_mode='seconds')
mic
[ ]:
mic = mic*-5.3075 #convert to Pa, this will be natively done with devices starting in the next month or so
[ ]:
fig = endaq.plot.plots.rolling_min_max_envelope(
    mic,
    desired_num_points=1000,
    plot_as_bars=True,
    opacity=1.0
    )
fig.update_layout(
    title_text='Gangnam Style, Then Fan',
    yaxis_title_text='Sound Level (Pa)',
    xaxis_title_text='',
    showlegend=False
)
fig.show()

Save .WAV File

[ ]:
import scipy.io.wavfile

mic_normalized = mic.copy()
mic_normalized /= np.max(np.abs(mic_normalized),axis=0)

scipy.io.wavfile.write(
    'sound.wav',
    int(np.round(1/endaq.calc.utils.sample_spacing(mic))),
    mic_normalized.values.astype(np.float32),
)

Play in Notebook

[ ]:
import IPython
IPython.display.display(IPython.display.Audio('sound.wav'))

Convert to dB

[ ]:
n = int(mic.shape[0]/100)
rolling_pa = mic.rolling(n).std()[::n]
rolling_dB = rolling_pa.apply(endaq.calc.utils.to_dB, reference=endaq.calc.utils.dB_refs["SPL"], raw=True)
[ ]:
fig = px.line(rolling_dB)
fig.update_layout(
    title_text='Gangnam Style, Then Fan Sound Level',
    yaxis_title_text='Sound Level (dB)',
    xaxis_title_text='',
    showlegend=False
)
fig.show()

dB vs Frequency

[ ]:
df_pascal_psd = endaq.calc.psd.welch(mic, bin_width=1)
df_pascal_octave = endaq.calc.psd.to_octave(df_pascal_psd*df_pascal_psd.index[1],agg="sum",octave_bins=3, fstart=10)

df_audio_psd_dB = df_pascal_octave.apply(endaq.calc.utils.to_dB,
                                         reference=endaq.calc.utils.dB_refs["SPL"]**2,
                                         squared=True,
                                         raw=True)
[ ]:
fig = px.line(df_audio_psd_dB)
fig.update_layout(
    title_text='Sound Level vs Frequency',
    xaxis_title_text='Frequency (Hz)',
    xaxis_type='log',
    yaxis_title_text='Sound Level (dB)',
    showlegend=False
)

Spectrogram

[ ]:
freqs, bins, Pxx, fig = endaq.plot.octave_spectrogram(mic, window=0.5, bins_per_octave=12, freq_start=40, max_freq=5000)
fig.show()

enDAQ Cloud as an Alternative

Our enDAQ cloud (cloud.endaq.com) offers an environment to generate interactive dashboards for free without the need to write Python code.

But what is especially unique is that our cloud also allows paying tiers (starting at $100/month) to customize these dashboards with code to accelerate the analysis cycle and allow deploying customizing dashboard generation to colleagues and customers without ever needing to install anything.

Here is an example that has it’s own unique URL for sharing with colleagues (requires log-in).

image0

For more on generating these custom reports see our Help Article.

What’s Coming Next?

More webinars and more functionality!

  1. User Requested Examples

  2. Release of endaq.batch for Batch Processing

  3. Updating enDAQ Cloud to Provide Access to New Python Library