Condition monitoring of hydraulic system

Title: Condition monitoring of hydraulic system
Author: Michal Bezak, Tangent Works
Industry: Manufacturing, Agriculture, Transportation
Area: Condition monitoring
Type: Forecasting

Description

Hydraulic systems are commonly used in heavy equipment. In a hydraulic system, pressure, applied to a contained fluid is transmitted. That pressurized fluid acts upon every part of the section of a containing vessel and creates force. Due to the use of this force it is possible for instance to lift heavy loads. A hydraulic circuit is a system that controls where fluid flows as well as fluid pressure, it is responsible for transporting liquid through a set of interconnected components. It consists of multiple components and offers range of areas to monitor to ensure optimal operation. In our use case we will focus on valve.

Hydraulic system of our interest consists of a primary working and a secondary cooling-filtration circuit, both connected via the oil tank. The system operates in 60 seconds cycles. From operations perspective it is desired to understand if system is in sub-optimal condition so pro-active action can be taken. Condition monitoring improves efficiency and maximizes uptime of a machine.

To understand at what condition is concerned component operating (e.g. via custom metric), evaluation of measurements from relevant sensors is necessary. In this Use case we will solve following problem: Quantify condition of valve based on actual sensory measurements. This is not typical forecasting case as we do not predict values for the future timestamps, instead we build models to quantify KPI of interest based on input variables at given moment.

Business parameters

Business objective: Reduce outages in production process
Business value: Reduce cost of down-times and inefficient operations
KPI: -
In [31]:
import pandas as pd
import numpy as np

import plotly.graph_objects as go

import logging
import json
import datetime

import tim_client

Credentials and logging

(Do not forget to fill in your credentials in the credentials.json file)

In [3]:
with open('credentials.json') as f:
    credentials_json = json.load(f)                     # loading the credentials from credentials.json

TIM_URL = 'https://timws.tangent.works/v4/api'          # URL to which the requests are sent

SAVE_JSON = False                                       # if True - JSON requests and responses are saved to JSON_SAVING_FOLDER
JSON_SAVING_FOLDER = 'logs/'                            # folder where the requests and responses are stored

LOGGING_LEVEL = 'INFO'
In [4]:
level = logging.getLevelName(LOGGING_LEVEL)
logging.basicConfig(level=level, format='[%(levelname)s] %(asctime)s - %(name)s:%(funcName)s:%(lineno)s - %(message)s')
logger = logging.getLogger(__name__)
In [5]:
credentials = tim_client.Credentials(credentials_json['license_key'], credentials_json['email'], credentials_json['password'], tim_url=TIM_URL)
api_client = tim_client.ApiClient(credentials)

api_client.save_json = SAVE_JSON
api_client.json_saving_folder_path = JSON_SAVING_FOLDER
[INFO] 2021-07-06 10:06:35,976 - tim_client.api_client:save_json:66 - Saving JSONs functionality has been disabled
[INFO] 2021-07-06 10:06:35,978 - tim_client.api_client:json_saving_folder_path:75 - JSON destination folder changed to logs

Dataset

This dataset was gathered by measuring hydraulic system run on a test rig. The system cyclically repeats constant load cycles (duration of 60 sec.) and measures values such as: pressure, volume flow and temperature. Meanwhile the condition of hydraulic components - cooler, valve, pump, and accumulator - is quantified.

One of conditions monitored - valve - is selected as target variable.

Sampling

Raw data files were transformed into time series with 1-minute sampling rate. Raw data were sampled at variable frequency, e.g. pressure 100x per second, volume flow at 10 Hz, so aggregation was necessary to synchronize predictors in dataset.

Data

Structure of CSV file:

Column name Description Type Availability
time Date Timestamp column
valve Condition of valve metric Target t-1
cycle Operations cycle no. Predictor t+0
TS1 ... TS4 Temperature (Celsius) Predictor t+0
PS1 ... PS6 Pressure (bars) Predictor t+0
VS1 Vibration (mm/s) Predictor t+0
FS1 ... FS2 Volume flow (l/min) Predictor t+0
CP Cooling power (virtual, kW) Predictor t+0
CE Cooling efficiency (virtual, %) Predictor t+0
SE Efficiency factor (%) Predictor t+0
EPS1 Motor power (W) Predictor t+0

Meaning of valve condition values are explained below:

  • 100: optimal switching behavior
  • 90: small lag
  • 80: severe lag
  • 73: close to total failure

Data situation

If we want TIM to quantify current condition based on measurement, it means that we want to predict value of target based on predictors values, and so the last record of target must be kept empty (NaN/None) in dataset. TIM will use available predictors to predict given record. This situation will be replicated by TIM to calculate results for all out-of-sample records.

CSV files used in experiments can be downloaded here.

Source

Raw data files were acquired at Kaggle.

In [6]:
data = tim_client.load_dataset_from_csv_file('data_valve.csv', sep=',')  

data
Out[6]:
time valve TS3 PS4 FS2 SE CP TS1 PS3 TS4 EPS1 FS1 CE PS2 TS2 PS5 PS6 PS1 VS1
0 2020-01-01 00:00:00 100.0 38.471017 0.000000 10.304592 59.157183 1.862750 35.621983 1.991475 31.745250 2538.929167 6.709815 39.601350 109.466914 40.978767 9.842170 9.728097 160.673492 0.576950
1 2020-01-01 00:01:00 100.0 38.978967 0.000000 10.403098 59.335617 1.255550 36.676967 1.976234 34.493867 2531.498900 6.715315 25.786433 109.354890 41.532767 9.635142 9.529488 160.603320 0.565850
2 2020-01-01 00:02:00 100.0 39.631950 0.000000 10.366250 59.543150 1.113217 37.880800 1.972224 35.646150 2519.928000 6.718522 22.218233 109.158845 42.442450 9.530548 9.427949 160.347720 0.576533
3 2020-01-01 00:03:00 100.0 40.403383 0.000000 10.302678 59.794900 1.062150 38.879050 1.946576 36.579467 2511.541633 6.720565 20.459817 109.064807 43.403983 9.438827 9.337430 160.188088 0.569267
4 2020-01-01 00:04:00 100.0 41.310550 0.000000 10.237750 59.455267 1.070467 39.803917 1.922707 37.427900 2503.449500 6.690308 19.787017 108.931434 44.332750 9.358762 9.260636 160.000472 0.577367
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2200 2020-01-02 12:40:00 100.0 38.269267 10.202473 10.184515 59.033100 2.160600 35.313783 2.001438 30.404733 2543.911033 6.689930 46.628517 109.779581 40.874800 9.972037 9.850361 161.227572 0.550833
2201 2020-01-02 12:41:00 100.0 38.268250 10.197918 10.177767 59.068000 2.151450 35.321600 1.998781 30.416233 2543.411333 6.692182 46.689817 109.787480 40.868883 9.966184 9.844854 161.206070 0.547483
2202 2020-01-02 12:42:00 100.0 38.246367 10.196824 10.176172 59.132350 2.143300 35.319183 1.993436 30.426250 2542.729767 6.693277 46.472300 109.756174 40.875950 9.964329 9.842629 161.192120 0.545233
2203 2020-01-02 12:43:00 100.0 38.245733 10.198588 10.178353 58.970800 2.148483 35.324767 2.007077 30.414283 2544.046333 6.684128 46.544967 109.793884 40.876067 9.968232 9.846690 161.208917 0.537017
2204 2020-01-02 12:44:00 NaN 38.248917 10.203126 10.183393 59.053900 2.157050 35.322233 2.002690 30.390800 2543.818300 6.692302 46.647933 109.792177 40.859400 9.973638 9.851949 161.217128 0.546583

2205 rows × 19 columns

In [7]:
target_column = 'valve'

timestamp_column = 'time'

Visualization of data

In [8]:
fig = go.Figure()

fig.add_trace( go.Scatter( x = data['time'], y=data['valve'], name='Valve') )     

fig.show()

Engine settings

Parameters that need to be set are:

  • Prediction horizon is set to 1 because we want to predict (evaluate) current timestamp only.
  • Back-test length - defines out-of-sample interval.
  • selection of dictionaries (via features parameter) is critital because we want to build models based solely on values from sensors, without periodic component and influence of date; data was obtained on a test rig and period of cycles is rather regular, however we want robuts model that would learn from sensory measurements only not cycle parameters present in training interval; we also want to prevent generating features from dates (day of week, month) as we assume machine will be in operation without interuptions and influence of sasonality factor (assumption based on domain knowledge); thus we will use: Polynomial, Intercept and Identity dictionaries.
  • setting allowOffsets to False will prevent TIM to look back to actual target values (as well as predictors), this is critical as we want TIM to decide solely based on sensory measurements in given moment.

We also ask for additional data from engine to see details of sub-models so we define extendedOutputConfiguration parameter as well.

In [9]:
prediction_horizon = 1

backtest_length = int( data.shape[0] * .25 )
In [10]:
configuration_engine = {
        'usage': {                                 
            'predictionTo': { 
                'baseUnit': 'Sample', 
                'offset': prediction_horizon
            }, 
            'backtestLength': backtest_length, 
        },
        'features': [ 'Polynomial','Intercept','Identity' ],
        'allowOffsets': False,
        'extendedOutputConfiguration': {
            'returnExtendedImportances': True
        }
    }

Experiment iteration(s)

In [11]:
backtest = api_client.prediction_build_model_predict( data, configuration_engine ) 

backtest.status
Out[11]:
'Finished'
In [12]:
backtest.result_explanations
Out[12]:
[]

Insights into models and predictor's importance

In [15]:
simple_importances = backtest.predictors_importances['simpleImportances']
simple_importances = sorted(simple_importances, key = lambda i: i['importance'], reverse=True) 
simple_importances = pd.DataFrame.from_dict( simple_importances )
In [17]:
fig = go.Figure()

fig.add_trace(go.Bar( x = simple_importances['predictorName'], y = simple_importances['importance'] ) )

fig.update_layout( title='Simple importances' )

fig.show()
In [18]:
extended_importances = backtest.predictors_importances['extendedImportances']
extended_importances = sorted(extended_importances, key = lambda i: i['importance'], reverse=True) 
extended_importances = pd.DataFrame.from_dict( extended_importances )
In [19]:
fig = go.Figure()

fig.add_trace( go.Bar( x = extended_importances[ extended_importances['time'] == '[1]' ]['termName'],
                      y = extended_importances[ extended_importances['time'] == '[1]' ]['importance'] ) )

fig.update_layout( title='Features generated from predictors used by model')

fig.show()

Accuracy metrics for in-sample and out-of-sample intervals

In [21]:
backtest.aggregated_predictions[0]['accuracyMetrics'] # in-sample
Out[21]:
{'MAE': 1.4181039710964507,
 'MSE': 3.2020096295979052,
 'MAPE': 1.594784544880187,
 'RMSE': 1.7894160023867858}
In [22]:
backtest.aggregated_predictions[1]['accuracyMetrics'] # out-of-sample
Out[22]:
{'MAE': 2.9531794032388192,
 'MSE': 12.441941480577057,
 'MAPE': 3.437638708465135,
 'RMSE': 3.5273136351304313}

Visualization of results

In [24]:
def create_eval_df( predictions ):
    data2 = data.copy()
    data2[ 'time' ] = pd.to_datetime( data2[ 'time' ]).dt.tz_localize('UTC')
    data2.rename( columns={'time':'Timestamp'}, inplace=True)
    data2.set_index( 'Timestamp', inplace=True)

    eval_data = data2[ [ target_column ] ].join( predictions, how='inner' )

    return eval_data
In [26]:
edf_in_sample = create_eval_df( backtest.aggregated_predictions[0]['values'] )
In [27]:
fig = go.Figure()

fig.add_trace( go.Scatter( x = edf_in_sample.index, y=edf_in_sample['Prediction'], name='In-Sample') )     
fig.add_trace( go.Scatter( x = edf_in_sample.index, y=edf_in_sample[ target_column ], name='Actual') )     

fig.show()
In [28]:
edf_out_of_sample = create_eval_df( backtest.aggregated_predictions[1]['values'] )
In [30]:
fig = go.Figure()

fig.add_trace( go.Scatter( x = edf_out_of_sample.index, y=edf_out_of_sample['Prediction'], name='Out-of-Sample') )     
fig.add_trace( go.Scatter( x = edf_out_of_sample.index, y=edf_out_of_sample[ target_column ], name='Actual') )     

fig.show()

Summary

We demonstrated how TIM can support pro-active condition monitoring. Information provided by prediction can be used by operator of machine, if it drops below treshold value, appropriate action will be taken.

Domain knowledge is key factor in development of solutions for use cases like this one; it is important for proper target encoding as well as setting the right warning tresholds.