Analyze Z7 Smartfin Data

  • Decode from base85/ascii85 to human-readable data
  • put data in DataFrame for subsequent analysis
  • visualize time-series data
  • calculate additional wave stats from motion data
In [1]:
import base64
import html
import pandas as pd
import numpy as np
import datetime

from scipy.signal import butter, lfilter, freqz
from scipy import signal
from scipy import integrate
from scipy.signal import find_peaks

import matplotlib.pyplot as plt

import plotly.plotly as py
import plotly.graph_objs as go
from plotly import tools

import pytz
pacific = pytz.timezone('US/Pacific')

from scraping_utils import decoded_to_df

%matplotlib widget

Quick clean-up

  • Make sure we don't have hidden, latent variables from a previously executed run
In [2]:
if 'message' in locals():
    del message
if 'messages' in locals():
    del messages

# Only change this to true if the Smartfin's firmware has
# `#define USING_IMU_TEMP_SENSOR 1`
# which is indicative of a faulty TMP116 sensor and an unlikely scenario in future fin distribution
imu_temp_enabled = False

Enter filename

In [3]:
messages = pd.read_csv('Z_Data/Sfin-Z7-00037_190515-000000_CDIP.csv', header=None)

Decode and place data in pandas DataFrame

Only one of decode_particle or decode_google_doc will be used, depending on whether input data come from a single Particle cloud message or an arbitrary number of lines copied out of a Google Sheet and saved in a CSV file. The CSV file option (decode_google_doc) is far more common.

In [4]:
def decode_particle(message):
    decoded = base64.b85decode(message)
    return decoded

def decode_google_doc(message):
    url_decoded = html.unescape(message)
    decoded = base64.b85decode(url_decoded)
    return decoded

Choose which of above functions to execute in order to get a DataFrame of decoded data

In [5]:
#%% Create DF, decode data, add to DF
# create df
columns = ['type_name', 'time', 'temp', 'wet', 'Ax', 'Ay', 'Az', 'lat', 'lon', 'text', 'batt_v']
df = pd.DataFrame(columns=columns)

if 'message' in locals():
    # decoded = decode_particle(message) # if directly off of Particle (eg CoolTerm readout)
    decoded = decode_google_doc(message) # if getting from Google Doc
    df= decoded_to_df(df, decoded)
elif 'messages' in locals():
    for index, row in messages.iterrows():
        # print(row[1])
        decoded = decode_google_doc(row[1])
        df = decoded_to_df(df, decoded)
else:
    print("Check for proper input variable naming")

df.head()
Out[5]:
type_name time temp wet Ax Ay Az lat lon text batt_v epoch
0 TEMP+IMU+GPS 1468.8 18.992188 False 0.206848 -0.406250 0.811218 32.870002 -117.266687 NaN NaN NaN
1 TEMP+IMU+GPS 1469.8 19.000000 False 0.228394 -0.499878 0.842773 32.870004 -117.266688 NaN NaN NaN
2 TEMP+IMU+GPS 1470.8 19.015625 False 0.199707 -0.447205 0.778870 32.869993 -117.266696 NaN NaN NaN
3 TEMP+IMU+GPS 1471.8 18.984375 False 0.210571 -0.524353 0.862061 32.869989 -117.266700 NaN NaN NaN
4 TEMP+IMU+GPS 1472.7 18.976562 False 0.189392 -0.502869 0.792358 32.869990 -117.266703 NaN NaN NaN

Filter based on wetness, good locations, and accelerations, if desired

In [6]:
# obs that are wet and with lat/lon
wet_locations = df[df['wet'] == True]
wet_locations = wet_locations.dropna(subset = ['lat', 'lon'])

# simple_filter = (wet_locations['Ay'] < 1.2) & \
#                 (wet_locations['Ay'] > 0.8)
# wet_locations = wet_locations[simple_filter]

Time-series plots of decoded and wet data

In [9]:
y_var  = 'Ay'

# Create traces
trace1 = go.Scatter(
    x = df['time'],
    y = df[y_var],
    mode = 'markers',
    name = 'all data'
)

trace2 = go.Scatter(
    x = wet_locations['time'],
    y = wet_locations[y_var],
    mode = 'markers',
    name = 'filtered data'
)

data = [trace1, trace2]

# Plot and embed here
py.iplot(data, filename='time-series-scatter')


# plt.plot(, , 'r.', label = 'All data')
# plt.plot(wet_locations['time'], wet_locations[y_var], 'b.', label = 'Filtered data')
# plt.legend()
Out[9]:

Set up filters

In [10]:
#%% Filter out noisy data

# Filter requirements
order = 6
fs = 1       # sample rate, Hz
cutoff = 0.2  # desired cutoff frequency of the filter, Hz

def butter_lowpass(cutoff, fs, order=5):
    nyq = 0.5 * fs
    normal_cutoff = cutoff / nyq
    b, a = butter(order, normal_cutoff, btype='low', analog=False)
    return b, a

def butter_lowpass_filter(data, cutoff, fs, order=5):
    b, a = butter_lowpass(cutoff, fs, order=order)
    y = lfilter(b, a, data)
    return y

Filter and integrate motion sensor data

  • Get velocity from integrated accelerations
  • Get displacement from integrated velocity
In [40]:
#%% Integrate motion sensor vars

# Get rid of rows where IMU A vals are NaN
motion_df_goodAcc = wet_locations.dropna(subset = ['Ax', 'Ay', 'Az']) 

# Covert from G's to m2/s 
ax_m2_s = motion_df_goodAcc.loc[:, 'Ax']*9.81 
ay_m2_s = motion_df_goodAcc.loc[:, 'Ay']*9.81
az_m2_s = motion_df_goodAcc.loc[:, 'Az']*9.81

# Calculate the magnitude of the 3-D vectors and subtract Earth's gravity
acc_mag = (ax_m2_s**2 + ay_m2_s**2 + az_m2_s**2)**0.5-9.81
#acc_mag = butter_lowpass_filter(acc_mag, 1, 5, 6)

# Integrate acc to get vel
vel_mag = integrate.cumtrapz(acc_mag, dx = 1) # dt = 1 s
vel_mag = np.append(vel_mag, vel_mag[-1])

# Make a Pandas series for math below
vel_series = pd.Series(vel_mag, motion_df_goodAcc.index, name = 'vel (m/s)')
vel_filt = butter_lowpass_filter(vel_series, cutoff, fs, order)
vel_series -= vel_filt
vel_series -= vel_series.mean()

# Integrate vel to get displacement
dis_mag = integrate.cumtrapz(vel_series, dx = 1) # dt = 1 s
dis_mag = np.append(dis_mag, dis_mag[-1])
dis_series = pd.Series(dis_mag, motion_df_goodAcc.index, name = 'displacement (m)')
dis_filt = butter_lowpass_filter(dis_series, cutoff, fs, order)
dis_series -= dis_filt
dis_series -= dis_series.mean()

# Add velocity and displacement back to motion DF
motion_df_integrated = pd.concat([motion_df_goodAcc, vel_series, dis_series], axis = 1)
motion_df_integrated.sort_values(by = 'time', inplace = True)
motion_df_integrated.reset_index(inplace = True, drop = True)

motion_df_integrated.head(20)
Out[40]:
type_name time temp wet Ax Ay Az lat lon text batt_v epoch vel (m/s) displacement (m)
0 TEMP+IMU 1.9 18.609375 True 0.027954 1.045410 -0.154358 32.868102 -117.266319 NaN 3.832 NaN 0.510080 0.761407
1 TEMP+IMU 2.9 18.617188 True 0.077515 1.014160 -0.103271 32.868102 -117.266319 NaN 3.832 NaN 0.228426 0.724428
2 TEMP+IMU+GPS 3.9 18.625000 True 0.021606 0.993408 -0.072266 32.868091 -117.266305 NaN 3.832 NaN -0.305345 -0.048997
3 TEMP+IMU 4.9 18.617188 True 0.021851 1.021545 -0.104919 32.868091 -117.266305 NaN 3.832 NaN -0.651840 -1.167660
4 TEMP+IMU 5.9 18.648438 True 0.019104 1.021423 -0.101013 32.868091 -117.266305 NaN 3.832 NaN -0.876063 -2.028768
5 TEMP+IMU+GPS 6.8 18.664062 True 0.026062 0.976135 -0.081360 32.868109 -117.266329 NaN 3.832 NaN -0.783025 -2.151765
6 TEMP+IMU+GPS 7.8 18.671875 True 0.022339 1.027466 -0.095703 32.868117 -117.266333 NaN 3.832 NaN -0.445695 -1.531002
7 BATT INFO 8.6 18.671875 True 0.022339 1.027466 -0.095703 32.868117 -117.266333 NaN 3.830 NaN -0.034731 -0.370825
8 TEMP+IMU+GPS 8.8 18.687500 True 0.019775 1.077942 -0.098022 32.868114 -117.266335 NaN 3.830 NaN 0.626675 0.816424
9 TEMP+IMU 9.8 18.687500 True 0.016113 1.104919 -0.084290 32.868114 -117.266335 NaN 3.830 NaN 0.658182 1.413724
10 TEMP+IMU 10.8 18.687500 True 0.027710 0.965271 -0.107361 32.868114 -117.266335 NaN 3.830 NaN 0.369859 1.485894
11 TEMP+IMU 11.8 18.687500 True 0.018372 1.070740 -0.088379 32.868114 -117.266335 NaN 3.830 NaN 0.461458 1.313454
12 TEMP+IMU 12.8 18.687500 True 0.016846 1.076172 -0.093018 32.868114 -117.266335 NaN 3.830 NaN 0.396844 1.184961
13 TEMP+IMU 13.7 18.726562 True 0.020874 1.036194 -0.078613 32.868114 -117.266335 NaN 3.830 NaN 0.606921 1.371286
14 TEMP+IMU 14.7 18.718750 True 0.016602 1.100708 -0.100098 32.868114 -117.266335 NaN 3.830 NaN 0.777273 1.634920
15 TEMP+IMU 15.7 18.742188 True 0.024353 1.015381 -0.087463 32.868114 -117.266335 NaN 3.830 NaN 0.570263 1.614088
16 TEMP+IMU 16.7 18.750000 True 0.020630 1.052612 -0.055603 32.868114 -117.266335 NaN 3.830 NaN 0.320031 1.185109
17 TEMP+IMU+GPS 17.7 18.718750 True 0.020264 1.035095 -0.078918 32.868108 -117.266316 NaN 3.830 NaN 0.036618 0.638008
18 BATT INFO 18.6 18.718750 True 0.020264 1.035095 -0.078918 32.868108 -117.266316 NaN 3.830 NaN 0.165976 0.374604
19 TEMP+IMU+GPS 18.7 18.734375 True 0.020752 1.079102 -0.104065 32.868110 -117.266316 NaN 3.830 NaN 0.280717 0.275863

Plot integrated/filtered motion sensor data

In [35]:
#%% Plot Integrals
# fig, axs = plt.subplots(figsize = (8, 6), nrows = 4, sharex = True)

# axs[0].plot(motion_df_integrated['time'], motion_df_integrated.loc[:, 'Ax':'Az'], '.-')
# axs[1].plot(motion_df_integrated['time'], motion_df_integrated.loc[:, 'vel (m/s)'], '.-')
# axs[2].plot(motion_df_integrated['time'], motion_df_integrated.loc[:, 'displacement (m)'], '.-')
# axs[3].plot(motion_df_integrated['time'], motion_df_integrated['temp'], '.')

# axs[0].set_ylabel('G Force (g\'s)')
# axs[1].set_ylabel('Vel (m/s)')
# axs[2].set_ylabel('Disp (m/s)')
# axs[3].set_ylabel('Temp $^o$F')
# axs[3].set_xlabel('Time (s)')
# fig.autofmt_xdate()
# date_format = '%H:%M:%S'
# myFmt = mdates.DateFormatter(date_format, tz = pacific)
# axs[2].xaxis.set_major_formatter(myFmt)

# plt.tight_layout()

# Create traces
trace1 = go.Scatter(
    x = motion_df_integrated['time'], 
    y = motion_df_integrated['Ax'],
    mode = 'lines+markers',
    name = 'Ax'
)

trace2 = go.Scatter(
    x = motion_df_integrated['time'], 
    y = motion_df_integrated['Ay'],
    mode = 'lines+markers',
    name = 'Ay'
)

trace3 = go.Scatter(
    x = motion_df_integrated['time'], 
    y = motion_df_integrated['Az'],
    mode = 'lines+markers',
    name = 'Az'
)

trace4 = go.Scatter(
    x = motion_df_integrated['time'], 
    y = motion_df_integrated['vel (m/s)'],
    mode = 'lines+markers'
)

trace5 = go.Scatter(
    x = motion_df_integrated['time'], 
    y = motion_df_integrated['displacement (m)'],
    mode = 'lines+markers'
)

trace6 = go.Scatter(
    x = motion_df_integrated['time'], 
    y = motion_df_integrated['temp'],
    mode = 'lines+markers'
)

# data = [trace1, trace2]

fig = tools.make_subplots(rows = 4, cols = 1, shared_xaxes=True)

fig.append_trace(trace1, 1, 1)
fig.append_trace(trace2, 1, 1)
fig.append_trace(trace3, 1, 1)
fig.append_trace(trace4, 2, 1)
fig.append_trace(trace5, 3, 1)
fig.append_trace(trace6, 4, 1)


# Plot and embed here
py.iplot(fig, filename='all-integrated')
This is the format of your plot grid:
[ (1,1) x1,y1 ]
[ (2,1) x1,y2 ]
[ (3,1) x1,y3 ]
[ (4,1) x1,y4 ]

Out[35]:

Find peaks in displacement signal, $\eta$

  • repeat for the negative signal to find troughs
In [36]:
eta_plus = motion_df_integrated['displacement (m)'] 
eta_minus = -1*motion_df_integrated['displacement (m)'] 
time = motion_df_integrated['time']

peaks, _ = find_peaks(eta_plus, distance = 3)
troughs, _ = find_peaks(eta_minus, distance = 3)
In [37]:
trace1 = go.Scatter(
    x = time, 
    y = eta_plus,
    mode = 'lines+markers', 
    name = 'Free Surface'
)

trace2 = go.Scatter(
    x = time[peaks], 
    y = eta_plus[peaks],
    mode = 'markers',
    marker = dict(
        size = 8,
        color = 'rgb(255,0,0)',
        symbol = 'cross'
    ),
    name = 'Detected Peaks'
)

trace3 = go.Scatter(
    x = time[troughs], 
    y = eta_plus[troughs],
    mode = 'markers',
    marker = dict(
        size = 8,
        color = 'rgb(255,0,255)',
        symbol = 'cross'
    ),
    name = 'Detected Troughs'
)

fig = tools.make_subplots(rows = 1, cols = 1, shared_xaxes=True)

fig.append_trace(trace1, 1, 1)
fig.append_trace(trace2, 1, 1)
fig.append_trace(trace3, 1, 1)

# Plot and embed here
py.iplot(fig, filename='peaks')
This is the format of your plot grid:
[ (1,1) x1,y1 ]

Out[37]:
In [38]:
print(len(peaks), len(troughs))
211 211

Calculate amplitude and $H_s$

In [39]:
# Calculate shorter array of peaks vs. troughs and make sure both arrays are same size
n_peaks = len(peaks)
n_troughs = len(troughs)
n_lesser = np.min([n_peaks, n_troughs])

# Grab heights of peaks and troughs up to length of smaller array
h_peaks = eta_plus[peaks].values[:n_lesser]
h_troughs = eta_plus[troughs].values[:n_lesser]

# Calculate height as peak minus trough (amplitude x 2)
height = np.abs(h_peaks-h_troughs)

# Calculate length of top third of all waves observed
n_topthird = int(len(height)/3)

# Calculate height of top third of all waves
h_topthird = (height[np.argsort(height)[-n_topthird:]])
# print(h_topthird)

# Calculate significant wave height: mean of height of top third
h_s = np.nanmean(h_topthird)
print("Significant wave height:", h_s, "m")
Significant wave height: 4.584837576804674 m

CDIP Comparison

At the same time as these data were collected, here is what was being recorded by the nearby CDIP Wave Rider buoy
significant wave height (about an order of magnitude smaller than my clearly erroneous calculation above):
Hs Scripps Nearshore CDIP 201.png

and peak period:
Ts Scripps Nearshore CDIP 201.png