Analyzing Electric Motor Temperatures with ASAM ODS#

This example Notebook shows how to implement Kirgsn example notebook EDA and Baseline on Motor Temperature Estimation from Kaggl using odsbox.

The data are collected by the LEA (Power Electronics and Electrical Drives) Department of the Paderborn University from a permanent magnet synchronous motor (PMSM) which is deployed on a test bench: Electric Motor Temperature.

The first sections are on initializing and connecting. The fun starts with “Load data”.

Dependencies for this notebook#

The ASAM ODSBox contains some functionality that wraps the ODS HTTP API making using Python easier ;-)

try:  
  import odsbox
except:
  !pip install odsbox 
  
from odsbox.con_i import ConI
from odsbox.submatrix_to_pandas import submatrix_to_pandas

Electric Motor Temperature#

Establish session#

The ODS HTTP API is a session based API. The session ID is called con_i in the ODS documentation. The ASAM ODSBox uses con_i as API object representing the session. Close this session to release the connection license. Otherwise the session will be auto closed after 30 minutes of inactivity.

con_i = ConI(url='http://79.140.180.128:10032/api', auth=('Demo','mdm'))

Import Libs#

import numpy as np # linear algebra
import pandas as pd # data processing
import matplotlib.pyplot as plt
from matplotlib.colors import rgb2hex
import seaborn as sns
from sklearn.decomposition import PCA

Load Data#

The electric motor example data is stored in individual measurements starting with “Profile*” and we query für all (69) measurement (or more precisely their submatrices).

(We gave those measurements a random start date, which we use to order them by date…)

The mass data hasn’t been touched yet and we can examine it a bit before we actually load it - a bit like shape an steroids.

mea_lenghts = con_i.query_data({
    "SubMatrix": {"measurement.test.Name": {"$like": "Profile*"}},
    "$attributes": {
        "measurement.Name": 1,
        "SubMatrixNoRows": 1
    },
    "$orderby": {
        "measurement.measurement_begin": 1
    }
})

mea_lenghts = mea_lenghts.set_index(mea_lenghts.columns[0])
print(mea_lenghts.head())

ax = mea_lenghts.plot.barh(figsize=(10, 20), title='Sample size')
_ = ax.set_xticks(2*3600*np.arange(1, 8)) # 2Hz sample rate
_ = ax.set_xticklabels(list(range(1, 8)))
_ = ax.set_xlabel('Time in hours')
                SubMatrix.SubMatrixNoRows
MeaResult.Name                           
Profile_17                          15964
Profile_05                          14788
Profile_12                          21942
Profile_32                          20960
Profile_21                          17321
../_images/aabfb63a8584c5e17c747599118d07de8e6dcf3f3028b4a5cad02bae96b32844.png
ax = mea_lenghts.plot.hist(title='Sample size distribution', bins=50, figsize=(10, 5), grid=True)
_ = ax.set_xticks(2*3600*np.arange(1, 8)) # 2Hz sample rate
_ = ax.set_xticklabels(list(range(1, 8)))
_ = ax.set_xlabel('Time in hours')
../_images/e88e15f08d66db2fbf35252b28cac2b76b4464236588c833a39e20780f02c14b.png

Distributions#

Until now, we haven’t loaded any mass data, yet.

In our data model, the individual profiles are not contained in a big single dataset. They are stored profile by profile in so called submatrices (we looked at them before a bit). So we go and fetch the first profile returned as DataFrame by using submatrix_to_dataframe.

data_matrices = con_i.query_data({
    "SubMatrix": {"measurement.name": {"$like": "Profile_68*"}},
    "$attributes": {
        "measurement.Name": 1,
        "Id": 1
    },
    "$orderby": {
        "measurement.measurement_begin": 1
    }
})

# just pick the first one
dm_id = data_matrices["SubMatrix.Id"][0]
profile_name = data_matrices["MeaResult.Name"][0]

print(profile_name)

dm_df = submatrix_to_pandas(con_i, dm_id)
if "Time" in dm_df.columns:
    dm_df.set_index("Time", inplace=True)
elif "time" in dm_df.columns:
    dm_df.set_index("time", inplace=True)
dm_df.head()
Profile_68
U_q Coolant Stator_winding U_d Stator_tooth Motor_speed I_d I_q Pm Stator_yoke Ambient Torque
Time
0.0 -1.250622 26.948739 26.401552 -0.269267 26.165591 -0.003114 -2.000875 1.097745 28.011248 26.141670 26.280088 2.785112e-07
0.5 -0.510442 26.950800 26.396721 0.110050 26.172235 21.150647 -4.751546 -14.421556 28.002123 26.155253 26.275925 -1.122167e+01
1.0 -0.388580 26.953776 26.408694 0.907830 26.177153 42.000488 -15.815420 -49.228154 27.996198 26.159354 26.280659 -3.799358e+01
1.5 -0.260576 26.962654 26.401838 1.678359 26.181832 60.023822 -25.347121 -77.327501 27.993540 26.173699 26.280193 -5.986944e+01
2.0 0.134215 26.948778 26.396926 2.581367 26.172657 80.657265 -32.076248 -97.315560 28.001602 26.181490 26.277929 -7.541367e+01

… and now plot the distribution ….

target_features = ['Pm', 'Stator_tooth', 'Stator_yoke', 'Stator_winding']

# prepare colors
color_list = plt.cm.tab10(np.linspace(0, 1, 10)[list(range(10))+[0, 1]])
coi = target_features + [c for c in dm_df if c not in target_features]  # columns of interest
feat_clrs = {k: rgb2hex(color_list[i][:3]) for i, k in enumerate(coi)} if color_list is not None else {}

n_cols = 4
n_rows = np.ceil(dm_df.shape[1] / n_cols).astype(int)
fig, axes = plt.subplots(n_rows, n_cols, figsize=(2.8*n_cols, n_rows*3))
for i, (ax, col) in enumerate(zip(axes.flatten(), list(dm_df.columns))):
    sns.histplot(dm_df[col], kde=True, stat="density", ax=ax, color=feat_clrs[col], bins=30)
    if i % n_cols == 0:
        ax.set_ylabel('Frequency')
plt.tight_layout()
../_images/9e02d32fa35ce5ddc99ad2845d1f10547424544604c22e700c30f48c3f8fd35f.png

Time series gestalt#

First we’ll get an overview of the target temperature trends.

dm_df = submatrix_to_pandas(con_i, dm_id)
if "Time" in dm_df.columns:
    dm_df.set_index("Time", inplace=True)
elif "time" in dm_df.columns:
    dm_df.set_index("time", inplace=True)

coi = target_features
fig, ax = plt.subplots()
for c in coi:
    lines = ax.plot(dm_df[c], label=c, color=feat_clrs[c])
ax.set_title(profile_name)
ax.set_ylabel('Temp in °C')
ax.set_xlabel('Time in sec')
    
fig.tight_layout()
# _ = ax.legend(ncol=15, loc='lower center', bbox_to_anchor=(.5, 1), bbox_transform=fig.transFigure)
../_images/08c8e75465a213bbec87b4bd5503d8242bf8867d829083c91966a5437cac395c.png

Now we have a look at the input features

coi = [c for c in dm_df if c not in target_features]
max_values_per_col = dm_df.abs().max(axis=0)
fig, ax = plt.subplots()
for c in coi:
    lines = ax.plot(dm_df[c]/max_values_per_col[c], label=c, color=feat_clrs[c])
ax.set_title(profile_name)
ax.set_ylabel('Normalized feature')
ax.set_xlabel('Time in sec')
fig.tight_layout()
#_ = ax.legend(ncol=15, loc='lower center', bbox_to_anchor=(.5, 1), bbox_transform=fig.transFigure)
../_images/782734b39dd1bd70416d2a4f5f169024ab304fd78d7ae25ed54b6bc747524cf5.png
_ = dm_df[target_features].rolling(200).mean().plot(kind="line", subplots=False, title=profile_name)
_ = dm_df.drop(target_features,axis=1).rolling(200).mean().plot(kind="line", subplots=True, layout=(4,2), title=profile_name)
../_images/6d5d0b10598fc2f09c5f280c1d76c3a2f9c5d8ef5fd6171428110243f98c75d3.png ../_images/fd5dada39c6c5ec0258bb055a9b43dc641e8568b11c53842894ef3a941e6aada.png

Operation points#

The operation point of a motor is often explained by its location in the motor_speed-torque-plane.

fig, ax = plt.subplots()
ax.plot(dm_df['Motor_speed'], dm_df['Torque'])
ax.set_title(profile_name)
ax.set_ylabel('Torque in Nm')
ax.set_xlabel('Motor speed in rpm')
Text(0.5, 0, 'Motor speed in rpm')
../_images/92294ee4a59daac9ebcece71fcaa0900a85f5b8e51bceedad23e2d863496bd9b.png

We see that some driving cycles only capture little of the valid operation region, while other profiles do comprehensive random walks over the full operative range.

Note that motors are power rated, and since power is defined as P=motorspeed⋅torque, there are elliptical borders that can’t be exceeded.

Dimensionality reduced visualization#

Depict the sessions principal component axes, shifting the color from blue to red as the permanent magnet temperature rises.

Showing the two most significant principal components.

# normalize
dm_df = submatrix_to_pandas(con_i, dm_id)

dm_df = dm_df / dm_df.abs().max(axis=0)
transformed = PCA().fit_transform(dm_df.drop(target_features, axis=1))
sess_df = dm_df
_trans = transformed[sess_df.index, :]
plt.scatter(_trans[:, 0], _trans[:, 1], c=dm_df.loc[sess_df.index, 'Pm'].values, cmap=plt.get_cmap('coolwarm'), marker='.', vmin=dm_df['Pm'].min(), vmax=dm_df['Pm'].max())
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.tick_params(axis='both', which='both', bottom=False, top=False,
                        labelbottom=False, right=False, left=False,
                        labelleft=False)
plt.tight_layout()
../_images/2396cca70613f26822e87f2597f745ee17239911f34b61656aea4896e225c152.png

Close session#

Don’t forget to close the session to release the connection license. Otherwise the session will be auto closed after 30 minutes of inactivity.

con_i.logout()

Acknowledgement#

The data used by this notebook is based on the Kaggle Electric Motor Temperature data provided by Paderborn University under the CC BY-SA 4.0-License

License#

Copyright © 2024 Peak Solution GmbH

The training material in this repository is licensed under a Creative Commons BY-NC-SA 4.0 license. See LICENSE file for more information.