From c9cc034e66a952739d6a22fdf491831991e4f879 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 14 Oct 2020 21:32:58 -0700 Subject: [PATCH 01/83] initial commit for app that detects passing valves --- detect_passing_valves/README.md | 12 ++++++ detect_passing_valves/app.py | 59 ++++++++++++++++++++++++++ detect_passing_valves/requirements.txt | 2 + 3 files changed, 73 insertions(+) create mode 100644 detect_passing_valves/README.md create mode 100644 detect_passing_valves/app.py create mode 100644 detect_passing_valves/requirements.txt diff --git a/detect_passing_valves/README.md b/detect_passing_valves/README.md new file mode 100644 index 0000000..791cc4c --- /dev/null +++ b/detect_passing_valves/README.md @@ -0,0 +1,12 @@ +# Detect passing valves in VAV terminals + +This app detects valves that do not close all the way also known as passing valves. + +This app produces a CSV file called `passing_valves.csv` when run. Each row is a possible incidence of a passing valve. The CSV file contains the following columns: + +- site +- valve name +- start of incident +- end of incident +- expected temperature difference +- actual temperature difference \ No newline at end of file diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py new file mode 100644 index 0000000..76780c4 --- /dev/null +++ b/detect_passing_valves/app.py @@ -0,0 +1,59 @@ +import pymortar +import os + +# define parameters +eval_start_time = "2018-06-01T00:00:00Z" +eval_end_time = "2018-06-30T00:00:00Z" + + +client = pymortar.Client() + +# define query to return valves +query = """SELECT ?vlv ? equip WHERE { + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + OPTIONAL { + ?vlv bf:isPointOf ?equip + } +};""" + +# find sites with these sensors and setpoints +qualify_resp = client.qualify([query]) +if qualify_resp.error != "": + print("ERROR: ", qualify_resp.error) + os.exit(1) + +print("running on {0} sites".format(len(qualify_resp.sites))) + +# build the fetch request +request = pymortar.FetchRequest( + sites=qualify_resp.sites, + views=[ + pymortar.View( + name="valves", + definition=query, + ), + ], + dataFrames=[ + pymortar.DataFrame( + name="Vlv", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="valves", + dataVars=["?vlv"], + ) + ] + ), + ], + time=pymortar.TimeParams( + start=eval_start_time, + end=eval_end_time, + ) +) + +# call the fetch api +fetch_resp = client.fetch(request) +print(fetch_resp) + +import pdb; pdb.set_trace() \ No newline at end of file diff --git a/detect_passing_valves/requirements.txt b/detect_passing_valves/requirements.txt new file mode 100644 index 0000000..5465713 --- /dev/null +++ b/detect_passing_valves/requirements.txt @@ -0,0 +1,2 @@ +pymortar==1.0.9a2 +pandas>=1.1 \ No newline at end of file From d995d877f93b1e21b3eabe5c13c07e7320860d99 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 14 Oct 2020 22:24:04 -0700 Subject: [PATCH 02/83] developed query to return valve command points --- detect_passing_valves/app.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 76780c4..00e15ee 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1,5 +1,5 @@ import pymortar -import os +import sys # define parameters eval_start_time = "2018-06-01T00:00:00Z" @@ -9,18 +9,20 @@ client = pymortar.Client() # define query to return valves -query = """SELECT ?vlv ? equip WHERE { - ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - OPTIONAL { - ?vlv bf:isPointOf ?equip - } +query = """SELECT ?vlv ?equip ?subclass WHERE { + ?vlv rdf:type/rdfs:subClassOf? brick:Valve_Command . + ?vlv bf:isPointOf ?equip . + ?vlv rdf:type ?subclass . };""" # find sites with these sensors and setpoints qualify_resp = client.qualify([query]) if qualify_resp.error != "": print("ERROR: ", qualify_resp.error) - os.exit(1) + sys.exit(1) +elif len(qualify_resp.sites) == 0: + print("NO SITES RETURNED") + sys.exit(0) print("running on {0} sites".format(len(qualify_resp.sites))) @@ -35,7 +37,7 @@ ], dataFrames=[ pymortar.DataFrame( - name="Vlv", + name="valves", aggregation=pymortar.MEAN, window="15m", timeseries=[ From f3fd53d74d64745a0394822b970f26a78bb38e73 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 16 Oct 2020 13:47:00 -0700 Subject: [PATCH 03/83] develop query that gets points needed for detect passing valves --- detect_passing_valves/app.py | 84 ++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 00e15ee..ecf2f9c 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -9,14 +9,34 @@ client = pymortar.Client() # define query to return valves -query = """SELECT ?vlv ?equip ?subclass WHERE { - ?vlv rdf:type/rdfs:subClassOf? brick:Valve_Command . - ?vlv bf:isPointOf ?equip . - ?vlv rdf:type ?subclass . + +# returns equipments with valves including ahus +equip_query = """SELECT ?vlv ?ahu ?vlv_subclass ?equip ?equip_subclass ?sensor ?sensor_subclass +WHERE { + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?vlv bf:isPointOf ?equip . + ?vlv rdf:type ?vlv_subclass . + ?equip bf:hasPoint ?sensor . + ?sensor rdf:type/rdfs:subClassOf* brick:Temperature_Sensor . + ?sensor rdf:type ?sensor_subclass . + ?equip rdf:type ?equip_subclass . +};""" + +# returns supply air temps from ahu and vav and vav valve +vav_query = """SELECT * +WHERE { + ?vav rdf:type/rdfs:subClassOf? brick:VAV . + ?vav bf:isFedBy+ ?ahu . + ?ahu bf:hasPoint ?ahu_supply . + ?vav bf:hasPoint ?vav_supply . + ?ahu_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?vav_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?vav bf:hasPoint ?vav_vlv . + ?vav_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . };""" # find sites with these sensors and setpoints -qualify_resp = client.qualify([query]) +qualify_resp = client.qualify([equip_query, vav_query]) if qualify_resp.error != "": print("ERROR: ", qualify_resp.error) sys.exit(1) @@ -27,25 +47,43 @@ print("running on {0} sites".format(len(qualify_resp.sites))) # build the fetch request +# request = pymortar.FetchRequest( +# sites=qualify_resp.sites, +# views=[ +# pymortar.View( +# name="valves", +# definition=query, +# ), +# ], +# dataFrames=[ +# pymortar.DataFrame( +# name="valves", +# aggregation=pymortar.MEAN, +# window="15m", +# timeseries=[ +# pymortar.Timeseries( +# view="valves", +# dataVars=["?vlv"], +# ) +# ] +# ), +# ], +# time=pymortar.TimeParams( +# start=eval_start_time, +# end=eval_end_time, +# ) +# ) + request = pymortar.FetchRequest( sites=qualify_resp.sites, views=[ pymortar.View( - name="valves", - definition=query, + name="all_equip", + definition=equip_query, ), - ], - dataFrames=[ - pymortar.DataFrame( - name="valves", - aggregation=pymortar.MEAN, - window="15m", - timeseries=[ - pymortar.Timeseries( - view="valves", - dataVars=["?vlv"], - ) - ] + pymortar.View( + name="vav_equip", + definition=vav_query, ), ], time=pymortar.TimeParams( @@ -54,8 +92,16 @@ ) ) + + # call the fetch api fetch_resp = client.fetch(request) print(fetch_resp) +print(fetch_resp.view('vav_equip')) + + +# print the different types of valves in the data +#print(fetch_resp.view('valves').groupby(['vlv_subclass']).count()) + import pdb; pdb.set_trace() \ No newline at end of file From fb2444037a3ad3469f3950625c209fae89806686 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 16 Oct 2020 14:40:39 -0700 Subject: [PATCH 04/83] narrow scope of app to vav reheat valves --- detect_passing_valves/app.py | 87 ++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index ecf2f9c..f9b60fd 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -9,24 +9,12 @@ client = pymortar.Client() # define query to return valves - -# returns equipments with valves including ahus -equip_query = """SELECT ?vlv ?ahu ?vlv_subclass ?equip ?equip_subclass ?sensor ?sensor_subclass -WHERE { - ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - ?vlv bf:isPointOf ?equip . - ?vlv rdf:type ?vlv_subclass . - ?equip bf:hasPoint ?sensor . - ?sensor rdf:type/rdfs:subClassOf* brick:Temperature_Sensor . - ?sensor rdf:type ?sensor_subclass . - ?equip rdf:type ?equip_subclass . -};""" - # returns supply air temps from ahu and vav and vav valve vav_query = """SELECT * WHERE { ?vav rdf:type/rdfs:subClassOf? brick:VAV . ?vav bf:isFedBy+ ?ahu . + ?vav_vlv rdf:type ?vlv_type . ?ahu bf:hasPoint ?ahu_supply . ?vav bf:hasPoint ?vav_supply . ?ahu_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . @@ -36,7 +24,7 @@ };""" # find sites with these sensors and setpoints -qualify_resp = client.qualify([equip_query, vav_query]) +qualify_resp = client.qualify([vav_query]) if qualify_resp.error != "": print("ERROR: ", qualify_resp.error) sys.exit(1) @@ -47,45 +35,49 @@ print("running on {0} sites".format(len(qualify_resp.sites))) # build the fetch request -# request = pymortar.FetchRequest( -# sites=qualify_resp.sites, -# views=[ -# pymortar.View( -# name="valves", -# definition=query, -# ), -# ], -# dataFrames=[ -# pymortar.DataFrame( -# name="valves", -# aggregation=pymortar.MEAN, -# window="15m", -# timeseries=[ -# pymortar.Timeseries( -# view="valves", -# dataVars=["?vlv"], -# ) -# ] -# ), -# ], -# time=pymortar.TimeParams( -# start=eval_start_time, -# end=eval_end_time, -# ) -# ) - request = pymortar.FetchRequest( sites=qualify_resp.sites, views=[ pymortar.View( - name="all_equip", - definition=equip_query, - ), - pymortar.View( - name="vav_equip", + name="valves", definition=vav_query, ), ], + dataFrames=[ + pymortar.DataFrame( + name="valve", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="valves", + dataVars=["?vav_vlv"], + ) + ] + ), + pymortar.DataFrame( + name="vav_temp", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="valves", + dataVars=["?vav_supply"], + ) + ] + ), + pymortar.DataFrame( + name="ahu_temp", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="valves", + dataVars=["?ahu_supply"], + ) + ] + ), + ], time=pymortar.TimeParams( start=eval_start_time, end=eval_end_time, @@ -93,11 +85,10 @@ ) - # call the fetch api fetch_resp = client.fetch(request) print(fetch_resp) -print(fetch_resp.view('vav_equip')) +print(fetch_resp.view('valves')) # print the different types of valves in the data From 3ed1c5d8a511de920f104f0221f018eea7b2723e Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 21 Oct 2020 16:54:46 -0700 Subject: [PATCH 05/83] developed clean and analysis sections for the app --- detect_passing_valves/app.py | 249 ++++++++++++++++++++++++- detect_passing_valves/requirements.txt | 5 +- 2 files changed, 251 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index f9b60fd..9dfe748 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1,8 +1,14 @@ import pymortar import sys +import pandas as pd +import numpy as np + +from os.path import join +import matplotlib.pyplot as plt +from scipy.optimize import curve_fit # define parameters -eval_start_time = "2018-06-01T00:00:00Z" +eval_start_time = "2018-01-01T00:00:00Z" eval_end_time = "2018-06-30T00:00:00Z" @@ -94,5 +100,244 @@ # print the different types of valves in the data #print(fetch_resp.view('valves').groupby(['vlv_subclass']).count()) +def _clean(row): + + # combine data points in one dataframe + vav_sa = fetch_resp['vav_temp'][row['vav_supply_uuid']] + ahu_sa = fetch_resp['ahu_temp'][row['ahu_supply_uuid']] + vlv_po = fetch_resp['valve'][row['vav_vlv_uuid']] + + vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) + vav_df.columns = ['ahu_sa', 'vav_sa', 'vlv_po'] + + # identify when valve is open + vav_df['vlv_open'] = vav_df['vlv_po'] > 0 + + # calculate temperature difference between ahu and vav supply air + vav_df['temp_diff'] = vav_df['vav_sa'] - vav_df['ahu_sa'] + + # drop na + vav_df = vav_df.dropna() + + # drop values where vav supply air is less than ahu supply air + vav_df = vav_df[vav_df['temp_diff'] >= 0] + + return vav_df + + +def scale_0to1(temp_diff): + max_t = temp_diff.max() + min_t = temp_diff.min() + + new_t = (temp_diff - min_t) / (max_t - min_t) + + return new_t + +def rescale_fit(scaled_x, temp_diff): + max_t = temp_diff.max() + min_t = temp_diff.min() + + rescaled = min_t + scaled_x*(max_t - min_t) + + return rescaled + +def sigmoid(x, k, x0): + return 1.0 / (1 + np.exp(-k * (x - x0))) + +def get_fit_line(vav_df, x_col='vlv_po', y_col='temp_diff'): + # fit the curve + scaled_pos = scale_0to1(vav_df[x_col]) + scaled_t = scale_0to1(vav_df[y_col]) + popt, pcov = curve_fit(sigmoid, scaled_pos, scaled_t) + + # calculate fitted temp difference values + est_k, est_x0 = popt + y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), vav_df[y_col]) + y_fitted.name = 'y_fitted' + + # sort values + df_fit = pd.concat([vav_df[x_col], y_fitted], axis=1) + df_fit = df_fit.sort_values(by=x_col) + + return df_fit + +def try_limit_dat_fit_model(vav_df, df_fraction): + # calculate fit model + nrows, ncols = vav_df.shape + some_pts = np.random.choice(nrows, int(nrows*df_fraction)) + try: + df_fit = get_fit_line(vav_df.iloc[some_pts]) + except RuntimeError: + try: + df_fit = get_fit_line(vav_df) + except RuntimeError: + print("No regression found") + df_fit = None + return df_fit + +def calc_long_t_diff(vav_df, vlv_open=False): + if vlv_open: + # long-term average when valve is open + df_vlv_close = vav_df[vav_df['vlv_open']] + else: + # long-term average when valve is closed + df_vlv_close = vav_df[~vav_df['vlv_open']] + + long_t = df_vlv_close['temp_diff'].describe() + + return long_t + +def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, df_fit=None, folder='./'): + # plot temperature difference vs valve position + fig, ax = plt.subplots(figsize=(8,4.5)) + ax.scatter(x=vav_df['vlv_po'], y=vav_df['temp_diff'], alpha=1/3, s=10) + ax.set_ylabel('Temperature difference') + ax.set_xlabel('Valve pct opened') + ax.set_title("Valve = {}\nVAV = {}".format(row['vav_vlv'], row['vav']), loc='left') + + if df_fit is not None: + # add fit line + ax.plot(df_fit['vlv_po'], df_fit['y_fitted'], '--', label='fitted', color='red') + + if long_t is not None: + # add long-term temperature diff + ax.axhline(y=long_t, color='green') + + plt_name = "{}-{}-{}".format(row['site'], row['vav'], row['vav_vlv']) + plt.savefig(join(folder, plt_name + '.png')) + +def return_exceedance(vav_df, long_t, th_time=45, window=15): + # find datapoints that exceed long-term temperature difference + min_ts = int(th_time/window) + th_exceed = np.logical_and((vav_df['temp_diff'] >= long_t), ~(vav_df['vlv_open'])) + df_bad = vav_df[th_exceed] + + # only get consecutive timestamps datapoints + ts = pd.Series(df_bad.index) + ts_int = pd.Timedelta(window, unit='min') + cons_ts = ((ts - ts.shift(-1)).abs() <= ts_int) | (ts.diff() <= ts_int) + + if (len(cons_ts) < min_ts) | ~(np.any(cons_ts)): + return None + + #df_bad['cons_ts'] = np.array(cons_ts) + df_bad.loc[:, 'cons_ts'] = np.array(cons_ts) + df_bad.loc[:, 'same'] = df_bad['cons_ts'].astype(int).diff().ne(0).cumsum() + #df_bad['same'] = df_bad['cons_ts'].astype(int).diff().ne(0).cumsum() + + df_cons_ts = df_bad[df_bad['cons_ts']] + + # subset by consecutive times that exceed th_time + lal = df_cons_ts.groupby('same') + grp_exceed = lal['same'].count()[lal['same'].count() >= min_ts].index + + exceeded = [x in grp_exceed for x in df_cons_ts['same']] + bad_vlv = df_cons_ts[exceeded] + + return bad_vlv.drop(columns=['cons_ts']) + + +valve_metadata = fetch_resp.view('valves') +import pdb; pdb.set_trace() + +for idx, row in valve_metadata.iterrows(): + try: + # clean data + vav_df = _clean(row) + + if vav_df.shape[0] == 0: + print("'{}' in site {} has no data! Skipping...".format(row['vav_vlv'], row['site'])) + continue + + # determine if valve datastream has open and closed data + bool_type = vav_df['vlv_open'].value_counts().index + + bad_vlv_val = 5 + + if len(bool_type) < 2: + if bool_type[0]: + # only open valve data + long_to = calc_long_t_diff(vav_df, vlv_open=True) + if long_to['50%'] < bad_vlv_val: + print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vav_vlv'], row['site'])) + _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_to['50%'], folder='./bad_valves') + else: + # only closed valve data + long_tc = calc_long_t_diff(vav_df) + if long_tc['50%'] > bad_vlv_val: + print("Probable passing valve '{}' in site {}".format(row['vav_vlv'], row['site'])) + _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['50%'], folder='./bad_valves') + continue + + # calculate long-term temp diff when valve is closed + bad_klass = [] + long_tc = calc_long_t_diff(vav_df) + long_to = calc_long_t_diff(vav_df, vlv_open=True) + + + # make a simple comparison of between long-term open and long-term closed temp diff + if (long_tc['mean'] + long_tc['std']) > long_to['mean']: + print("Probable passing valve '{}' in site {}\n".format(row['vav_vlv'], row['site'])) + bad_klass.append(True) + + # assume a 0 deg difference at 0% open valve + no_zeros_po = vav_df.copy() + no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 + + # # make a logit regression model based on a threshold value + # # compare long-term average with actual temp diff + # no_zeros_po['sig_diff'] = (no_zeros_po.loc[:, 'temp_diff'] > long_tc['50%']).astype(int) + + # df_fit_sig = get_fit_line(no_zeros_po, x_col='vlv_po', y_col='sig_diff') + # df_fit_sig['y_fitted'] = rescale_fit(df_fit_sig['y_fitted'], no_zeros_po['temp_diff']) + + # est_lt_diff_sig = df_fit_sig[df_fit_sig['vlv_po'] == 0]['y_fitted'].mean() + # bad_vlv = return_exceedance(vav_df, est_lt_diff_sig, th_time=45, window=15) + + # make a logit regression model assuming that closed valves make a zero temp difference + try: + df_fit_nz = get_fit_line(no_zeros_po) + except RuntimeError: + df_fit_nz = None + + # determine estimated long-term difference + if df_fit_nz is not None: + est_lt_diff_nz = df_fit_nz[df_fit_nz['vlv_po'] == 0]['y_fitted'].mean() + else: + est_lt_diff_nz = long_tc['25%'] + + # calculate bad valve instances vs overall dataframe + th_ratio = 0.10 + bad_vlv = return_exceedance(vav_df, est_lt_diff_nz, th_time=45, window=15) + + if bad_vlv is None: + bad_ratio = 0 + else: + bad_ratio = bad_vlv.shape[0]/vav_df.shape[0] + + if bad_ratio > th_ratio: + bad_klass.append(True) + + if len(bad_klass) > 0: + folder = './bad_valves' + print("Probable passing valve '{}' in site {}\n".format(row['vav_vlv'], row['site'])) + if len(bad_klass) > 1: + print("{} percentage of time is leaking!".format(bad_ratio)) + else: + folder = './good_valves' + + _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['25%'], df_fit=df_fit_nz, folder=folder) + + # # get a detailed report of the when valve is malfunctioning + # lal = bad_vlv.groupby('same') + # grps = list(lal.groups.keys()) + # bad_vlv.loc[lal.groups[grps[0]]] + -import pdb; pdb.set_trace() \ No newline at end of file + # # logit fit with limited points + # df_fit = try_limit_dat_fit_model(vav_df, df_fraction=1) + except: + print("Error try to debug") + print(sys.exc_info()[0]) + import pdb; pdb.set_trace() + continue \ No newline at end of file diff --git a/detect_passing_valves/requirements.txt b/detect_passing_valves/requirements.txt index 5465713..e4fa98d 100644 --- a/detect_passing_valves/requirements.txt +++ b/detect_passing_valves/requirements.txt @@ -1,2 +1,5 @@ pymortar==1.0.9a2 -pandas>=1.1 \ No newline at end of file +pandas>=1.1 +numpy>=1.19.0 +matplotlib>=3.0.3 +scipy>=1.5.2 From 77469a3a5e13538ec1d464776df012f1191e4467 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 21 Oct 2020 17:26:14 -0700 Subject: [PATCH 06/83] added func to check existance of container folders --- detect_passing_valves/app.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 9dfe748..d54b9cd 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -2,6 +2,7 @@ import sys import pandas as pd import numpy as np +import os from os.path import join import matplotlib.pyplot as plt @@ -236,6 +237,16 @@ def return_exceedance(vav_df, long_t, th_time=45, window=15): return bad_vlv.drop(columns=['cons_ts']) +def check_folder_exist(folder): + if not os.path.exists(folder): + os.makedirs(folder) + +bad_folder = './bad_valves' +good_folder = './good_valves' + +# check if holding folders exist +check_folder_exist(bad_folder) +check_folder_exist(good_folder) valve_metadata = fetch_resp.view('valves') import pdb; pdb.set_trace() @@ -260,13 +271,13 @@ def return_exceedance(vav_df, long_t, th_time=45, window=15): long_to = calc_long_t_diff(vav_df, vlv_open=True) if long_to['50%'] < bad_vlv_val: print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_to['50%'], folder='./bad_valves') + _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_to['50%'], folder=bad_folder) else: # only closed valve data long_tc = calc_long_t_diff(vav_df) if long_tc['50%'] > bad_vlv_val: print("Probable passing valve '{}' in site {}".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['50%'], folder='./bad_valves') + _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['50%'], folder=bad_folder) continue # calculate long-term temp diff when valve is closed @@ -319,12 +330,12 @@ def return_exceedance(vav_df, long_t, th_time=45, window=15): bad_klass.append(True) if len(bad_klass) > 0: - folder = './bad_valves' + folder = bad_folder print("Probable passing valve '{}' in site {}\n".format(row['vav_vlv'], row['site'])) if len(bad_klass) > 1: print("{} percentage of time is leaking!".format(bad_ratio)) else: - folder = './good_valves' + folder = good_folder _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['25%'], df_fit=df_fit_nz, folder=folder) From c378a2b89124c0b3a0c06c37a0b8e07f892aabef Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 21 Oct 2020 18:33:24 -0700 Subject: [PATCH 07/83] adjusted the color of the plots --- detect_passing_valves/app.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index d54b9cd..2fc0780 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -188,21 +188,30 @@ def calc_long_t_diff(vav_df, vlv_open=False): return long_t -def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, df_fit=None, folder='./'): +def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, df_fit=None, bad_ratio=None, folder='./'): # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) - ax.scatter(x=vav_df['vlv_po'], y=vav_df['temp_diff'], alpha=1/3, s=10) ax.set_ylabel('Temperature difference') ax.set_xlabel('Valve pct opened') ax.set_title("Valve = {}\nVAV = {}".format(row['vav_vlv'], row['vav']), loc='left') + if 'color' in vav_df.columns: + ax.scatter(x=vav_df['vlv_po'], y=vav_df['temp_diff'], color = vav_df['color'], alpha=1/3, s=10) + else: + ax.scatter(x=vav_df['vlv_po'], y=vav_df['temp_diff'], color = '#005ab3', alpha=1/3, s=10) + if df_fit is not None: # add fit line - ax.plot(df_fit['vlv_po'], df_fit['y_fitted'], '--', label='fitted', color='red') + ax.plot(df_fit['vlv_po'], df_fit['y_fitted'], '--', label='fitted', color='#5900b3') if long_t is not None: # add long-term temperature diff - ax.axhline(y=long_t, color='green') + ax.axhline(y=long_t, color='#00b3b3') + + if bad_ratio is not None: + # add ratio where presumably passing valve + y_max = vav_df['temp_diff'].max() + ax.text(.2, 0.95*y_max, "bad ratio={:.1f}%".format(bad_ratio*100)) plt_name = "{}-{}-{}".format(row['site'], row['vav'], row['vav_vlv']) plt.savefig(join(folder, plt_name + '.png')) @@ -337,7 +346,12 @@ def check_folder_exist(folder): else: folder = good_folder - _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['25%'], df_fit=df_fit_nz, folder=folder) + if bad_vlv is not None: + # colorize good and bad points + vav_df['color'] = '#5ab300' + vav_df.loc[bad_vlv.index, 'color'] = '#b3005a' + + _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['25%'], df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) # # get a detailed report of the when valve is malfunctioning # lal = bad_vlv.groupby('same') From 673e833c0845fdf2fe8efe2e0e67c61b659bc5ba Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 21 Oct 2020 21:19:47 -0700 Subject: [PATCH 08/83] fixed bug in bad valve decision --- detect_passing_valves/app.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 2fc0780..539dce1 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -188,7 +188,7 @@ def calc_long_t_diff(vav_df, vlv_open=False): return long_t -def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, df_fit=None, bad_ratio=None, folder='./'): +def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference') @@ -208,10 +208,13 @@ def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, df_fit=None, bad_ratio=N # add long-term temperature diff ax.axhline(y=long_t, color='#00b3b3') + if long_tbad is not None: + ax.axhline(y=long_tbad, color='#ff8cc6') + if bad_ratio is not None: # add ratio where presumably passing valve y_max = vav_df['temp_diff'].max() - ax.text(.2, 0.95*y_max, "bad ratio={:.1f}%".format(bad_ratio*100)) + ax.text(.2, 0.95*y_max, "bad ratio={:.1f}%".format(bad_ratio)) plt_name = "{}-{}-{}".format(row['site'], row['vav'], row['vav_vlv']) plt.savefig(join(folder, plt_name + '.png')) @@ -258,7 +261,10 @@ def check_folder_exist(folder): check_folder_exist(good_folder) valve_metadata = fetch_resp.view('valves') -import pdb; pdb.set_trace() + +# import pdb; pdb.set_trace() +# idx = valve_metadata[valve_metadata['vav'] == 'VAVRM4314'].index[0] +# row = valve_metadata.iloc[idx] for idx, row in valve_metadata.iterrows(): try: @@ -327,15 +333,22 @@ def check_folder_exist(folder): est_lt_diff_nz = long_tc['25%'] # calculate bad valve instances vs overall dataframe - th_ratio = 0.10 + th_ratio = 20 bad_vlv = return_exceedance(vav_df, est_lt_diff_nz, th_time=45, window=15) if bad_vlv is None: bad_ratio = 0 + long_tbad = long_tc['mean'] + else: + bad_ratio = 100*(bad_vlv.shape[0]/vav_df.shape[0]) + long_tbad = bad_vlv['temp_diff'].describe()['mean'] + + if df_fit_nz is not None: + est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() else: - bad_ratio = bad_vlv.shape[0]/vav_df.shape[0] + est_leak = bad_ratio - if bad_ratio > th_ratio: + if est_leak > th_ratio: bad_klass.append(True) if len(bad_klass) > 0: @@ -351,7 +364,7 @@ def check_folder_exist(folder): vav_df['color'] = '#5ab300' vav_df.loc[bad_vlv.index, 'color'] = '#b3005a' - _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['25%'], df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) # # get a detailed report of the when valve is malfunctioning # lal = bad_vlv.groupby('same') From e9e0e13c8b0e03ebecf93c7a92327cb0013e9145 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 27 Oct 2020 11:56:26 -0700 Subject: [PATCH 09/83] improved the formating of the plot --- detect_passing_valves/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 539dce1..06db599 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -191,8 +191,8 @@ def calc_long_t_diff(vav_df, vlv_open=False): def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) - ax.set_ylabel('Temperature difference') - ax.set_xlabel('Valve pct opened') + ax.set_ylabel('Temperature difference [°F]') + ax.set_xlabel('Valve opened [%]') ax.set_title("Valve = {}\nVAV = {}".format(row['vav_vlv'], row['vav']), loc='left') if 'color' in vav_df.columns: @@ -214,7 +214,7 @@ def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, long_tbad=None, df_fit=N if bad_ratio is not None: # add ratio where presumably passing valve y_max = vav_df['temp_diff'].max() - ax.text(.2, 0.95*y_max, "bad ratio={:.1f}%".format(bad_ratio)) + ax.text(.2, 0.95*y_max, "Bad ratio={:.1f}%".format(bad_ratio)) plt_name = "{}-{}-{}".format(row['site'], row['vav'], row['vav_vlv']) plt.savefig(join(folder, plt_name + '.png')) From 6abc00fc62a3b309d1f94cd8e2b82a5b45b75eea Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 28 Oct 2020 18:46:01 -0700 Subject: [PATCH 10/83] changed variable names --- detect_passing_valves/app.py | 136 +++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 53 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 06db599..6978b8e 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -30,34 +30,62 @@ ?vav_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . };""" +ahu_sa_query = """SELECT * +WHERE { + ?ahu_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?ahu_vlv rdf:type ?vlv_type . + ?ahu bf:hasPoint ?ahu_vlv . + ?ahu rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?air_temps rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?ahu bf:hasPoint ?air_temps . + ?air_temps rdf:type ?temp_type . +};""" + +ahu_ra_query = """SELECT * +WHERE { + ?ahu_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?ahu_vlv rdf:type ?vlv_type . + ?ahu bf:hasPoint ?ahu_vlv . + ?ahu rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?air_temps rdf:type/rdfs:subClassOf* brick:Return_Air_Temperature_Sensor . + ?ahu bf:hasPoint ?air_temps . + ?air_temps rdf:type ?temp_type . +};""" + # find sites with these sensors and setpoints -qualify_resp = client.qualify([vav_query]) -if qualify_resp.error != "": - print("ERROR: ", qualify_resp.error) +qualify_vav_resp = client.qualify([vav_query]) +qualify_sa_resp = client.qualify([ahu_sa_query]) +qualify_ra_resp = client.qualify([ahu_ra_query]) + +if qualify_vav_resp.error != "": + print("ERROR: ", qualify_vav_resp.error) sys.exit(1) -elif len(qualify_resp.sites) == 0: +elif len(qualify_vav_resp.sites) == 0: print("NO SITES RETURNED") sys.exit(0) -print("running on {0} sites".format(len(qualify_resp.sites))) +vav_sites = qualify_vav_resp.sites +ahu_sites = np.intersect1d(qualify_sa_resp.sites, qualify_ra_resp.sites) +tlt_sites = np.union1d(vav_sites, ahu_sites) +print("running on {0} sites".format(len(tlt_sites))) # build the fetch request -request = pymortar.FetchRequest( - sites=qualify_resp.sites, +vav_request = pymortar.FetchRequest( + sites=qualify_vav_resp.sites, views=[ pymortar.View( - name="valves", + name="vav_temps", definition=vav_query, ), ], dataFrames=[ pymortar.DataFrame( - name="valve", + name="vav_valve", aggregation=pymortar.MEAN, window="15m", timeseries=[ pymortar.Timeseries( - view="valves", + view="vav_temps", dataVars=["?vav_vlv"], ) ] @@ -68,7 +96,7 @@ window="15m", timeseries=[ pymortar.Timeseries( - view="valves", + view="vav_temps", dataVars=["?vav_supply"], ) ] @@ -79,7 +107,7 @@ window="15m", timeseries=[ pymortar.Timeseries( - view="valves", + view="vav_temps", dataVars=["?ahu_supply"], ) ] @@ -145,60 +173,60 @@ def rescale_fit(scaled_x, temp_diff): def sigmoid(x, k, x0): return 1.0 / (1 + np.exp(-k * (x - x0))) -def get_fit_line(vav_df, x_col='vlv_po', y_col='temp_diff'): +def get_fit_line(vlv_df, x_col='vlv_po', y_col='temp_diff'): # fit the curve - scaled_pos = scale_0to1(vav_df[x_col]) - scaled_t = scale_0to1(vav_df[y_col]) + scaled_pos = scale_0to1(vlv_df[x_col]) + scaled_t = scale_0to1(vlv_df[y_col]) popt, pcov = curve_fit(sigmoid, scaled_pos, scaled_t) # calculate fitted temp difference values est_k, est_x0 = popt - y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), vav_df[y_col]) + y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), vlv_df[y_col]) y_fitted.name = 'y_fitted' # sort values - df_fit = pd.concat([vav_df[x_col], y_fitted], axis=1) + df_fit = pd.concat([vlv_df[x_col], y_fitted], axis=1) df_fit = df_fit.sort_values(by=x_col) return df_fit -def try_limit_dat_fit_model(vav_df, df_fraction): +def try_limit_dat_fit_model(vlv_df, df_fraction): # calculate fit model - nrows, ncols = vav_df.shape + nrows, ncols = vlv_df.shape some_pts = np.random.choice(nrows, int(nrows*df_fraction)) try: - df_fit = get_fit_line(vav_df.iloc[some_pts]) + df_fit = get_fit_line(vlv_df.iloc[some_pts]) except RuntimeError: try: - df_fit = get_fit_line(vav_df) + df_fit = get_fit_line(vlv_df) except RuntimeError: print("No regression found") df_fit = None return df_fit -def calc_long_t_diff(vav_df, vlv_open=False): +def calc_long_t_diff(vlv_df, vlv_open=False): if vlv_open: # long-term average when valve is open - df_vlv_close = vav_df[vav_df['vlv_open']] + df_vlv_close = vlv_df[vlv_df['vlv_open']] else: # long-term average when valve is closed - df_vlv_close = vav_df[~vav_df['vlv_open']] + df_vlv_close = vlv_df[~vlv_df['vlv_open']] long_t = df_vlv_close['temp_diff'].describe() return long_t -def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): +def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Valve opened [%]') ax.set_title("Valve = {}\nVAV = {}".format(row['vav_vlv'], row['vav']), loc='left') - if 'color' in vav_df.columns: - ax.scatter(x=vav_df['vlv_po'], y=vav_df['temp_diff'], color = vav_df['color'], alpha=1/3, s=10) + if 'color' in vlv_df.columns: + ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = vlv_df['color'], alpha=1/3, s=10) else: - ax.scatter(x=vav_df['vlv_po'], y=vav_df['temp_diff'], color = '#005ab3', alpha=1/3, s=10) + ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = '#005ab3', alpha=1/3, s=10) if df_fit is not None: # add fit line @@ -213,17 +241,17 @@ def _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=None, long_tbad=None, df_fit=N if bad_ratio is not None: # add ratio where presumably passing valve - y_max = vav_df['temp_diff'].max() + y_max = vlv_df['temp_diff'].max() ax.text(.2, 0.95*y_max, "Bad ratio={:.1f}%".format(bad_ratio)) plt_name = "{}-{}-{}".format(row['site'], row['vav'], row['vav_vlv']) plt.savefig(join(folder, plt_name + '.png')) -def return_exceedance(vav_df, long_t, th_time=45, window=15): +def return_exceedance(vlv_df, long_t, th_time=45, window=15): # find datapoints that exceed long-term temperature difference min_ts = int(th_time/window) - th_exceed = np.logical_and((vav_df['temp_diff'] >= long_t), ~(vav_df['vlv_open'])) - df_bad = vav_df[th_exceed] + th_exceed = np.logical_and((vlv_df['temp_diff'] >= long_t), ~(vlv_df['vlv_open'])) + df_bad = vlv_df[th_exceed] # only get consecutive timestamps datapoints ts = pd.Series(df_bad.index) @@ -260,45 +288,47 @@ def check_folder_exist(folder): check_folder_exist(bad_folder) check_folder_exist(good_folder) -valve_metadata = fetch_resp.view('valves') +vav_metadata = fetch_resp_vav.view('vav_temps') # import pdb; pdb.set_trace() -# idx = valve_metadata[valve_metadata['vav'] == 'VAVRM4314'].index[0] -# row = valve_metadata.iloc[idx] +# idx = vav_metadata[vav_metadata['vav'] == 'VAVRM4314'].index[0] +# row = vav_metadata.iloc[idx] + +import pdb; pdb.set_trace() -for idx, row in valve_metadata.iterrows(): +for idx, row in vav_metadata.iterrows(): try: # clean data - vav_df = _clean(row) + vlv_df = _clean_vav(row) - if vav_df.shape[0] == 0: + if vlv_df.shape[0] == 0: print("'{}' in site {} has no data! Skipping...".format(row['vav_vlv'], row['site'])) continue # determine if valve datastream has open and closed data - bool_type = vav_df['vlv_open'].value_counts().index + bool_type = vlv_df['vlv_open'].value_counts().index bad_vlv_val = 5 if len(bool_type) < 2: if bool_type[0]: # only open valve data - long_to = calc_long_t_diff(vav_df, vlv_open=True) + long_to = calc_long_t_diff(vlv_df, vlv_open=True) if long_to['50%'] < bad_vlv_val: print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_to['50%'], folder=bad_folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=bad_folder) else: # only closed valve data - long_tc = calc_long_t_diff(vav_df) + long_tc = calc_long_t_diff(vlv_df) if long_tc['50%'] > bad_vlv_val: print("Probable passing valve '{}' in site {}".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['50%'], folder=bad_folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=bad_folder) continue # calculate long-term temp diff when valve is closed bad_klass = [] - long_tc = calc_long_t_diff(vav_df) - long_to = calc_long_t_diff(vav_df, vlv_open=True) + long_tc = calc_long_t_diff(vlv_df) + long_to = calc_long_t_diff(vlv_df, vlv_open=True) # make a simple comparison of between long-term open and long-term closed temp diff @@ -307,7 +337,7 @@ def check_folder_exist(folder): bad_klass.append(True) # assume a 0 deg difference at 0% open valve - no_zeros_po = vav_df.copy() + no_zeros_po = vlv_df.copy() no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 # # make a logit regression model based on a threshold value @@ -318,7 +348,7 @@ def check_folder_exist(folder): # df_fit_sig['y_fitted'] = rescale_fit(df_fit_sig['y_fitted'], no_zeros_po['temp_diff']) # est_lt_diff_sig = df_fit_sig[df_fit_sig['vlv_po'] == 0]['y_fitted'].mean() - # bad_vlv = return_exceedance(vav_df, est_lt_diff_sig, th_time=45, window=15) + # bad_vlv = return_exceedance(vlv_df, est_lt_diff_sig, th_time=45, window=15) # make a logit regression model assuming that closed valves make a zero temp difference try: @@ -334,13 +364,13 @@ def check_folder_exist(folder): # calculate bad valve instances vs overall dataframe th_ratio = 20 - bad_vlv = return_exceedance(vav_df, est_lt_diff_nz, th_time=45, window=15) + bad_vlv = return_exceedance(vlv_df, est_lt_diff_nz, th_time=45, window=15) if bad_vlv is None: bad_ratio = 0 long_tbad = long_tc['mean'] else: - bad_ratio = 100*(bad_vlv.shape[0]/vav_df.shape[0]) + bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) long_tbad = bad_vlv['temp_diff'].describe()['mean'] if df_fit_nz is not None: @@ -361,10 +391,10 @@ def check_folder_exist(folder): if bad_vlv is not None: # colorize good and bad points - vav_df['color'] = '#5ab300' - vav_df.loc[bad_vlv.index, 'color'] = '#b3005a' + vlv_df['color'] = '#5ab300' + vlv_df.loc[bad_vlv.index, 'color'] = '#b3005a' - _make_tdiff_vs_vlvpo_plot(vav_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) # # get a detailed report of the when valve is malfunctioning # lal = bad_vlv.groupby('same') @@ -373,7 +403,7 @@ def check_folder_exist(folder): # # logit fit with limited points - # df_fit = try_limit_dat_fit_model(vav_df, df_fraction=1) + # df_fit = try_limit_dat_fit_model(vlv_df, df_fraction=1) except: print("Error try to debug") print(sys.exc_info()[0]) From ffcd9fe1b8b703cbff23469474caf404e3d1ab91 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 29 Oct 2020 10:31:33 -0700 Subject: [PATCH 11/83] added functionality to download AHU valve data --- detect_passing_valves/app.py | 388 +++++++++++++++++++++++------------ 1 file changed, 254 insertions(+), 134 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 6978b8e..5ca93ec 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -12,43 +12,42 @@ eval_start_time = "2018-01-01T00:00:00Z" eval_end_time = "2018-06-30T00:00:00Z" - client = pymortar.Client() # define query to return valves # returns supply air temps from ahu and vav and vav valve vav_query = """SELECT * WHERE { - ?vav rdf:type/rdfs:subClassOf? brick:VAV . - ?vav bf:isFedBy+ ?ahu . - ?vav_vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?ahu_supply . - ?vav bf:hasPoint ?vav_supply . - ?ahu_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?vav_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?vav bf:hasPoint ?vav_vlv . - ?vav_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?equip rdf:type/rdfs:subClassOf? brick:VAV . + ?equip bf:isFedBy+ ?ahu . + ?vlv rdf:type ?vlv_type . + ?ahu bf:hasPoint ?ahu_supply . + ?equip bf:hasPoint ?vav_supply . + ?ahu_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?vav_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?equip bf:hasPoint ?vlv . + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . };""" ahu_sa_query = """SELECT * WHERE { - ?ahu_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - ?ahu_vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?ahu_vlv . - ?ahu rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?vlv rdf:type ?vlv_type . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?ahu bf:hasPoint ?air_temps . + ?equip bf:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" ahu_ra_query = """SELECT * WHERE { - ?ahu_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - ?ahu_vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?ahu_vlv . - ?ahu rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?vlv rdf:type ?vlv_type . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Return_Air_Temperature_Sensor . - ?ahu bf:hasPoint ?air_temps . + ?equip bf:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" @@ -86,7 +85,7 @@ timeseries=[ pymortar.Timeseries( view="vav_temps", - dataVars=["?vav_vlv"], + dataVars=["?vlv"], ) ] ), @@ -120,30 +119,110 @@ ) -# call the fetch api -fetch_resp = client.fetch(request) -print(fetch_resp) -print(fetch_resp.view('valves')) +# build the fetch request +ahu_request = pymortar.FetchRequest( + sites=ahu_sites, + views=[ + pymortar.View( + name="ahu_sa_temp", + definition=ahu_sa_query, + ), + pymortar.View( + name="ahu_ra_temp", + definition=ahu_ra_query, + ), + ], + dataFrames=[ + pymortar.DataFrame( + name="ahu_valve", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="ahu_sa_temp", + dataVars=["?vlv"], + ) + ] + ), + pymortar.DataFrame( + name="ahu_sa_temp", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="ahu_sa_temp", + dataVars=["?air_temps"], + ) + ] + ), + pymortar.DataFrame( + name="ahu_ra_temp", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="ahu_ra_temp", + dataVars=["?air_temps"], + ) + ] + ), + ], + time=pymortar.TimeParams( + start=eval_start_time, + end=eval_end_time, + ) +) + +def _clean_ahu_view(fetch_resp_ahu): + # supply air temp metadata + ahu_sa = fetch_resp_ahu.view('ahu_sa_temp') + ahu_sa = ahu_sa.rename(columns={'air_temps': 'dnstream_ta', 'temp_type': 'supply type', 'air_temps_uuid': 'dnstream_ta uuid'}) + + # return air temp metadata + ahu_ra = fetch_resp_ahu.view('ahu_ra_temp') + ahu_ra = ahu_ra.rename(columns={'air_temps': 'upstream_ta', 'temp_type': 'return type', 'air_temps_uuid': 'upstream_ta uuid'}) + + # join supply and return air temperature data into on dataset + ahu_metadata = ahu_sa.merge(ahu_ra, on=['vlv', 'equip', 'vlv_type', 'site'], how='inner') + # delete cooling valve commands + heat_vlv = [x not in ['Cooling_Valve_Command'] for x in ahu_metadata['vlv_type']] + + return ahu_metadata[heat_vlv] + +# call the fetch api for VAV data +fetch_resp_vav = client.fetch(vav_request) + +print("-----Dataframe for VAV valves-----") +print(fetch_resp_vav) +print(fetch_resp_vav.view('vav_temps')) + +# call the fetch api for AHU data +fetch_resp_ahu = client.fetch(ahu_request) +ahu_metadata = _clean_ahu_view(fetch_resp_ahu) + +print("-----Dataframe for AHU valves-----") +print(fetch_resp_ahu) +print(ahu_metadata) # print the different types of valves in the data #print(fetch_resp.view('valves').groupby(['vlv_subclass']).count()) -def _clean(row): +def _clean_vav(row): # combine data points in one dataframe - vav_sa = fetch_resp['vav_temp'][row['vav_supply_uuid']] - ahu_sa = fetch_resp['ahu_temp'][row['ahu_supply_uuid']] - vlv_po = fetch_resp['valve'][row['vav_vlv_uuid']] + vav_sa = fetch_resp_vav['vav_temp'][row['vav_supply_uuid']] + ahu_sa = fetch_resp_vav['ahu_temp'][row['ahu_supply_uuid']] + vlv_po = fetch_resp_vav['vav_valve'][row['vlv_uuid']] vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) - vav_df.columns = ['ahu_sa', 'vav_sa', 'vlv_po'] + vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] # identify when valve is open vav_df['vlv_open'] = vav_df['vlv_po'] > 0 - # calculate temperature difference between ahu and vav supply air - vav_df['temp_diff'] = vav_df['vav_sa'] - vav_df['ahu_sa'] + # calculate temperature difference between downstream and upstream air + vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] # drop na vav_df = vav_df.dropna() @@ -153,6 +232,29 @@ def _clean(row): return vav_df +def _clean_ahu(row): + dnstream = fetch_resp_ahu['ahu_sa_temp'][row['dnstream_ta uuid']] + upstream = fetch_resp_ahu['ahu_ra_temp'][row['upstream_ta uuid']] + + vlv_po = fetch_resp_ahu['ahu_valve'][row['vlv_uuid']] + + ahu_df = pd.concat([upstream, dnstream, vlv_po], axis=1) + ahu_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] + + # identify when valve is open + ahu_df['vlv_open'] = ahu_df['vlv_po'] > 0 + + # calculate temperature difference between downstream and upstream air + ahu_df['temp_diff'] = ahu_df['dnstream_ta'] - ahu_df['upstream_ta'] + + # drop na + ahu_df = ahu_df.dropna() + + # drop values where vav supply air is less than ahu supply air + #ahu_df = ahu_df[ahu_df['temp_diff'] >= 0] + + return ahu_df + def scale_0to1(temp_diff): max_t = temp_diff.max() @@ -221,7 +323,7 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Valve opened [%]') - ax.set_title("Valve = {}\nVAV = {}".format(row['vav_vlv'], row['vav']), loc='left') + ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') if 'color' in vlv_df.columns: ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = vlv_df['color'], alpha=1/3, s=10) @@ -244,8 +346,9 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N y_max = vlv_df['temp_diff'].max() ax.text(.2, 0.95*y_max, "Bad ratio={:.1f}%".format(bad_ratio)) - plt_name = "{}-{}-{}".format(row['site'], row['vav'], row['vav_vlv']) + plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) plt.savefig(join(folder, plt_name + '.png')) + plt.close() def return_exceedance(vlv_df, long_t, th_time=45, window=15): # find datapoints that exceed long-term temperature difference @@ -281,131 +384,148 @@ def check_folder_exist(folder): if not os.path.exists(folder): os.makedirs(folder) -bad_folder = './bad_valves' -good_folder = './good_valves' - -# check if holding folders exist -check_folder_exist(bad_folder) -check_folder_exist(good_folder) - -vav_metadata = fetch_resp_vav.view('vav_temps') +def _analyze_vav(vlv_df, row, bad_folder = './bad_valves', good_folder = './good_valves'): -# import pdb; pdb.set_trace() -# idx = vav_metadata[vav_metadata['vav'] == 'VAVRM4314'].index[0] -# row = vav_metadata.iloc[idx] + # check if holding folders exist + check_folder_exist(bad_folder) + check_folder_exist(good_folder) -import pdb; pdb.set_trace() + if vlv_df.shape[0] == 0: + print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) + return -for idx, row in vav_metadata.iterrows(): - try: - # clean data - vlv_df = _clean_vav(row) + # determine if valve datastream has open and closed data + bool_type = vlv_df['vlv_open'].value_counts().index - if vlv_df.shape[0] == 0: - print("'{}' in site {} has no data! Skipping...".format(row['vav_vlv'], row['site'])) - continue + bad_vlv_val = 5 - # determine if valve datastream has open and closed data - bool_type = vlv_df['vlv_open'].value_counts().index - - bad_vlv_val = 5 - - if len(bool_type) < 2: - if bool_type[0]: - # only open valve data - long_to = calc_long_t_diff(vlv_df, vlv_open=True) - if long_to['50%'] < bad_vlv_val: - print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=bad_folder) - else: - # only closed valve data - long_tc = calc_long_t_diff(vlv_df) - if long_tc['50%'] > bad_vlv_val: - print("Probable passing valve '{}' in site {}".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=bad_folder) - continue + if len(bool_type) < 2: + if bool_type[0]: + # only open valve data + long_to = calc_long_t_diff(vlv_df, vlv_open=True) + if long_to['50%'] < bad_vlv_val: + print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=bad_folder) + else: + # only closed valve data + long_tc = calc_long_t_diff(vlv_df) + if long_tc['50%'] > bad_vlv_val: + print("Probable passing valve '{}' in site {}".format(row['vlv'], row['site'])) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=bad_folder) + return - # calculate long-term temp diff when valve is closed - bad_klass = [] - long_tc = calc_long_t_diff(vlv_df) - long_to = calc_long_t_diff(vlv_df, vlv_open=True) + # calculate long-term temp diff when valve is closed + bad_klass = [] + long_tc = calc_long_t_diff(vlv_df) + long_to = calc_long_t_diff(vlv_df, vlv_open=True) - # make a simple comparison of between long-term open and long-term closed temp diff - if (long_tc['mean'] + long_tc['std']) > long_to['mean']: - print("Probable passing valve '{}' in site {}\n".format(row['vav_vlv'], row['site'])) - bad_klass.append(True) + # make a simple comparison of between long-term open and long-term closed temp diff + if (long_tc['mean'] + long_tc['std']) > long_to['mean']: + print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) + bad_klass.append(True) - # assume a 0 deg difference at 0% open valve - no_zeros_po = vlv_df.copy() - no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 + # assume a 0 deg difference at 0% open valve + no_zeros_po = vlv_df.copy() + no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 - # # make a logit regression model based on a threshold value - # # compare long-term average with actual temp diff - # no_zeros_po['sig_diff'] = (no_zeros_po.loc[:, 'temp_diff'] > long_tc['50%']).astype(int) + # # make a logit regression model based on a threshold value + # # compare long-term average with actual temp diff + # no_zeros_po['sig_diff'] = (no_zeros_po.loc[:, 'temp_diff'] > long_tc['50%']).astype(int) - # df_fit_sig = get_fit_line(no_zeros_po, x_col='vlv_po', y_col='sig_diff') - # df_fit_sig['y_fitted'] = rescale_fit(df_fit_sig['y_fitted'], no_zeros_po['temp_diff']) + # df_fit_sig = get_fit_line(no_zeros_po, x_col='vlv_po', y_col='sig_diff') + # df_fit_sig['y_fitted'] = rescale_fit(df_fit_sig['y_fitted'], no_zeros_po['temp_diff']) - # est_lt_diff_sig = df_fit_sig[df_fit_sig['vlv_po'] == 0]['y_fitted'].mean() - # bad_vlv = return_exceedance(vlv_df, est_lt_diff_sig, th_time=45, window=15) + # est_lt_diff_sig = df_fit_sig[df_fit_sig['vlv_po'] == 0]['y_fitted'].mean() + # bad_vlv = return_exceedance(vlv_df, est_lt_diff_sig, th_time=45, window=15) - # make a logit regression model assuming that closed valves make a zero temp difference - try: - df_fit_nz = get_fit_line(no_zeros_po) - except RuntimeError: - df_fit_nz = None + # make a logit regression model assuming that closed valves make a zero temp difference + try: + df_fit_nz = get_fit_line(no_zeros_po) + except RuntimeError: + df_fit_nz = None - # determine estimated long-term difference - if df_fit_nz is not None: - est_lt_diff_nz = df_fit_nz[df_fit_nz['vlv_po'] == 0]['y_fitted'].mean() - else: - est_lt_diff_nz = long_tc['25%'] + # determine estimated long-term difference + if df_fit_nz is not None: + est_lt_diff_nz = df_fit_nz[df_fit_nz['vlv_po'] == 0]['y_fitted'].mean() + else: + est_lt_diff_nz = long_tc['25%'] - # calculate bad valve instances vs overall dataframe - th_ratio = 20 - bad_vlv = return_exceedance(vlv_df, est_lt_diff_nz, th_time=45, window=15) + # calculate bad valve instances vs overall dataframe + th_ratio = 20 + bad_vlv = return_exceedance(vlv_df, est_lt_diff_nz, th_time=45, window=15) - if bad_vlv is None: - bad_ratio = 0 - long_tbad = long_tc['mean'] - else: - bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) - long_tbad = bad_vlv['temp_diff'].describe()['mean'] + if bad_vlv is None: + bad_ratio = 0 + long_tbad = long_tc['mean'] + else: + bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) + long_tbad = bad_vlv['temp_diff'].describe()['mean'] - if df_fit_nz is not None: - est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() - else: - est_leak = bad_ratio + if df_fit_nz is not None: + est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() + else: + est_leak = bad_ratio - if est_leak > th_ratio: - bad_klass.append(True) + if est_leak > th_ratio: + bad_klass.append(True) - if len(bad_klass) > 0: - folder = bad_folder - print("Probable passing valve '{}' in site {}\n".format(row['vav_vlv'], row['site'])) + if len(bad_klass) > 0: + folder = bad_folder + if bad_ratio > 5: + print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) if len(bad_klass) > 1: print("{} percentage of time is leaking!".format(bad_ratio)) else: folder = good_folder + else: + folder = good_folder + + if bad_vlv is not None: + # colorize good and bad points + vlv_df['color'] = '#5ab300' + vlv_df.loc[bad_vlv.index, 'color'] = '#b3005a' + + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + + # # get a detailed report of the when valve is malfunctioning + # lal = bad_vlv.groupby('same') + # grps = list(lal.groups.keys()) + # bad_vlv.loc[lal.groups[grps[0]]] + - if bad_vlv is not None: - # colorize good and bad points - vlv_df['color'] = '#5ab300' - vlv_df.loc[bad_vlv.index, 'color'] = '#b3005a' + # # logit fit with limited points + # df_fit = try_limit_dat_fit_model(vlv_df, df_fraction=1) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) +def _analyze_ahu(vlv_df, row): + _make_tdiff_vs_vlvpo_plot(vlv_df, row, folder='./') - # # get a detailed report of the when valve is malfunctioning - # lal = bad_vlv.groupby('same') - # grps = list(lal.groups.keys()) - # bad_vlv.loc[lal.groups[grps[0]]] +def analyze(metadata, clean_func, analyze_func): + # analyze valves + for idx, row in metadata.iterrows(): + try: + # clean data + vlv_df = clean_func(row) + + # analyze for passing valves + analyze_func(vlv_df, row) + + except: + print("Error try to debug") + print(sys.exc_info()[0]) + import pdb; pdb.set_trace() + continue + +vav_metadata = fetch_resp_vav.view('vav_temps') +# import pdb; pdb.set_trace() +# idx = vav_metadata[vav_metadata['vav'] == 'VAVRM4314'].index[0] +# row = vav_metadata.iloc[idx] - # # logit fit with limited points - # df_fit = try_limit_dat_fit_model(vlv_df, df_fraction=1) - except: - print("Error try to debug") - print(sys.exc_info()[0]) - import pdb; pdb.set_trace() - continue \ No newline at end of file +# analyze VAV valves +analyze(vav_metadata, _clean_vav, _analyze_vav) + +# analyze AHU valves +analyze(ahu_metadata, _clean_ahu, _analyze_ahu) + +import pdb; pdb.set_trace() From 9a57ced017450e6e2dd9e9fa9cafec3a714ebe41 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 29 Oct 2020 10:31:33 -0700 Subject: [PATCH 12/83] added functionality to download AHU valve data --- detect_passing_valves/app.py | 386 +++++++++++++++++++++++------------ 1 file changed, 252 insertions(+), 134 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 6978b8e..a124899 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -12,43 +12,42 @@ eval_start_time = "2018-01-01T00:00:00Z" eval_end_time = "2018-06-30T00:00:00Z" - client = pymortar.Client() # define query to return valves # returns supply air temps from ahu and vav and vav valve vav_query = """SELECT * WHERE { - ?vav rdf:type/rdfs:subClassOf? brick:VAV . - ?vav bf:isFedBy+ ?ahu . - ?vav_vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?ahu_supply . - ?vav bf:hasPoint ?vav_supply . - ?ahu_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?vav_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?vav bf:hasPoint ?vav_vlv . - ?vav_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?equip rdf:type/rdfs:subClassOf? brick:VAV . + ?equip bf:isFedBy+ ?ahu . + ?vlv rdf:type ?vlv_type . + ?ahu bf:hasPoint ?ahu_supply . + ?equip bf:hasPoint ?vav_supply . + ?ahu_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?vav_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?equip bf:hasPoint ?vlv . + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . };""" ahu_sa_query = """SELECT * WHERE { - ?ahu_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - ?ahu_vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?ahu_vlv . - ?ahu rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?vlv rdf:type ?vlv_type . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?ahu bf:hasPoint ?air_temps . + ?equip bf:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" ahu_ra_query = """SELECT * WHERE { - ?ahu_vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - ?ahu_vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?ahu_vlv . - ?ahu rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?vlv rdf:type ?vlv_type . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Return_Air_Temperature_Sensor . - ?ahu bf:hasPoint ?air_temps . + ?equip bf:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" @@ -86,7 +85,7 @@ timeseries=[ pymortar.Timeseries( view="vav_temps", - dataVars=["?vav_vlv"], + dataVars=["?vlv"], ) ] ), @@ -120,30 +119,110 @@ ) -# call the fetch api -fetch_resp = client.fetch(request) -print(fetch_resp) -print(fetch_resp.view('valves')) +# build the fetch request +ahu_request = pymortar.FetchRequest( + sites=ahu_sites, + views=[ + pymortar.View( + name="ahu_sa_temp", + definition=ahu_sa_query, + ), + pymortar.View( + name="ahu_ra_temp", + definition=ahu_ra_query, + ), + ], + dataFrames=[ + pymortar.DataFrame( + name="ahu_valve", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="ahu_sa_temp", + dataVars=["?vlv"], + ) + ] + ), + pymortar.DataFrame( + name="ahu_sa_temp", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="ahu_sa_temp", + dataVars=["?air_temps"], + ) + ] + ), + pymortar.DataFrame( + name="ahu_ra_temp", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="ahu_ra_temp", + dataVars=["?air_temps"], + ) + ] + ), + ], + time=pymortar.TimeParams( + start=eval_start_time, + end=eval_end_time, + ) +) + +def _clean_ahu_view(fetch_resp_ahu): + # supply air temp metadata + ahu_sa = fetch_resp_ahu.view('ahu_sa_temp') + ahu_sa = ahu_sa.rename(columns={'air_temps': 'dnstream_ta', 'temp_type': 'supply type', 'air_temps_uuid': 'dnstream_ta uuid'}) + + # return air temp metadata + ahu_ra = fetch_resp_ahu.view('ahu_ra_temp') + ahu_ra = ahu_ra.rename(columns={'air_temps': 'upstream_ta', 'temp_type': 'return type', 'air_temps_uuid': 'upstream_ta uuid'}) + + # join supply and return air temperature data into on dataset + ahu_metadata = ahu_sa.merge(ahu_ra, on=['vlv', 'equip', 'vlv_type', 'site'], how='inner') + + # delete cooling valve commands + heat_vlv = [x not in ['Cooling_Valve_Command'] for x in ahu_metadata['vlv_type']] + + return ahu_metadata[heat_vlv] + +# call the fetch api for VAV data +fetch_resp_vav = client.fetch(vav_request) + +print("-----Dataframe for VAV valves-----") +print(fetch_resp_vav) +print(fetch_resp_vav.view('vav_temps')) + +# call the fetch api for AHU data +fetch_resp_ahu = client.fetch(ahu_request) +ahu_metadata = _clean_ahu_view(fetch_resp_ahu) +print("-----Dataframe for AHU valves-----") +print(fetch_resp_ahu) +print(ahu_metadata) # print the different types of valves in the data #print(fetch_resp.view('valves').groupby(['vlv_subclass']).count()) -def _clean(row): +def _clean_vav(row): # combine data points in one dataframe - vav_sa = fetch_resp['vav_temp'][row['vav_supply_uuid']] - ahu_sa = fetch_resp['ahu_temp'][row['ahu_supply_uuid']] - vlv_po = fetch_resp['valve'][row['vav_vlv_uuid']] + vav_sa = fetch_resp_vav['vav_temp'][row['vav_supply_uuid']] + ahu_sa = fetch_resp_vav['ahu_temp'][row['ahu_supply_uuid']] + vlv_po = fetch_resp_vav['vav_valve'][row['vlv_uuid']] vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) - vav_df.columns = ['ahu_sa', 'vav_sa', 'vlv_po'] + vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] # identify when valve is open vav_df['vlv_open'] = vav_df['vlv_po'] > 0 - # calculate temperature difference between ahu and vav supply air - vav_df['temp_diff'] = vav_df['vav_sa'] - vav_df['ahu_sa'] + # calculate temperature difference between downstream and upstream air + vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] # drop na vav_df = vav_df.dropna() @@ -153,6 +232,29 @@ def _clean(row): return vav_df +def _clean_ahu(row): + dnstream = fetch_resp_ahu['ahu_sa_temp'][row['dnstream_ta uuid']] + upstream = fetch_resp_ahu['ahu_ra_temp'][row['upstream_ta uuid']] + + vlv_po = fetch_resp_ahu['ahu_valve'][row['vlv_uuid']] + + ahu_df = pd.concat([upstream, dnstream, vlv_po], axis=1) + ahu_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] + + # identify when valve is open + ahu_df['vlv_open'] = ahu_df['vlv_po'] > 0 + + # calculate temperature difference between downstream and upstream air + ahu_df['temp_diff'] = ahu_df['dnstream_ta'] - ahu_df['upstream_ta'] + + # drop na + ahu_df = ahu_df.dropna() + + # drop values where vav supply air is less than ahu supply air + #ahu_df = ahu_df[ahu_df['temp_diff'] >= 0] + + return ahu_df + def scale_0to1(temp_diff): max_t = temp_diff.max() @@ -221,7 +323,7 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Valve opened [%]') - ax.set_title("Valve = {}\nVAV = {}".format(row['vav_vlv'], row['vav']), loc='left') + ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') if 'color' in vlv_df.columns: ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = vlv_df['color'], alpha=1/3, s=10) @@ -244,8 +346,9 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N y_max = vlv_df['temp_diff'].max() ax.text(.2, 0.95*y_max, "Bad ratio={:.1f}%".format(bad_ratio)) - plt_name = "{}-{}-{}".format(row['site'], row['vav'], row['vav_vlv']) + plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) plt.savefig(join(folder, plt_name + '.png')) + plt.close() def return_exceedance(vlv_df, long_t, th_time=45, window=15): # find datapoints that exceed long-term temperature difference @@ -281,131 +384,146 @@ def check_folder_exist(folder): if not os.path.exists(folder): os.makedirs(folder) -bad_folder = './bad_valves' -good_folder = './good_valves' +def _analyze_vav(vlv_df, row, bad_folder = './bad_valves', good_folder = './good_valves'): -# check if holding folders exist -check_folder_exist(bad_folder) -check_folder_exist(good_folder) + # check if holding folders exist + check_folder_exist(bad_folder) + check_folder_exist(good_folder) -vav_metadata = fetch_resp_vav.view('vav_temps') + if vlv_df.shape[0] == 0: + print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) + return -# import pdb; pdb.set_trace() -# idx = vav_metadata[vav_metadata['vav'] == 'VAVRM4314'].index[0] -# row = vav_metadata.iloc[idx] + # determine if valve datastream has open and closed data + bool_type = vlv_df['vlv_open'].value_counts().index -import pdb; pdb.set_trace() + bad_vlv_val = 5 -for idx, row in vav_metadata.iterrows(): - try: - # clean data - vlv_df = _clean_vav(row) - - if vlv_df.shape[0] == 0: - print("'{}' in site {} has no data! Skipping...".format(row['vav_vlv'], row['site'])) - continue - - # determine if valve datastream has open and closed data - bool_type = vlv_df['vlv_open'].value_counts().index - - bad_vlv_val = 5 - - if len(bool_type) < 2: - if bool_type[0]: - # only open valve data - long_to = calc_long_t_diff(vlv_df, vlv_open=True) - if long_to['50%'] < bad_vlv_val: - print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=bad_folder) - else: - # only closed valve data - long_tc = calc_long_t_diff(vlv_df) - if long_tc['50%'] > bad_vlv_val: - print("Probable passing valve '{}' in site {}".format(row['vav_vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=bad_folder) - continue + if len(bool_type) < 2: + if bool_type[0]: + # only open valve data + long_to = calc_long_t_diff(vlv_df, vlv_open=True) + if long_to['50%'] < bad_vlv_val: + print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=bad_folder) + else: + # only closed valve data + long_tc = calc_long_t_diff(vlv_df) + if long_tc['50%'] > bad_vlv_val: + print("Probable passing valve '{}' in site {}".format(row['vlv'], row['site'])) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=bad_folder) + return - # calculate long-term temp diff when valve is closed - bad_klass = [] - long_tc = calc_long_t_diff(vlv_df) - long_to = calc_long_t_diff(vlv_df, vlv_open=True) + # calculate long-term temp diff when valve is closed + bad_klass = [] + long_tc = calc_long_t_diff(vlv_df) + long_to = calc_long_t_diff(vlv_df, vlv_open=True) - # make a simple comparison of between long-term open and long-term closed temp diff - if (long_tc['mean'] + long_tc['std']) > long_to['mean']: - print("Probable passing valve '{}' in site {}\n".format(row['vav_vlv'], row['site'])) - bad_klass.append(True) + # make a simple comparison of between long-term open and long-term closed temp diff + if (long_tc['mean'] + long_tc['std']) > long_to['mean']: + print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) + bad_klass.append(True) - # assume a 0 deg difference at 0% open valve - no_zeros_po = vlv_df.copy() - no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 + # assume a 0 deg difference at 0% open valve + no_zeros_po = vlv_df.copy() + no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 - # # make a logit regression model based on a threshold value - # # compare long-term average with actual temp diff - # no_zeros_po['sig_diff'] = (no_zeros_po.loc[:, 'temp_diff'] > long_tc['50%']).astype(int) + # # make a logit regression model based on a threshold value + # # compare long-term average with actual temp diff + # no_zeros_po['sig_diff'] = (no_zeros_po.loc[:, 'temp_diff'] > long_tc['50%']).astype(int) - # df_fit_sig = get_fit_line(no_zeros_po, x_col='vlv_po', y_col='sig_diff') - # df_fit_sig['y_fitted'] = rescale_fit(df_fit_sig['y_fitted'], no_zeros_po['temp_diff']) + # df_fit_sig = get_fit_line(no_zeros_po, x_col='vlv_po', y_col='sig_diff') + # df_fit_sig['y_fitted'] = rescale_fit(df_fit_sig['y_fitted'], no_zeros_po['temp_diff']) - # est_lt_diff_sig = df_fit_sig[df_fit_sig['vlv_po'] == 0]['y_fitted'].mean() - # bad_vlv = return_exceedance(vlv_df, est_lt_diff_sig, th_time=45, window=15) + # est_lt_diff_sig = df_fit_sig[df_fit_sig['vlv_po'] == 0]['y_fitted'].mean() + # bad_vlv = return_exceedance(vlv_df, est_lt_diff_sig, th_time=45, window=15) - # make a logit regression model assuming that closed valves make a zero temp difference - try: - df_fit_nz = get_fit_line(no_zeros_po) - except RuntimeError: - df_fit_nz = None + # make a logit regression model assuming that closed valves make a zero temp difference + try: + df_fit_nz = get_fit_line(no_zeros_po) + except RuntimeError: + df_fit_nz = None - # determine estimated long-term difference - if df_fit_nz is not None: - est_lt_diff_nz = df_fit_nz[df_fit_nz['vlv_po'] == 0]['y_fitted'].mean() - else: - est_lt_diff_nz = long_tc['25%'] + # determine estimated long-term difference + if df_fit_nz is not None: + est_lt_diff_nz = df_fit_nz[df_fit_nz['vlv_po'] == 0]['y_fitted'].mean() + else: + est_lt_diff_nz = long_tc['25%'] - # calculate bad valve instances vs overall dataframe - th_ratio = 20 - bad_vlv = return_exceedance(vlv_df, est_lt_diff_nz, th_time=45, window=15) + # calculate bad valve instances vs overall dataframe + th_ratio = 20 + bad_vlv = return_exceedance(vlv_df, est_lt_diff_nz, th_time=45, window=15) - if bad_vlv is None: - bad_ratio = 0 - long_tbad = long_tc['mean'] - else: - bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) - long_tbad = bad_vlv['temp_diff'].describe()['mean'] + if bad_vlv is None: + bad_ratio = 0 + long_tbad = long_tc['mean'] + else: + bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) + long_tbad = bad_vlv['temp_diff'].describe()['mean'] - if df_fit_nz is not None: - est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() - else: - est_leak = bad_ratio + if df_fit_nz is not None: + est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() + else: + est_leak = bad_ratio - if est_leak > th_ratio: - bad_klass.append(True) + if est_leak > th_ratio: + bad_klass.append(True) - if len(bad_klass) > 0: - folder = bad_folder - print("Probable passing valve '{}' in site {}\n".format(row['vav_vlv'], row['site'])) + if len(bad_klass) > 0: + folder = bad_folder + if bad_ratio > 5: + print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) if len(bad_klass) > 1: print("{} percentage of time is leaking!".format(bad_ratio)) else: folder = good_folder + else: + folder = good_folder + + if bad_vlv is not None: + # colorize good and bad points + vlv_df['color'] = '#5ab300' + vlv_df.loc[bad_vlv.index, 'color'] = '#b3005a' + + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + + # # get a detailed report of the when valve is malfunctioning + # lal = bad_vlv.groupby('same') + # grps = list(lal.groups.keys()) + # bad_vlv.loc[lal.groups[grps[0]]] - if bad_vlv is not None: - # colorize good and bad points - vlv_df['color'] = '#5ab300' - vlv_df.loc[bad_vlv.index, 'color'] = '#b3005a' - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + # # logit fit with limited points + # df_fit = try_limit_dat_fit_model(vlv_df, df_fraction=1) - # # get a detailed report of the when valve is malfunctioning - # lal = bad_vlv.groupby('same') - # grps = list(lal.groups.keys()) - # bad_vlv.loc[lal.groups[grps[0]]] +def _analyze_ahu(vlv_df, row): + _make_tdiff_vs_vlvpo_plot(vlv_df, row, folder='./') + +def analyze(metadata, clean_func, analyze_func): + # analyze valves + for idx, row in metadata.iterrows(): + try: + # clean data + vlv_df = clean_func(row) + + # analyze for passing valves + analyze_func(vlv_df, row) + + except: + print("Error try to debug") + print(sys.exc_info()[0]) + import pdb; pdb.set_trace() + continue + +vav_metadata = fetch_resp_vav.view('vav_temps') + +# import pdb; pdb.set_trace() +# idx = vav_metadata[vav_metadata['vav'] == 'VAVRM4314'].index[0] +# row = vav_metadata.iloc[idx] +# analyze VAV valves +analyze(vav_metadata, _clean_vav, _analyze_vav) - # # logit fit with limited points - # df_fit = try_limit_dat_fit_model(vlv_df, df_fraction=1) - except: - print("Error try to debug") - print(sys.exc_info()[0]) - import pdb; pdb.set_trace() - continue \ No newline at end of file +# analyze AHU valves +analyze(ahu_metadata, _clean_ahu, _analyze_ahu) From 84317781a0ce202e0a2061e11af0e731b4495992 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 29 Oct 2020 10:39:48 -0700 Subject: [PATCH 13/83] delete merge conflict messages --- detect_passing_valves/app.py | 37 ------------------------------------ 1 file changed, 37 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 6b13913..a124899 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -184,7 +184,6 @@ def _clean_ahu_view(fetch_resp_ahu): # join supply and return air temperature data into on dataset ahu_metadata = ahu_sa.merge(ahu_ra, on=['vlv', 'equip', 'vlv_type', 'site'], how='inner') -<<<<<<< HEAD # delete cooling valve commands heat_vlv = [x not in ['Cooling_Valve_Command'] for x in ahu_metadata['vlv_type']] @@ -202,25 +201,6 @@ def _clean_ahu_view(fetch_resp_ahu): fetch_resp_ahu = client.fetch(ahu_request) ahu_metadata = _clean_ahu_view(fetch_resp_ahu) -======= - - # delete cooling valve commands - heat_vlv = [x not in ['Cooling_Valve_Command'] for x in ahu_metadata['vlv_type']] - - return ahu_metadata[heat_vlv] - -# call the fetch api for VAV data -fetch_resp_vav = client.fetch(vav_request) - -print("-----Dataframe for VAV valves-----") -print(fetch_resp_vav) -print(fetch_resp_vav.view('vav_temps')) - -# call the fetch api for AHU data -fetch_resp_ahu = client.fetch(ahu_request) -ahu_metadata = _clean_ahu_view(fetch_resp_ahu) - ->>>>>>> ffcd9fe1b8b703cbff23469474caf404e3d1ab91 print("-----Dataframe for AHU valves-----") print(fetch_resp_ahu) print(ahu_metadata) @@ -405,22 +385,6 @@ def check_folder_exist(folder): os.makedirs(folder) def _analyze_vav(vlv_df, row, bad_folder = './bad_valves', good_folder = './good_valves'): -<<<<<<< HEAD - - # check if holding folders exist - check_folder_exist(bad_folder) - check_folder_exist(good_folder) - - if vlv_df.shape[0] == 0: - print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) - return - - # determine if valve datastream has open and closed data - bool_type = vlv_df['vlv_open'].value_counts().index - - bad_vlv_val = 5 - -======= # check if holding folders exist check_folder_exist(bad_folder) @@ -435,7 +399,6 @@ def _analyze_vav(vlv_df, row, bad_folder = './bad_valves', good_folder = './good bad_vlv_val = 5 ->>>>>>> ffcd9fe1b8b703cbff23469474caf404e3d1ab91 if len(bool_type) < 2: if bool_type[0]: # only open valve data From a161a696977e8d4f3798a77f831437184413648a Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 30 Oct 2020 11:01:45 -0700 Subject: [PATCH 14/83] cleaned up comments and variable names --- detect_passing_valves/app.py | 107 +++++++++++++++-------------------- 1 file changed, 47 insertions(+), 60 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index a124899..1bf843b 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -21,10 +21,10 @@ ?equip rdf:type/rdfs:subClassOf? brick:VAV . ?equip bf:isFedBy+ ?ahu . ?vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?ahu_supply . - ?equip bf:hasPoint ?vav_supply . - ?ahu_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?vav_supply rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?ahu bf:hasPoint ?upstream_ta . + ?equip bf:hasPoint ?dnstream_ta . + ?upstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?dnstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . ?equip bf:hasPoint ?vlv . ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . };""" @@ -33,10 +33,10 @@ WHERE { ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . ?vlv rdf:type ?vlv_type . - ?equip bf:hasPoint ?vlv . - ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?equip bf:hasPoint ?air_temps . + ?equip bf:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" @@ -44,10 +44,10 @@ WHERE { ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . ?vlv rdf:type ?vlv_type . - ?equip bf:hasPoint ?vlv . - ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Return_Air_Temperature_Sensor . - ?equip bf:hasPoint ?air_temps . + ?equip bf:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" @@ -73,41 +73,41 @@ sites=qualify_vav_resp.sites, views=[ pymortar.View( - name="vav_temps", + name="dnstream_ta", definition=vav_query, ), ], dataFrames=[ pymortar.DataFrame( - name="vav_valve", + name="vlv", aggregation=pymortar.MEAN, window="15m", timeseries=[ pymortar.Timeseries( - view="vav_temps", + view="dnstream_ta", dataVars=["?vlv"], ) ] ), pymortar.DataFrame( - name="vav_temp", + name="dnstream_ta", aggregation=pymortar.MEAN, window="15m", timeseries=[ pymortar.Timeseries( - view="vav_temps", - dataVars=["?vav_supply"], + view="dnstream_ta", + dataVars=["?dnstream_ta"], ) ] ), pymortar.DataFrame( - name="ahu_temp", + name="upstream_ta", aggregation=pymortar.MEAN, window="15m", timeseries=[ pymortar.Timeseries( - view="vav_temps", - dataVars=["?ahu_supply"], + view="dnstream_ta", + dataVars=["?upstream_ta"], ) ] ), @@ -124,11 +124,11 @@ sites=ahu_sites, views=[ pymortar.View( - name="ahu_sa_temp", + name="dnstream_ta", definition=ahu_sa_query, ), pymortar.View( - name="ahu_ra_temp", + name="upstream_ta", definition=ahu_ra_query, ), ], @@ -139,29 +139,29 @@ window="15m", timeseries=[ pymortar.Timeseries( - view="ahu_sa_temp", + view="dnstream_ta", dataVars=["?vlv"], ) ] ), pymortar.DataFrame( - name="ahu_sa_temp", + name="dnstream_ta", aggregation=pymortar.MEAN, window="15m", timeseries=[ pymortar.Timeseries( - view="ahu_sa_temp", + view="dnstream_ta", dataVars=["?air_temps"], ) ] ), pymortar.DataFrame( - name="ahu_ra_temp", + name="upstream_ta", aggregation=pymortar.MEAN, window="15m", timeseries=[ pymortar.Timeseries( - view="ahu_ra_temp", + view="upstream_ta", dataVars=["?air_temps"], ) ] @@ -175,12 +175,12 @@ def _clean_ahu_view(fetch_resp_ahu): # supply air temp metadata - ahu_sa = fetch_resp_ahu.view('ahu_sa_temp') - ahu_sa = ahu_sa.rename(columns={'air_temps': 'dnstream_ta', 'temp_type': 'supply type', 'air_temps_uuid': 'dnstream_ta uuid'}) + ahu_sa = fetch_resp_ahu.view('dnstream_ta') + ahu_sa = ahu_sa.rename(columns={'air_temps': 'dnstream_ta', 'temp_type': 'dnstream_ta', 'air_temps_uuid': 'dnstream_ta uuid'}) # return air temp metadata - ahu_ra = fetch_resp_ahu.view('ahu_ra_temp') - ahu_ra = ahu_ra.rename(columns={'air_temps': 'upstream_ta', 'temp_type': 'return type', 'air_temps_uuid': 'upstream_ta uuid'}) + ahu_ra = fetch_resp_ahu.view('upstream_ta') + ahu_ra = ahu_ra.rename(columns={'air_temps': 'upstream_ta', 'temp_type': 'upstream_type', 'air_temps_uuid': 'upstream_ta uuid'}) # join supply and return air temperature data into on dataset ahu_metadata = ahu_sa.merge(ahu_ra, on=['vlv', 'equip', 'vlv_type', 'site'], how='inner') @@ -195,7 +195,7 @@ def _clean_ahu_view(fetch_resp_ahu): print("-----Dataframe for VAV valves-----") print(fetch_resp_vav) -print(fetch_resp_vav.view('vav_temps')) +print(fetch_resp_vav.view('dnstream_ta')) # call the fetch api for AHU data fetch_resp_ahu = client.fetch(ahu_request) @@ -205,15 +205,13 @@ def _clean_ahu_view(fetch_resp_ahu): print(fetch_resp_ahu) print(ahu_metadata) -# print the different types of valves in the data -#print(fetch_resp.view('valves').groupby(['vlv_subclass']).count()) def _clean_vav(row): # combine data points in one dataframe - vav_sa = fetch_resp_vav['vav_temp'][row['vav_supply_uuid']] - ahu_sa = fetch_resp_vav['ahu_temp'][row['ahu_supply_uuid']] - vlv_po = fetch_resp_vav['vav_valve'][row['vlv_uuid']] + vav_sa = fetch_resp_vav['dnstream_ta'][row['dnstream_ta_uuid']] + ahu_sa = fetch_resp_vav['upstream_ta'][row['upstream_ta_uuid']] + vlv_po = fetch_resp_vav['vlv'][row['vlv_uuid']] vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] @@ -233,8 +231,8 @@ def _clean_vav(row): return vav_df def _clean_ahu(row): - dnstream = fetch_resp_ahu['ahu_sa_temp'][row['dnstream_ta uuid']] - upstream = fetch_resp_ahu['ahu_ra_temp'][row['upstream_ta uuid']] + dnstream = fetch_resp_ahu['dnstream_ta'][row['dnstream_ta uuid']] + upstream = fetch_resp_ahu['upstream_ta'][row['upstream_ta uuid']] vlv_po = fetch_resp_ahu['ahu_valve'][row['vlv_uuid']] @@ -384,7 +382,7 @@ def check_folder_exist(folder): if not os.path.exists(folder): os.makedirs(folder) -def _analyze_vav(vlv_df, row, bad_folder = './bad_valves', good_folder = './good_valves'): +def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good_valves'): # check if holding folders exist check_folder_exist(bad_folder) @@ -429,16 +427,6 @@ def _analyze_vav(vlv_df, row, bad_folder = './bad_valves', good_folder = './good no_zeros_po = vlv_df.copy() no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 - # # make a logit regression model based on a threshold value - # # compare long-term average with actual temp diff - # no_zeros_po['sig_diff'] = (no_zeros_po.loc[:, 'temp_diff'] > long_tc['50%']).astype(int) - - # df_fit_sig = get_fit_line(no_zeros_po, x_col='vlv_po', y_col='sig_diff') - # df_fit_sig['y_fitted'] = rescale_fit(df_fit_sig['y_fitted'], no_zeros_po['temp_diff']) - - # est_lt_diff_sig = df_fit_sig[df_fit_sig['vlv_po'] == 0]['y_fitted'].mean() - # bad_vlv = return_exceedance(vlv_df, est_lt_diff_sig, th_time=45, window=15) - # make a logit regression model assuming that closed valves make a zero temp difference try: df_fit_nz = get_fit_line(no_zeros_po) @@ -488,17 +476,20 @@ def _analyze_vav(vlv_df, row, bad_folder = './bad_valves', good_folder = './good _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) - # # get a detailed report of the when valve is malfunctioning + # TODO get a detailed report of the when valve is malfunctioning # lal = bad_vlv.groupby('same') # grps = list(lal.groups.keys()) # bad_vlv.loc[lal.groups[grps[0]]] +def _analyze_ahu(vlv_df, row): + import pdb; pdb.set_trace() - # # logit fit with limited points - # df_fit = try_limit_dat_fit_model(vlv_df, df_fraction=1) + if row['upstream_type'] != 'Mixed_Air_Temperature_Sensor': + print('No upstream sensor data available for coil in AHU {} for site {}'.format(row['equip'], row['site'])) + #_make_tdiff_vs_vlvpo_plot(vlv_df, row, folder='./') + else: + _analyze_vlv(vlv_df, row) -def _analyze_ahu(vlv_df, row): - _make_tdiff_vs_vlvpo_plot(vlv_df, row, folder='./') def analyze(metadata, clean_func, analyze_func): # analyze valves @@ -516,14 +507,10 @@ def analyze(metadata, clean_func, analyze_func): import pdb; pdb.set_trace() continue -vav_metadata = fetch_resp_vav.view('vav_temps') - -# import pdb; pdb.set_trace() -# idx = vav_metadata[vav_metadata['vav'] == 'VAVRM4314'].index[0] -# row = vav_metadata.iloc[idx] +vav_metadata = fetch_resp_vav.view('dnstream_ta') # analyze VAV valves -analyze(vav_metadata, _clean_vav, _analyze_vav) +analyze(vav_metadata, _clean_vav, _analyze_vlv) # analyze AHU valves analyze(ahu_metadata, _clean_ahu, _analyze_ahu) From 27d33a3f4d6ba649391356d3b2e65e045fd9ebb0 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 30 Oct 2020 11:30:15 -0700 Subject: [PATCH 15/83] example of outputs --- ...3B.RM239B_LAB.Zone_Reheat_Valve_Command.png | Bin 0 -> 147059 bytes ...4A.RM041C_LAB.Zone_Reheat_Valve_Command.png | Bin 0 -> 105774 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 detect_passing_valves/bad_valves/brig-VAVRM239B_LAB-BRIG.ZONE.AHU03B.RM239B_LAB.Zone_Reheat_Valve_Command.png create mode 100644 detect_passing_valves/good_valves/brig-VAVRM041C_LAB-BRIG.ZONE.AHU04A.RM041C_LAB.Zone_Reheat_Valve_Command.png diff --git a/detect_passing_valves/bad_valves/brig-VAVRM239B_LAB-BRIG.ZONE.AHU03B.RM239B_LAB.Zone_Reheat_Valve_Command.png b/detect_passing_valves/bad_valves/brig-VAVRM239B_LAB-BRIG.ZONE.AHU03B.RM239B_LAB.Zone_Reheat_Valve_Command.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f20a06fb10a94182968ea237b205c3806655c2 GIT binary patch literal 147059 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¨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 Date: Fri, 30 Oct 2020 14:48:51 -0700 Subject: [PATCH 16/83] deleted debug code snippet --- detect_passing_valves/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 1bf843b..aaeac66 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -482,7 +482,6 @@ def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good # bad_vlv.loc[lal.groups[grps[0]]] def _analyze_ahu(vlv_df, row): - import pdb; pdb.set_trace() if row['upstream_type'] != 'Mixed_Air_Temperature_Sensor': print('No upstream sensor data available for coil in AHU {} for site {}'.format(row['equip'], row['site'])) From 3bef8259a7b4e8a055d366fb4298412eaa400e1d Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 10:29:22 -0800 Subject: [PATCH 17/83] refactored quiery and qualify steps into a function --- detect_passing_valves/app.py | 163 ++++++++++++++++++++++------------- 1 file changed, 104 insertions(+), 59 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index aaeac66..4083e1d 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -12,69 +12,114 @@ eval_start_time = "2018-01-01T00:00:00Z" eval_end_time = "2018-06-30T00:00:00Z" +def _query_and_qualify(): + """ + Build query to return control valves, up- and down- stream air temperatures relative to the + valve, and other related data. Then qualify which sites can run this app. + + Parameters + ---------- + None + + Returns + ------- + query: dictionary containing query, sites, and qualify response + + """ + + # connect to client + client = pymortar.Client() + + # initialize container for query information + query = dict() + + # define query to analyze VAV valves + vav_query = """SELECT * + WHERE { + ?equip rdf:type/rdfs:subClassOf? brick:VAV . + ?equip bf:isFedBy+ ?ahu . + ?vlv rdf:type ?vlv_type . + ?ahu bf:hasPoint ?upstream_ta . + ?equip bf:hasPoint ?dnstream_ta . + ?upstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?dnstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?equip bf:hasPoint ?vlv . + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + };""" + + # define queries to analyze AHU valves + ahu_sa_query = """SELECT * + WHERE { + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?vlv rdf:type ?vlv_type . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?air_temps rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . + ?equip bf:hasPoint ?air_temps . + ?air_temps rdf:type ?temp_type . + };""" + + ahu_ra_query = """SELECT * + WHERE { + ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . + ?vlv rdf:type ?vlv_type . + ?equip bf:hasPoint ?vlv . + ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . + ?air_temps rdf:type/rdfs:subClassOf* brick:Return_Air_Temperature_Sensor . + ?equip bf:hasPoint ?air_temps . + ?air_temps rdf:type ?temp_type . + };""" + + # find sites with these sensors and setpoints + qualify_vav_resp = client.qualify([vav_query]) + qualify_sa_resp = client.qualify([ahu_sa_query]) + qualify_ra_resp = client.qualify([ahu_ra_query]) + + if qualify_vav_resp.error != "": + print("ERROR: ", qualify_vav_resp.error) + sys.exit(1) + elif len(qualify_vav_resp.sites) == 0: + print("NO SITES RETURNED") + sys.exit(0) + + vav_sites = qualify_vav_resp.sites + ahu_sites = np.intersect1d(qualify_sa_resp.sites, qualify_ra_resp.sites) + tlt_sites = np.union1d(vav_sites, ahu_sites) + print("running on {0} sites".format(len(tlt_sites))) + + # save queries + query['query'] = dict() + query['query']['vav'] = qualify_vav_resp + query['query']['ahu_sa'] = qualify_sa_resp + query['query']['ahu_ra'] = qualify_ra_resp + + # save qualify responses + query['qualify'] = dict() + query['qualify']['vav'] = vav_query + query['qualify']['ahu_sa'] = ahu_sa_query + query['qualify']['ahu_ra'] = ahu_ra_query + + # save sites + query['sites'] = dict() + query['sites']['vav'] = vav_sites + query['sites']['ahu'] = ahu_sites + query['sites']['tlt'] = tlt_sites + + return query + +# build query and qualify +query = _query_and_qualify() + +# connect to client client = pymortar.Client() -# define query to return valves -# returns supply air temps from ahu and vav and vav valve -vav_query = """SELECT * -WHERE { - ?equip rdf:type/rdfs:subClassOf? brick:VAV . - ?equip bf:isFedBy+ ?ahu . - ?vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?upstream_ta . - ?equip bf:hasPoint ?dnstream_ta . - ?upstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?dnstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?equip bf:hasPoint ?vlv . - ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . -};""" - -ahu_sa_query = """SELECT * -WHERE { - ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - ?vlv rdf:type ?vlv_type . - ?equip bf:hasPoint ?vlv . - ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . - ?air_temps rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?equip bf:hasPoint ?air_temps . - ?air_temps rdf:type ?temp_type . -};""" - -ahu_ra_query = """SELECT * -WHERE { - ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . - ?vlv rdf:type ?vlv_type . - ?equip bf:hasPoint ?vlv . - ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . - ?air_temps rdf:type/rdfs:subClassOf* brick:Return_Air_Temperature_Sensor . - ?equip bf:hasPoint ?air_temps . - ?air_temps rdf:type ?temp_type . -};""" - -# find sites with these sensors and setpoints -qualify_vav_resp = client.qualify([vav_query]) -qualify_sa_resp = client.qualify([ahu_sa_query]) -qualify_ra_resp = client.qualify([ahu_ra_query]) - -if qualify_vav_resp.error != "": - print("ERROR: ", qualify_vav_resp.error) - sys.exit(1) -elif len(qualify_vav_resp.sites) == 0: - print("NO SITES RETURNED") - sys.exit(0) - -vav_sites = qualify_vav_resp.sites -ahu_sites = np.intersect1d(qualify_sa_resp.sites, qualify_ra_resp.sites) -tlt_sites = np.union1d(vav_sites, ahu_sites) -print("running on {0} sites".format(len(tlt_sites))) - # build the fetch request vav_request = pymortar.FetchRequest( - sites=qualify_vav_resp.sites, + sites=query['sites']['vav'], views=[ pymortar.View( name="dnstream_ta", - definition=vav_query, + definition=query['query']['vav'], ), ], dataFrames=[ @@ -121,15 +166,15 @@ # build the fetch request ahu_request = pymortar.FetchRequest( - sites=ahu_sites, + sites=query['sites']['ahu'], views=[ pymortar.View( name="dnstream_ta", - definition=ahu_sa_query, + definition=query['query']['ahu_sa'], ), pymortar.View( name="upstream_ta", - definition=ahu_ra_query, + definition=query['query']['ahu_ra'], ), ], dataFrames=[ From e00dbefd4b01416afc5b46df401b511bfd6c29d6 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 10:44:00 -0800 Subject: [PATCH 18/83] refactored fetch step into a function --- detect_passing_valves/app.py | 315 ++++++++++++++++++++--------------- 1 file changed, 180 insertions(+), 135 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 4083e1d..b4d433e 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -8,9 +8,6 @@ import matplotlib.pyplot as plt from scipy.optimize import curve_fit -# define parameters -eval_start_time = "2018-01-01T00:00:00Z" -eval_end_time = "2018-06-30T00:00:00Z" def _query_and_qualify(): """ @@ -107,118 +104,174 @@ def _query_and_qualify(): return query -# build query and qualify -query = _query_and_qualify() -# connect to client -client = pymortar.Client() - -# build the fetch request -vav_request = pymortar.FetchRequest( - sites=query['sites']['vav'], - views=[ - pymortar.View( - name="dnstream_ta", - definition=query['query']['vav'], - ), - ], - dataFrames=[ - pymortar.DataFrame( - name="vlv", - aggregation=pymortar.MEAN, - window="15m", - timeseries=[ - pymortar.Timeseries( - view="dnstream_ta", - dataVars=["?vlv"], - ) - ] - ), - pymortar.DataFrame( - name="dnstream_ta", - aggregation=pymortar.MEAN, - window="15m", - timeseries=[ - pymortar.Timeseries( - view="dnstream_ta", - dataVars=["?dnstream_ta"], - ) - ] - ), - pymortar.DataFrame( - name="upstream_ta", - aggregation=pymortar.MEAN, - window="15m", - timeseries=[ - pymortar.Timeseries( - view="dnstream_ta", - dataVars=["?upstream_ta"], - ) - ] - ), - ], - time=pymortar.TimeParams( - start=eval_start_time, - end=eval_end_time, +def _fetch(query, eval_start_time, eval_end_time, window=15): + """ + Build the fetch query and define the time interval for analysis + + Parameters + ---------- + query: dictionary containing query, sites, and qualify response + + eval_start_time : start date and time in format (yyyy-mm-ddTHH:MM:SSZ) for the thermal + comfort evaluation period + + eval_end_time : end date and time in format (yyyy-mm-ddTHH:MM:SSZ) for the thermal + comfort evaluation period + + window : aggregation window in minutes to average the measurement data + + + Returns + ------- + fetch_resp : Mortar FetchResponse object + + """ + + # connect to client + client = pymortar.Client() + + # build the fetch request for the vav valves + vav_request = pymortar.FetchRequest( + sites=query['sites']['vav'], + views=[ + pymortar.View( + name="dnstream_ta", + definition=query['query']['vav'], + ), + ], + dataFrames=[ + pymortar.DataFrame( + name="vlv", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="dnstream_ta", + dataVars=["?vlv"], + ) + ] + ), + pymortar.DataFrame( + name="dnstream_ta", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="dnstream_ta", + dataVars=["?dnstream_ta"], + ) + ] + ), + pymortar.DataFrame( + name="upstream_ta", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="dnstream_ta", + dataVars=["?upstream_ta"], + ) + ] + ), + ], + time=pymortar.TimeParams( + start=eval_start_time, + end=eval_end_time, + ) ) -) - - -# build the fetch request -ahu_request = pymortar.FetchRequest( - sites=query['sites']['ahu'], - views=[ - pymortar.View( - name="dnstream_ta", - definition=query['query']['ahu_sa'], - ), - pymortar.View( - name="upstream_ta", - definition=query['query']['ahu_ra'], - ), - ], - dataFrames=[ - pymortar.DataFrame( - name="ahu_valve", - aggregation=pymortar.MEAN, - window="15m", - timeseries=[ - pymortar.Timeseries( - view="dnstream_ta", - dataVars=["?vlv"], - ) - ] - ), - pymortar.DataFrame( - name="dnstream_ta", - aggregation=pymortar.MEAN, - window="15m", - timeseries=[ - pymortar.Timeseries( - view="dnstream_ta", - dataVars=["?air_temps"], - ) - ] - ), - pymortar.DataFrame( - name="upstream_ta", - aggregation=pymortar.MEAN, - window="15m", - timeseries=[ - pymortar.Timeseries( - view="upstream_ta", - dataVars=["?air_temps"], - ) - ] - ), - ], - time=pymortar.TimeParams( - start=eval_start_time, - end=eval_end_time, + + + # build the fetch request for the ahu valves + ahu_request = pymortar.FetchRequest( + sites=query['sites']['ahu'], + views=[ + pymortar.View( + name="dnstream_ta", + definition=query['query']['ahu_sa'], + ), + pymortar.View( + name="upstream_ta", + definition=query['query']['ahu_ra'], + ), + ], + dataFrames=[ + pymortar.DataFrame( + name="ahu_valve", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="dnstream_ta", + dataVars=["?vlv"], + ) + ] + ), + pymortar.DataFrame( + name="dnstream_ta", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="dnstream_ta", + dataVars=["?air_temps"], + ) + ] + ), + pymortar.DataFrame( + name="upstream_ta", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="upstream_ta", + dataVars=["?air_temps"], + ) + ] + ), + ], + time=pymortar.TimeParams( + start=eval_start_time, + end=eval_end_time, + ) ) -) -def _clean_ahu_view(fetch_resp_ahu): + # call the fetch api for VAV data + fetch_resp_vav = client.fetch(vav_request) + + print("-----Dataframe for VAV valves-----") + print(fetch_resp_vav) + print(fetch_resp_vav.view('dnstream_ta')) + + # call the fetch api for AHU data + fetch_resp_ahu = client.fetch(ahu_request) + ahu_metadata = reformat_ahu_view(fetch_resp_ahu) + + print("-----Dataframe for AHU valves-----") + print(fetch_resp_ahu) + print(ahu_metadata) + + # save fetch responses + fetch_resp = dict() + fetch_resp['vav'] = fetch_resp_vav + fetch_resp['ahu'] = fetch_resp_ahu + + return fetch_resp + + +def reformat_ahu_view(fetch_resp_ahu): + """ + Rename, reformat, and delete cooling valves from ahu metadata + + Parameters + ---------- + fetch_resp_ahu : Mortar FetchResponse object for AHU data + + Returns + ------- + ahu_metadata: Pandas object with AHU metadata and no valves used for cooling + + """ # supply air temp metadata ahu_sa = fetch_resp_ahu.view('dnstream_ta') ahu_sa = ahu_sa.rename(columns={'air_temps': 'dnstream_ta', 'temp_type': 'dnstream_ta', 'air_temps_uuid': 'dnstream_ta uuid'}) @@ -235,28 +288,13 @@ def _clean_ahu_view(fetch_resp_ahu): return ahu_metadata[heat_vlv] -# call the fetch api for VAV data -fetch_resp_vav = client.fetch(vav_request) - -print("-----Dataframe for VAV valves-----") -print(fetch_resp_vav) -print(fetch_resp_vav.view('dnstream_ta')) - -# call the fetch api for AHU data -fetch_resp_ahu = client.fetch(ahu_request) -ahu_metadata = _clean_ahu_view(fetch_resp_ahu) - -print("-----Dataframe for AHU valves-----") -print(fetch_resp_ahu) -print(ahu_metadata) - -def _clean_vav(row): +def _clean_vav(fetch_resp, row): # combine data points in one dataframe - vav_sa = fetch_resp_vav['dnstream_ta'][row['dnstream_ta_uuid']] - ahu_sa = fetch_resp_vav['upstream_ta'][row['upstream_ta_uuid']] - vlv_po = fetch_resp_vav['vlv'][row['vlv_uuid']] + vav_sa = fetch_resp['vav']['dnstream_ta'][row['dnstream_ta_uuid']] + ahu_sa = fetch_resp['vav']['upstream_ta'][row['upstream_ta_uuid']] + vlv_po = fetch_resp['vav']['vlv'][row['vlv_uuid']] vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] @@ -275,11 +313,11 @@ def _clean_vav(row): return vav_df -def _clean_ahu(row): - dnstream = fetch_resp_ahu['dnstream_ta'][row['dnstream_ta uuid']] - upstream = fetch_resp_ahu['upstream_ta'][row['upstream_ta uuid']] +def _clean_ahu(fetch_resp, row): + dnstream = fetch_resp['ahu']['dnstream_ta'][row['dnstream_ta uuid']] + upstream = fetch_resp['ahu']['upstream_ta'][row['upstream_ta uuid']] - vlv_po = fetch_resp_ahu['ahu_valve'][row['vlv_uuid']] + vlv_po = fetch_resp['ahu']['ahu_valve'][row['vlv_uuid']] ahu_df = pd.concat([upstream, dnstream, vlv_po], axis=1) ahu_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] @@ -551,10 +589,17 @@ def analyze(metadata, clean_func, analyze_func): import pdb; pdb.set_trace() continue -vav_metadata = fetch_resp_vav.view('dnstream_ta') +# define parameters +eval_start_time = "2018-01-01T00:00:00Z" +eval_end_time = "2018-06-30T00:00:00Z" + +query = _query_and_qualify() +fetch_resp = _fetch(query, eval_start_time, eval_end_time, window=15) # analyze VAV valves +vav_metadata = fetch_resp['vav'].view('dnstream_ta') analyze(vav_metadata, _clean_vav, _analyze_vlv) # analyze AHU valves +ahu_metadata = reformat_ahu_view(fetch_resp['ahu']) analyze(ahu_metadata, _clean_ahu, _analyze_ahu) From 284115a67565ae61fca9469a1d3957e8cb4ebff0 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 11:13:46 -0800 Subject: [PATCH 19/83] added descriptions to clean functions --- detect_passing_valves/app.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index b4d433e..20b8bf6 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -290,6 +290,21 @@ def reformat_ahu_view(fetch_resp_ahu): def _clean_vav(fetch_resp, row): + """ + Make a pandas dataframe with relavent vav data for the specific valve + and clean from NA values. + + Parameters + ---------- + fetch_resp : Mortar FetchResponse object + + row: Pandas series object with metadata for the specific vav valve + + Returns + ------- + vav_df: Pandas dataframe with valve timeseries data + + """ # combine data points in one dataframe vav_sa = fetch_resp['vav']['dnstream_ta'][row['dnstream_ta_uuid']] @@ -314,6 +329,21 @@ def _clean_vav(fetch_resp, row): return vav_df def _clean_ahu(fetch_resp, row): + """ + Make a pandas dataframe with relavent ahu data for the specific valve + and clean from NA values. + + Parameters + ---------- + fetch_resp : Mortar FetchResponse object + + row: Pandas series object with metadata for the specific vav valve + + Returns + ------- + ahu_df: Pandas dataframe with valve timeseries data + + """ dnstream = fetch_resp['ahu']['dnstream_ta'][row['dnstream_ta uuid']] upstream = fetch_resp['ahu']['upstream_ta'][row['upstream_ta uuid']] From d8d53d7a8fa82d1949cc16f5efc7a5d9e008cc94 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 13:34:01 -0800 Subject: [PATCH 20/83] refactored and added comments for the helper tool functions --- detect_passing_valves/app.py | 192 ++++++++++++++++++++++++++++++----- 1 file changed, 167 insertions(+), 25 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 20b8bf6..623062b 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -119,7 +119,7 @@ def _fetch(query, eval_start_time, eval_end_time, window=15): eval_end_time : end date and time in format (yyyy-mm-ddTHH:MM:SSZ) for the thermal comfort evaluation period - window : aggregation window in minutes to average the measurement data + window : aggregation window, in minutes, to average the raw measurement data Returns @@ -292,7 +292,8 @@ def reformat_ahu_view(fetch_resp_ahu): def _clean_vav(fetch_resp, row): """ Make a pandas dataframe with relavent vav data for the specific valve - and clean from NA values. + and clean from NA values. Calculate temperature difference between + downstream and upstream air temperatures. Parameters ---------- @@ -367,38 +368,101 @@ def _clean_ahu(fetch_resp, row): return ahu_df -def scale_0to1(temp_diff): - max_t = temp_diff.max() - min_t = temp_diff.min() +###### +# define tools +# TODO: Separate the tools into a new python file +###### - new_t = (temp_diff - min_t) / (max_t - min_t) +def scale_0to1(vals): + """ + Scale pandas series object data from 0 to 1 - return new_t + Parameters + ---------- + vals: Pandas series object or Pandas dataframe colum to scale from 0 to 1. -def rescale_fit(scaled_x, temp_diff): - max_t = temp_diff.max() - min_t = temp_diff.min() + Returns + ------- + scaled_vals: Pandas series object with values scaled from 0 to 1 + """ - rescaled = min_t + scaled_x*(max_t - min_t) + max_val = vals.max() + min_val = vals.min() + + scaled_vals = (vals - min_val) / (max_val - min_val) + + return scaled_vals + + +def rescale_fit(scaled_vals, vals): + """ + Rescale values of pandas series that are 0 to 1 to match the interval + of another pandas series object values + + Parameters + ---------- + scaled_vals: Pandas series object with values scaled from 0 to 1 and needs to be unscaled + + vals: Pandas series object or Pandas dataframe colum with unnormalized values. + This is used to extract max and min to unscaled the scaled_vals. + + Returns + ------- + unscaled_vals: Pandas series object of unscaled values + """ + max_val = vals.max() + min_val = vals.min() + + unscaled_vals = min_val + scaled_vals*(max_val - min_val) + + return unscaled_vals - return rescaled def sigmoid(x, k, x0): + """ + Sigmoid function curve to do a logistic model + + Parameters + ---------- + x: independent variable + k: slope of the sigmoid function + x0: midpoint of the sigmoid function + + Returns + ------- + y: value of the function at point x + """ return 1.0 / (1 + np.exp(-k * (x - x0))) -def get_fit_line(vlv_df, x_col='vlv_po', y_col='temp_diff'): +def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff'): + """ + Build a logistic model with data provided + + Parameters + ---------- + df: Pandas dataframe object with x and y variables to make model + + x_col: column name that contains x, independent, variable + + y_col: column name that contains y, dependent, variable + + Returns + ------- + df_fit: Pandas dataframe object with y_fitted values to a logistic model + """ + # fit the curve - scaled_pos = scale_0to1(vlv_df[x_col]) - scaled_t = scale_0to1(vlv_df[y_col]) + scaled_pos = scale_0to1(df[x_col]) + scaled_t = scale_0to1(df[y_col]) popt, pcov = curve_fit(sigmoid, scaled_pos, scaled_t) # calculate fitted temp difference values est_k, est_x0 = popt - y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), vlv_df[y_col]) + y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), df[y_col]) y_fitted.name = 'y_fitted' # sort values - df_fit = pd.concat([vlv_df[x_col], y_fitted], axis=1) + df_fit = pd.concat([df[x_col], y_fitted], axis=1) df_fit = df_fit.sort_values(by=x_col) return df_fit @@ -408,16 +472,48 @@ def try_limit_dat_fit_model(vlv_df, df_fraction): nrows, ncols = vlv_df.shape some_pts = np.random.choice(nrows, int(nrows*df_fraction)) try: - df_fit = get_fit_line(vlv_df.iloc[some_pts]) + df_fit = build_logistic_model(vlv_df.iloc[some_pts]) except RuntimeError: try: - df_fit = get_fit_line(vlv_df) + df_fit = build_logistic_model(vlv_df) except RuntimeError: print("No regression found") df_fit = None return df_fit + +def check_folder_exist(folder): + """ + Check the existance of the defined folder. If it does + not exist, then create folder. + + Parameters + ---------- + folder: name of path to check its existance + + Returns + ------- + None + """ + if not os.path.exists(folder): + os.makedirs(folder) + def calc_long_t_diff(vlv_df, vlv_open=False): + """ + Calculate statistic on difference between down- and up- + stream temperatures to determine the long term temperature difference. + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + vlv_open: boolean define if the statistics are performed on data that has + valve open (True) or closed (False) + + Returns + ------- + long_t: dictionary object with statisitics of temperature difference + """ if vlv_open: # long-term average when valve is open df_vlv_close = vlv_df[vlv_df['vlv_open']] @@ -430,6 +526,33 @@ def calc_long_t_diff(vlv_df, vlv_open=False): return long_t def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): + """ + Make plot showing the correct and bad operating points of the valve control along with helper annotations + e.g. long term average for correct and malfunction operating points when valve is commanded off, model fit, and + bad to good operating points. + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + row: Pandas series object with metadata for the specific valve + + long_t: long-term temperature difference between down and up air streams when valve is + commanded close for correct operation + + long_tbad: long-term temperature difference between down and up air streams when valve is + commanded close for malfunction operation + + df_fit: Pandas dataframe object with y_fitted values to a logistic model + + bad_ratio: ratio showing the mulfunction operation points to good operation points + + folder: name of path to save the plot image + + Returns + ------- + None + """ # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') @@ -461,7 +584,29 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N plt.savefig(join(folder, plt_name + '.png')) plt.close() -def return_exceedance(vlv_df, long_t, th_time=45, window=15): + +def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): + """ + + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + long_t: long-term temperature difference between down and up air streams when valve is + commanded close for correct operation + + th_time: length of time, in minutes, after the valve is closed to determine if + valve operating point is malfunctioning e.g. allow enough time for residue heat to + dissipate from the coil. + + window : aggregation window, in minutes, to average the raw measurement data + + Returns + ------- + bad_vlv: pandas dataframe object with the time intervals that the valve is malfunctioning + """ + # find datapoints that exceed long-term temperature difference min_ts = int(th_time/window) th_exceed = np.logical_and((vlv_df['temp_diff'] >= long_t), ~(vlv_df['vlv_open'])) @@ -491,9 +636,6 @@ def return_exceedance(vlv_df, long_t, th_time=45, window=15): return bad_vlv.drop(columns=['cons_ts']) -def check_folder_exist(folder): - if not os.path.exists(folder): - os.makedirs(folder) def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good_valves'): @@ -542,7 +684,7 @@ def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good # make a logit regression model assuming that closed valves make a zero temp difference try: - df_fit_nz = get_fit_line(no_zeros_po) + df_fit_nz = build_logistic_model(no_zeros_po) except RuntimeError: df_fit_nz = None @@ -554,7 +696,7 @@ def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good # calculate bad valve instances vs overall dataframe th_ratio = 20 - bad_vlv = return_exceedance(vlv_df, est_lt_diff_nz, th_time=45, window=15) + bad_vlv = find_bad_vlv_operation(vlv_df, est_lt_diff_nz, th_time=45, window=15) if bad_vlv is None: bad_ratio = 0 From 4f7a94c028226f7d9f7dd320fe9acfc09a77e184 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 14:07:28 -0800 Subject: [PATCH 21/83] refactored the analyze steps --- detect_passing_valves/app.py | 153 ++++++++++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 21 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 623062b..c427400 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -637,7 +637,30 @@ def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): return bad_vlv.drop(columns=['cons_ts']) -def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good_valves'): +def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valves', bad_folder='./bad_valves'): + """ + Analyze each valve and detect for passing valves + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + row: Pandas series object with metadata for the specific valve + + th_bad_vlv: temperature difference from long term temperature difference to consider an operating point as malfunctioning + + th_time: length of time, in minutes, after the valve is closed to determine if + valve operating point is malfunctioning e.g. allow enough time for residue heat to + dissipate from the coil. + + good_folder: name of path showing the folder to save the plots of the correct operating valves + + bad_folder: name of path showing the folder to save the plots of the malfunction valves + + Returns + ------- + None + """ # check if holding folders exist check_folder_exist(bad_folder) @@ -650,19 +673,17 @@ def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good # determine if valve datastream has open and closed data bool_type = vlv_df['vlv_open'].value_counts().index - bad_vlv_val = 5 - if len(bool_type) < 2: if bool_type[0]: # only open valve data long_to = calc_long_t_diff(vlv_df, vlv_open=True) - if long_to['50%'] < bad_vlv_val: + if long_to['50%'] < th_bad_vlv: print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=bad_folder) else: # only closed valve data long_tc = calc_long_t_diff(vlv_df) - if long_tc['50%'] > bad_vlv_val: + if long_tc['50%'] > th_bad_vlv: print("Probable passing valve '{}' in site {}".format(row['vlv'], row['site'])) _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=bad_folder) return @@ -696,7 +717,7 @@ def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good # calculate bad valve instances vs overall dataframe th_ratio = 20 - bad_vlv = find_bad_vlv_operation(vlv_df, est_lt_diff_nz, th_time=45, window=15) + bad_vlv = find_bad_vlv_operation(vlv_df, est_lt_diff_nz, th_time, window) if bad_vlv is None: bad_ratio = 0 @@ -736,16 +757,64 @@ def _analyze_vlv(vlv_df, row, bad_folder = './bad_valves', good_folder = './good # grps = list(lal.groups.keys()) # bad_vlv.loc[lal.groups[grps[0]]] -def _analyze_ahu(vlv_df, row): +def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder): + """ + Helper function to analyze AHU valves + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + row: Pandas series object with metadata for the specific valve + + th_bad_vlv: temperature difference from long term temperature difference to consider an operating point as malfunctioning + + th_time: length of time, in minutes, after the valve is closed to determine if + valve operating point is malfunctioning e.g. allow enough time for residue heat to + dissipate from the coil. + + good_folder: name of path showing the folder to save the plots of the correct operating valves + + bad_folder: name of path showing the folder to save the plots of the malfunction valves + + Returns + ------- + None + """ if row['upstream_type'] != 'Mixed_Air_Temperature_Sensor': print('No upstream sensor data available for coil in AHU {} for site {}'.format(row['equip'], row['site'])) #_make_tdiff_vs_vlvpo_plot(vlv_df, row, folder='./') else: - _analyze_vlv(vlv_df, row) + _analyze_vlv(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder) -def analyze(metadata, clean_func, analyze_func): +def _analyze(metadata, clean_func, analyze_func, th_bad_vlv, th_time, good_folder, bad_folder): + """ + Hi level analyze function that runs through each valve queried to detect passing valves + + Parameters + ---------- + metadata: metadata, i.e. view, for the valves that need to be analyzed + + clean_func: specific clean function for the valve in the equipment + + analyze_func: specific analyze function for the valve in the equipment + + th_bad_vlv: temperature difference from long term temperature difference to consider an operating point as malfunctioning + + th_time: length of time, in minutes, after the valve is closed to determine if + valve operating point is malfunctioning e.g. allow enough time for residue heat to + dissipate from the coil. + + good_folder: name of path showing the folder to save the plots of the correct operating valves + + bad_folder: name of path showing the folder to save the plots of the malfunction valves + + Returns + ------- + None + """ # analyze valves for idx, row in metadata.iterrows(): try: @@ -753,7 +822,7 @@ def analyze(metadata, clean_func, analyze_func): vlv_df = clean_func(row) # analyze for passing valves - analyze_func(vlv_df, row) + analyze_func(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder) except: print("Error try to debug") @@ -761,17 +830,59 @@ def analyze(metadata, clean_func, analyze_func): import pdb; pdb.set_trace() continue -# define parameters -eval_start_time = "2018-01-01T00:00:00Z" -eval_end_time = "2018-06-30T00:00:00Z" +def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder): + """ + Main function that runs all the steps of the application -query = _query_and_qualify() -fetch_resp = _fetch(query, eval_start_time, eval_end_time, window=15) + Parameters + ---------- + query: dictionary containing query, sites, and qualify response -# analyze VAV valves -vav_metadata = fetch_resp['vav'].view('dnstream_ta') -analyze(vav_metadata, _clean_vav, _analyze_vlv) + eval_start_time : start date and time in format (yyyy-mm-ddTHH:MM:SSZ) for the thermal + comfort evaluation period + + eval_end_time : end date and time in format (yyyy-mm-ddTHH:MM:SSZ) for the thermal + comfort evaluation period + + window : aggregation window, in minutes, to average the raw measurement data + + th_bad_vlv: temperature difference from long term temperature difference to consider an operating point as malfunctioning -# analyze AHU valves -ahu_metadata = reformat_ahu_view(fetch_resp['ahu']) -analyze(ahu_metadata, _clean_ahu, _analyze_ahu) + th_time: length of time, in minutes, after the valve is closed to determine if + valve operating point is malfunctioning e.g. allow enough time for residue heat to + dissipate from the coil. + + good_folder: name of path showing the folder to save the plots of the correct operating valves + + bad_folder: name of path showing the folder to save the plots of the malfunction valves + + + + Returns + ------- + None + """ + query = _query_and_qualify() + fetch_resp = _fetch(query, eval_start_time, eval_end_time, window) + + # analyze VAV valves + vav_metadata = fetch_resp['vav'].view('dnstream_ta') + _analyze(vav_metadata, _clean_vav, _analyze_vlv, th_bad_vlv, th_time, good_folder, bad_folder) + + # analyze AHU valves + ahu_metadata = reformat_ahu_view(fetch_resp['ahu']) + _analyze(ahu_metadata, _clean_ahu, _analyze_ahu, th_bad_vlv, th_time, good_folder, bad_folder) + + +if __name__ == '__main__': + # define parameters + eval_start_time = "2018-01-01T00:00:00Z" + eval_end_time = "2018-06-30T00:00:00Z" + window = 15 + th_bad_vlv = 5 + th_time = 45 + good_folder = './good_valves' + bad_folder = './bad_valves' + + # Run the app + detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder) \ No newline at end of file From d4c781c133973237ee04a5d270a9d012d5c61d69 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 14:19:00 -0800 Subject: [PATCH 22/83] fixed incorrect dictionary definition --- detect_passing_valves/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index c427400..4a61dfd 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -86,15 +86,15 @@ def _query_and_qualify(): # save queries query['query'] = dict() - query['query']['vav'] = qualify_vav_resp - query['query']['ahu_sa'] = qualify_sa_resp - query['query']['ahu_ra'] = qualify_ra_resp + query['query']['vav'] = vav_query + query['query']['ahu_sa'] = ahu_sa_query + query['query']['ahu_ra'] = ahu_ra_query # save qualify responses query['qualify'] = dict() - query['qualify']['vav'] = vav_query - query['qualify']['ahu_sa'] = ahu_sa_query - query['qualify']['ahu_ra'] = ahu_ra_query + query['qualify']['vav'] = qualify_vav_resp + query['qualify']['ahu_sa'] = qualify_sa_resp + query['qualify']['ahu_ra'] = qualify_ra_resp # save sites query['sites'] = dict() From fc70f33675cb57b9f34ee22843233fc470399acb Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 16:16:51 -0800 Subject: [PATCH 23/83] fixed missing parameter in function --- detect_passing_valves/app.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 4a61dfd..1e4749f 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -289,7 +289,7 @@ def reformat_ahu_view(fetch_resp_ahu): return ahu_metadata[heat_vlv] -def _clean_vav(fetch_resp, row): +def _clean_vav(fetch_resp_vav, row): """ Make a pandas dataframe with relavent vav data for the specific valve and clean from NA values. Calculate temperature difference between @@ -297,7 +297,7 @@ def _clean_vav(fetch_resp, row): Parameters ---------- - fetch_resp : Mortar FetchResponse object + fetch_resp_vav : Mortar FetchResponse object for the vav data row: Pandas series object with metadata for the specific vav valve @@ -308,9 +308,9 @@ def _clean_vav(fetch_resp, row): """ # combine data points in one dataframe - vav_sa = fetch_resp['vav']['dnstream_ta'][row['dnstream_ta_uuid']] - ahu_sa = fetch_resp['vav']['upstream_ta'][row['upstream_ta_uuid']] - vlv_po = fetch_resp['vav']['vlv'][row['vlv_uuid']] + vav_sa = fetch_resp_vav['dnstream_ta'][row['dnstream_ta_uuid']] + ahu_sa = fetch_resp_vav['upstream_ta'][row['upstream_ta_uuid']] + vlv_po = fetch_resp_vav['vlv'][row['vlv_uuid']] vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] @@ -329,14 +329,14 @@ def _clean_vav(fetch_resp, row): return vav_df -def _clean_ahu(fetch_resp, row): +def _clean_ahu(fetch_resp_ahu, row): """ Make a pandas dataframe with relavent ahu data for the specific valve and clean from NA values. Parameters ---------- - fetch_resp : Mortar FetchResponse object + fetch_resp_ahu : Mortar FetchResponse object for the AHU data row: Pandas series object with metadata for the specific vav valve @@ -345,10 +345,10 @@ def _clean_ahu(fetch_resp, row): ahu_df: Pandas dataframe with valve timeseries data """ - dnstream = fetch_resp['ahu']['dnstream_ta'][row['dnstream_ta uuid']] - upstream = fetch_resp['ahu']['upstream_ta'][row['upstream_ta uuid']] + dnstream = fetch_resp_ahu['dnstream_ta'][row['dnstream_ta uuid']] + upstream = fetch_resp_ahu['upstream_ta'][row['upstream_ta uuid']] - vlv_po = fetch_resp['ahu']['ahu_valve'][row['vlv_uuid']] + vlv_po = fetch_resp_ahu['ahu_valve'][row['vlv_uuid']] ahu_df = pd.concat([upstream, dnstream, vlv_po], axis=1) ahu_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] @@ -789,7 +789,7 @@ def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder): _analyze_vlv(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder) -def _analyze(metadata, clean_func, analyze_func, th_bad_vlv, th_time, good_folder, bad_folder): +def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time, good_folder, bad_folder): """ Hi level analyze function that runs through each valve queried to detect passing valves @@ -797,6 +797,8 @@ def _analyze(metadata, clean_func, analyze_func, th_bad_vlv, th_time, good_folde ---------- metadata: metadata, i.e. view, for the valves that need to be analyzed + fetch_resp : Mortar FetchResponse object + clean_func: specific clean function for the valve in the equipment analyze_func: specific analyze function for the valve in the equipment @@ -819,7 +821,7 @@ def _analyze(metadata, clean_func, analyze_func, th_bad_vlv, th_time, good_folde for idx, row in metadata.iterrows(): try: # clean data - vlv_df = clean_func(row) + vlv_df = clean_func(fetch_resp, row) # analyze for passing valves analyze_func(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder) @@ -867,11 +869,11 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th # analyze VAV valves vav_metadata = fetch_resp['vav'].view('dnstream_ta') - _analyze(vav_metadata, _clean_vav, _analyze_vlv, th_bad_vlv, th_time, good_folder, bad_folder) + _analyze(vav_metadata, fetch_resp['vav'], _clean_vav, _analyze_vlv, th_bad_vlv, th_time, good_folder, bad_folder) # analyze AHU valves ahu_metadata = reformat_ahu_view(fetch_resp['ahu']) - _analyze(ahu_metadata, _clean_ahu, _analyze_ahu, th_bad_vlv, th_time, good_folder, bad_folder) + _analyze(ahu_metadata, fetch_resp['ahu'], _clean_ahu, _analyze_ahu, th_bad_vlv, th_time, good_folder, bad_folder) if __name__ == '__main__': From 44396eb6920d833e8afff29cfa355e185e80d1ca Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 1 Dec 2020 16:26:31 -0800 Subject: [PATCH 24/83] Rewrote the app description. --- detect_passing_valves/README.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/detect_passing_valves/README.md b/detect_passing_valves/README.md index 791cc4c..fa57c74 100644 --- a/detect_passing_valves/README.md +++ b/detect_passing_valves/README.md @@ -1,12 +1,5 @@ -# Detect passing valves in VAV terminals +# Detect passing valves in HVAC equipment -This app detects valves that do not close all the way also known as passing valves. +This app detects valves in HVAC equipment that do not close fully even when actuated to a fully closed position, also known as “passing valves”. The application compares fluid temperatures upstream and downstream from the valve and calculates the expected long-term difference between the two fluid streams when the valve is currently closed, and has been closed for some time. The app then analyzes the expected trends with the actual data to determine if the valve is in good operating condition or malfunctioning. -This app produces a CSV file called `passing_valves.csv` when run. Each row is a possible incidence of a passing valve. The CSV file contains the following columns: - -- site -- valve name -- start of incident -- end of incident -- expected temperature difference -- actual temperature difference \ No newline at end of file +This app produces a plot for each analyzed valve with file name formatted as `--.png` when run. Each plot shows a solid green horizontal line that represents the average temperature difference between the downstream and upstream fluids when valve is commanded closed, a dashed purple line shows the expected correct operating behavior trend of the valve (based on the green points), a solid pink horizontal line shows the average temperature difference when the application detected a possible passing valve (based on the red points). The ‘Bad ratio’ value is a ratio of the number of bad operating point values to goodoperating point values. Each file is either allocated in a 'good' or 'bad' folder depending if the analyzed valve is operating correctly (good folder) or malfunctioning (bad folder). \ No newline at end of file From d5c2b3498e0b2f21cf565e7792e83f710c52b9bd Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 4 Dec 2020 13:35:00 -0800 Subject: [PATCH 25/83] added function to subset data by building occupancy --- detect_passing_valves/app.py | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 1e4749f..13bcd19 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -327,8 +327,48 @@ def _clean_vav(fetch_resp_vav, row): # drop values where vav supply air is less than ahu supply air vav_df = vav_df[vav_df['temp_diff'] >= 0] + # drop values outside occupancy hours + # TODO: retrieve HVAC status to subset df when fan is operating + vav_df = occupied_hours_subset(vav_df, occ_str=6, occ_end=18, wkend_str=5) + return vav_df +def occupied_hours_subset(df, occ_str, occ_end, wkend_str=5, timestamp_col=None): + """ + Returns data containing values during building occupancy of Pandas DataFrame + + Parameters + ---------- + df: Pandas dataframe object with timeseries data + + occ_str: float number indicating start of building occupancy + + occ_end: float number indicating end of building occupancy + + wkend_str: int number indicating start of weekend. 5 indicates Saturday and 6 indicates Sunday + + timestamp_col: If timeseries object is not defined in the index of the pandas dataframe then + input the column name containing the timeseries + + Returns + ------- + df_is_occupied: Pandas dataframe with data values during building occupancy hours + """ + # define the timeseries data + if timestamp_col is None: + df_ts = df.index + else: + df_ts = df[timestamp_col] + + bool_str_hr = (df_ts.hour + df_ts.minute/60.0) >= occ_str + bool_end_hr = (df_ts.hour + df_ts.minute/60.0) <= occ_end + bool_is_weekday = df_ts.weekday < 5 # 5 and 6 are Sat and Sun, respectively + + is_occupied = np.logical_and(bool_str_hr, bool_end_hr, bool_is_weekday) + + return df[is_occupied] + + def _clean_ahu(fetch_resp_ahu, row): """ Make a pandas dataframe with relavent ahu data for the specific valve @@ -362,6 +402,9 @@ def _clean_ahu(fetch_resp_ahu, row): # drop na ahu_df = ahu_df.dropna() + # drop values outside occupancy hours + ahu_df = occupied_hours_subset(ahu_df, occ_str=6, occ_end=18, wkend_str=5) + # drop values where vav supply air is less than ahu supply air #ahu_df = ahu_df[ahu_df['temp_diff'] >= 0] From 8986ea5bbc7fa2b734069782422fedbffec26bc1 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 4 Dec 2020 17:37:29 -0800 Subject: [PATCH 26/83] added check on vav supply air temperature --- detect_passing_valves/app.py | 86 +++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 13bcd19..18453e9 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -44,6 +44,13 @@ def _query_and_qualify(): ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . };""" + fan_query = """SELECT * + WHERE { + ?equip rdf:type/rdfs:subClassOf? brick:VAV . + ?equip bf:hasPoint ?air_flow . + ?air_flow rdf:type/rdfs:subClassOf* brick:Supply_Air_Flow_Sensor . + };""" + # define queries to analyze AHU valves ahu_sa_query = """SELECT * WHERE { @@ -71,6 +78,7 @@ def _query_and_qualify(): qualify_vav_resp = client.qualify([vav_query]) qualify_sa_resp = client.qualify([ahu_sa_query]) qualify_ra_resp = client.qualify([ahu_ra_query]) + qualify_fan_resp = client.qualify([fan_query]) if qualify_vav_resp.error != "": print("ERROR: ", qualify_vav_resp.error) @@ -89,12 +97,14 @@ def _query_and_qualify(): query['query']['vav'] = vav_query query['query']['ahu_sa'] = ahu_sa_query query['query']['ahu_ra'] = ahu_ra_query + query['query']['air_flow'] = fan_query # save qualify responses query['qualify'] = dict() query['qualify']['vav'] = qualify_vav_resp query['qualify']['ahu_sa'] = qualify_sa_resp query['qualify']['ahu_ra'] = qualify_ra_resp + query['qualify']['air_flow'] = qualify_fan_resp # save sites query['sites'] = dict() @@ -139,6 +149,10 @@ def _fetch(query, eval_start_time, eval_end_time, window=15): name="dnstream_ta", definition=query['query']['vav'], ), + pymortar.View( + name="air_flow", + definition=query['query']['air_flow'] + ), ], dataFrames=[ pymortar.DataFrame( @@ -152,6 +166,17 @@ def _fetch(query, eval_start_time, eval_end_time, window=15): ) ] ), + pymortar.DataFrame( + name="air_flow", + aggregation=pymortar.MEAN, + window="15m", + timeseries=[ + pymortar.Timeseries( + view="air_flow", + dataVars=["?air_flow"], + ) + ] + ), pymortar.DataFrame( name="dnstream_ta", aggregation=pymortar.MEAN, @@ -307,13 +332,25 @@ def _clean_vav(fetch_resp_vav, row): """ + df_flow = get_vav_flow(fetch_resp_vav, row, fillna=None) + # combine data points in one dataframe vav_sa = fetch_resp_vav['dnstream_ta'][row['dnstream_ta_uuid']] ahu_sa = fetch_resp_vav['upstream_ta'][row['upstream_ta_uuid']] vlv_po = fetch_resp_vav['vlv'][row['vlv_uuid']] - vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) - vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] + if df_flow is not None: + vav_df = pd.concat([ahu_sa, vav_sa, vlv_po, df_flow], axis=1) + vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po', 'air_flow'] + + # drop values where there is no air flow + vav_df = vav_df.loc[vav_df['air_flow'] > 0] + else: + vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) + vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] + + # drop values outside occupancy hours + vav_df = occupied_hours_subset(vav_df, occ_str=6, occ_end=18, wkend_str=5) # identify when valve is open vav_df['vlv_open'] = vav_df['vlv_po'] > 0 @@ -327,12 +364,49 @@ def _clean_vav(fetch_resp_vav, row): # drop values where vav supply air is less than ahu supply air vav_df = vav_df[vav_df['temp_diff'] >= 0] - # drop values outside occupancy hours - # TODO: retrieve HVAC status to subset df when fan is operating - vav_df = occupied_hours_subset(vav_df, occ_str=6, occ_end=18, wkend_str=5) - return vav_df +def get_vav_flow(fetch_resp_vav, row, fillna=None): + """ + Return VAV supply air flow + + Parameters + ---------- + fetch_resp_vav : Mortar FetchResponse object for the vav data + + row: Pandas series object with metadata for the specific vav valve + + fillna: Method to use for filling na values in dataframe. Options: backfill, bfill, pad, ffill, None + + Returns + ------- + df_flow: Pandas dataframe with vav supply air flow timeseries data + """ + + # fine corresponding air flow sensor for vav + flow_view = fetch_resp_vav.view('air_flow') + flow_meta = flow_view.loc[np.logical_and(flow_view['equip'] == row['equip'], flow_view['site'] == row['site'])] + + fidx = 0 + if flow_meta.shape[0] > 1: + print("Multiple airflow sensors found for VAV {} in site {}! \ + Please check or press 'c' to continue using the first sensor.".format(row['equip'], row['site'])) + print(flow_meta) + import pdb; pdb.set_trace() + if flow_meta.shape[0] == 0: + return None + + # return air flow timeseries data + flow_id = flow_meta.loc[flow_meta.index[fidx], 'air_flow_uuid'] + df_flow = fetch_resp_vav['air_flow'].loc[:, flow_id] + + if fillna is not None: + # fill na values + df_flow = df_flow.fillna(method=fillna) + + return df_flow + + def occupied_hours_subset(df, occ_str, occ_end, wkend_str=5, timestamp_col=None): """ Returns data containing values during building occupancy of Pandas DataFrame From d90e45472897a6bac49840a2c43dc618c175a923 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 21 Dec 2020 16:57:43 -0800 Subject: [PATCH 27/83] added function to delete transient data points --- detect_passing_valves/app.py | 63 +++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 18453e9..262d63b 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -615,7 +615,7 @@ def check_folder_exist(folder): if not os.path.exists(folder): os.makedirs(folder) -def calc_long_t_diff(vlv_df, vlv_open=False): +def calc_long_t_diff(vlv_df, vlv_open=False, row=None): """ Calculate statistic on difference between down- and up- stream temperatures to determine the long term temperature difference. @@ -635,13 +635,58 @@ def calc_long_t_diff(vlv_df, vlv_open=False): # long-term average when valve is open df_vlv_close = vlv_df[vlv_df['vlv_open']] else: - # long-term average when valve is closed - df_vlv_close = vlv_df[~vlv_df['vlv_open']] + # long-term average when valve is closed and only values after th_time minutes + # after valve has closed is included in average + + #df_vlv_close = vlv_df[~vlv_df['vlv_open']] + df_vlv_close = return_delayed_df(vlv_df, th_time=25, window=15) + if row is not None: + check_folder_exist("./csv_data") + _name = "{}-{}-{}_dat".format(row['site'], row['equip'], row['vlv']) + df_vlv_close.to_csv(join("./csv_data", _name + '.csv')) + + df_vlv_close = df_vlv_close[np.logical_and(df_vlv_close['cons_ts_vlv_c'], df_vlv_close['steady'])] long_t = df_vlv_close['temp_diff'].describe() return long_t +def return_delayed_df(df_subset, th_time, window): + """ + Return dataframe with row values that are X time after a changed state + """ + + min_ts = int(th_time/window) + (th_time % window > 0) + min_tst = pd.Timedelta(th_time, unit='min') + + # only get consecutive timestamps datapoints + ts = pd.Series(df_subset.index) + ts_int = pd.Timedelta(window, unit='min') + cons_ts = ((ts - ts.shift(-1)).abs() <= ts_int) | (ts.diff() <= ts_int) + + if (len(cons_ts) < min_ts) | ~(np.any(cons_ts)): + return None + + df_subset['cons_ts'] = np.array(cons_ts) + df_subset['cons_ts_vlv_c'] = np.logical_and(~df_subset['vlv_open'], df_subset['cons_ts']) + df_subset['same'] = df_subset['cons_ts_vlv_c'].astype(int).diff().ne(0).cumsum() + + df_cons_ts = df_subset.copy() + + # subset by consecutive times that exceed th_time + lal = df_cons_ts.groupby('same') + + steady = [] + for grp in lal.groups.keys(): + for ts in lal.groups[grp]: + init_ts = lal.groups[grp][0] + steady.append(init_ts+min_tst < ts) + + df_cons_ts['steady'] = np.array(steady) + + return df_cons_ts + + def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): """ Make plot showing the correct and bad operating points of the valve control along with helper annotations @@ -725,7 +770,7 @@ def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): """ # find datapoints that exceed long-term temperature difference - min_ts = int(th_time/window) + min_ts = int(th_time/window) + (th_time % window > 0) th_exceed = np.logical_and((vlv_df['temp_diff'] >= long_t), ~(vlv_df['vlv_open'])) df_bad = vlv_df[th_exceed] @@ -738,8 +783,8 @@ def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): return None #df_bad['cons_ts'] = np.array(cons_ts) - df_bad.loc[:, 'cons_ts'] = np.array(cons_ts) - df_bad.loc[:, 'same'] = df_bad['cons_ts'].astype(int).diff().ne(0).cumsum() + df_bad['cons_ts'] = np.array(cons_ts) + df_bad['same'] = df_bad['cons_ts'].astype(int).diff().ne(0).cumsum() #df_bad['same'] = df_bad['cons_ts'].astype(int).diff().ne(0).cumsum() df_cons_ts = df_bad[df_bad['cons_ts']] @@ -783,6 +828,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv check_folder_exist(bad_folder) check_folder_exist(good_folder) + # container for holding types of faults + bad_klass = [] + if vlv_df.shape[0] == 0: print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) return @@ -806,8 +854,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv return # calculate long-term temp diff when valve is closed - bad_klass = [] - long_tc = calc_long_t_diff(vlv_df) + long_tc = calc_long_t_diff(vlv_df, row=row) long_to = calc_long_t_diff(vlv_df, vlv_open=True) From 99a08af2b7551363c6ee0412b063802af8a82a69 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 23 Dec 2020 12:20:43 -0800 Subject: [PATCH 28/83] do a more detailed analysis on bldg data with airflow vals --- detect_passing_valves/app.py | 237 +++++++++++++++++++++++++++++------ 1 file changed, 196 insertions(+), 41 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 262d63b..b8d73b1 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -3,11 +3,12 @@ import pandas as pd import numpy as np import os +import time from os.path import join import matplotlib.pyplot as plt from scipy.optimize import curve_fit - +from scipy.stats import gaussian_kde def _query_and_qualify(): """ @@ -343,15 +344,10 @@ def _clean_vav(fetch_resp_vav, row): vav_df = pd.concat([ahu_sa, vav_sa, vlv_po, df_flow], axis=1) vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po', 'air_flow'] - # drop values where there is no air flow - vav_df = vav_df.loc[vav_df['air_flow'] > 0] else: vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] - # drop values outside occupancy hours - vav_df = occupied_hours_subset(vav_df, occ_str=6, occ_end=18, wkend_str=5) - # identify when valve is open vav_df['vlv_open'] = vav_df['vlv_po'] > 0 @@ -366,6 +362,25 @@ def _clean_vav(fetch_resp_vav, row): return vav_df + +def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5): + """ + Drop dataframe data rows for timeseries that are during unoccupied hours + """ + + if 'air_flow' in df.columns: + # drop values where there is no air flow + xs, ys = density_data(df['air_flow'], rescale_dat=df['temp_diff']) + min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) + + df = df.loc[df['air_flow'] > xs[min_idx[0]]] + else: + # drop values outside occupancy hours + df = occupied_hours_subset(df, occ_str, occ_end, wkend_str) + + return df + + def get_vav_flow(fetch_resp_vav, row, fillna=None): """ Return VAV supply air flow @@ -390,9 +405,9 @@ def get_vav_flow(fetch_resp_vav, row, fillna=None): fidx = 0 if flow_meta.shape[0] > 1: print("Multiple airflow sensors found for VAV {} in site {}! \ - Please check or press 'c' to continue using the first sensor.".format(row['equip'], row['site'])) + Please check. Will continue using the first sensor.".format(row['equip'], row['site'])) print(flow_meta) - import pdb; pdb.set_trace() + time.sleep(3) if flow_meta.shape[0] == 0: return None @@ -476,9 +491,6 @@ def _clean_ahu(fetch_resp_ahu, row): # drop na ahu_df = ahu_df.dropna() - # drop values outside occupancy hours - ahu_df = occupied_hours_subset(ahu_df, occ_str=6, occ_end=18, wkend_str=5) - # drop values where vav supply air is less than ahu supply air #ahu_df = ahu_df[ahu_df['temp_diff'] >= 0] @@ -511,7 +523,7 @@ def scale_0to1(vals): return scaled_vals -def rescale_fit(scaled_vals, vals): +def rescale_fit(scaled_vals, vals=None, max_val=None, min_val=None): """ Rescale values of pandas series that are 0 to 1 to match the interval of another pandas series object values @@ -521,14 +533,24 @@ def rescale_fit(scaled_vals, vals): scaled_vals: Pandas series object with values scaled from 0 to 1 and needs to be unscaled vals: Pandas series object or Pandas dataframe colum with unnormalized values. - This is used to extract max and min to unscaled the scaled_vals. + This is used to extract max and min to unscaled the scaled_vals. + + max_val: a float number indicating the maximum value to rescale vector. Must + also define min_val. + + min_val: a float number indicating the minimum value to rescale vector. Must + also define max_val. Returns ------- unscaled_vals: Pandas series object of unscaled values """ - max_val = vals.max() - min_val = vals.min() + + if vals is not None: + max_val = vals.max() + min_val = vals.min() + elif (max_val is None) or (min_val is None): + raise ValueError('Need to define vals dataframe or both maximum and minimum values for rescale!') unscaled_vals = min_val + scaled_vals*(max_val - min_val) @@ -640,10 +662,13 @@ def calc_long_t_diff(vlv_df, vlv_open=False, row=None): #df_vlv_close = vlv_df[~vlv_df['vlv_open']] df_vlv_close = return_delayed_df(vlv_df, th_time=25, window=15) + + if df_vlv_close is None: + return None + if row is not None: - check_folder_exist("./csv_data") _name = "{}-{}-{}_dat".format(row['site'], row['equip'], row['vlv']) - df_vlv_close.to_csv(join("./csv_data", _name + '.csv')) + df_vlv_close.to_csv(join(project_folder, "csv_data", _name + '.csv')) df_vlv_close = df_vlv_close[np.logical_and(df_vlv_close['cons_ts_vlv_c'], df_vlv_close['steady'])] @@ -687,6 +712,55 @@ def return_delayed_df(df_subset, th_time, window): return df_cons_ts +def return_extreme_points(dat, type_of_extreme=None, n_modes=None, sort=True): + """ + Return the peak and troughs of a multimodal distribution + """ + + a = np.diff(dat) + asign = np.sign(a) + + signchg = ((np.roll(asign, 1) - asign) != 0).astype(int) + idx = np.where(signchg == 1)[0] + + # delete extreme points + if 0 in idx: + idx = np.delete(idx, 0) + + if (len(dat) - 1) in idx: + idx = np.delete(idx, len(dat)-1) + + idx_num = len(idx) + type_of = [] + if dat[idx[0]] > dat[idx[1]]: + # if true then starting inflection point is a maximum + type_of = np.array(['max']*idx_num) + type_of[1:][::2] = 'min' + elif dat[idx[0]] < dat[idx[1]]: + # if true then starting inflection point is a minimum + type_of = np.array(['min']*idx_num) + type_of[1:][::2] = 'max' + + # return requested inflection points + if type_of_extreme == 'max': + idx = idx[type_of == 'max'] + elif type_of_extreme == 'min': + idx = idx[type_of == 'min'] + else: + print('Returning all inflection points') + + if sort or n_modes is not None: + idx = idx[np.argsort(dat[idx])] + + if n_modes is not None: + if type_of_extreme == 'max': + idx = idx[(-1*n_modes):] + elif type_of_extreme == 'min': + idx = idx[:n_modes] + + return idx + + def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): """ Make plot showing the correct and bad operating points of the valve control along with helper annotations @@ -747,6 +821,70 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N plt.close() +def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): + """ + Create temperature difference versus air flow plots + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + row: Pandas series object with metadata for the specific valve + + folder: name of path to save the plot image + + Returns + ------- + None + """ + + # plot temperature difference vs valve position + fig, ax = plt.subplots(figsize=(8,4.5)) + ax.set_ylabel('Temperature difference [°F]') + ax.set_xlabel('Air flow [cfm]') + ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') + + vlv_df['color_open'] = '#640064' + vlv_df.loc[vlv_df['vlv_open'], 'color_open'] = '#006400' + + ax.scatter(x=vlv_df['air_flow'], y=vlv_df['temp_diff'], color = vlv_df['color_open'], alpha=1/3, s=10) + + # create density plot for air flow + xs, ys = density_data(vlv_df['air_flow'], rescale_dat=vlv_df['temp_diff']) + ax.plot(xs, ys) + + # find modes of the distribution and the trough before/after the modes + max_idx = return_extreme_points(ys, type_of_extreme='max', n_modes=2) + min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) + + ax.scatter(x=xs[max_idx], y=ys[max_idx], color = '#ff0000', alpha=1, s=35) + ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35) + + plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) + plt.savefig(join(folder, plt_name + '.png')) + plt.close() + + +def density_data(dat, rescale_dat=None): + """ + + """ + #create data for density plot + density = gaussian_kde(dat) + xs = np.linspace(0, max(dat), 200) + + density.covariance_factor = lambda : 0.25 + density._compute_covariance() + + if isinstance(rescale_dat, (pd.Series, np.ndarray)): + ys = rescale_fit(scale_0to1(density(xs)), max_val=np.percentile(rescale_dat, 95), min_val=0) + elif isinstance(rescale_dat, str) and 'norm' in rescale_dat: + ys = scale_0to1(density(xs)) + else: + ys = density(xs) + + return xs, ys + def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): """ @@ -799,7 +937,7 @@ def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): return bad_vlv.drop(columns=['cons_ts']) -def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valves', bad_folder='./bad_valves'): +def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valves', bad_folder='./bad_valves', project_folder='./'): """ Analyze each valve and detect for passing valves @@ -824,17 +962,26 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv None """ - # check if holding folders exist - check_folder_exist(bad_folder) - check_folder_exist(good_folder) + # check for empty dataframe + if vlv_df.empty: + print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) + return - # container for holding types of faults - bad_klass = [] + if 'air_flow' in vlv_df.columns: + # plot temp diff vs air flow + _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots')) - if vlv_df.shape[0] == 0: - print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) + # drop data that occurs during unoccupied hours + vlv_df = drop_unoccupied_dat(vlv_df, occ_str=6, occ_end=18, wkend_str=5) + + if vlv_df.empty: + print("'{}' in site {} has no data after hours of \ + occupancy check! Skipping...".format(row['vlv'], row['site'])) return + # container for holding types of faults + bad_klass = [] + # determine if valve datastream has open and closed data bool_type = vlv_df['vlv_open'].value_counts().index @@ -844,15 +991,16 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv long_to = calc_long_t_diff(vlv_df, vlv_open=True) if long_to['50%'] < th_bad_vlv: print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=bad_folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=join(project_folder, bad_folder)) else: # only closed valve data - long_tc = calc_long_t_diff(vlv_df) + long_tc = calc_long_t_diff(vlv_df, row=row) if long_tc['50%'] > th_bad_vlv: print("Probable passing valve '{}' in site {}".format(row['vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=bad_folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=join(project_folder, bad_folder)) return + # TODO: Figure out what to do if long_tc is None! # calculate long-term temp diff when valve is closed long_tc = calc_long_t_diff(vlv_df, row=row) long_to = calc_long_t_diff(vlv_df, vlv_open=True) @@ -899,15 +1047,15 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv bad_klass.append(True) if len(bad_klass) > 0: - folder = bad_folder + folder = join(project_folder, bad_folder) if bad_ratio > 5: print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) if len(bad_klass) > 1: print("{} percentage of time is leaking!".format(bad_ratio)) else: - folder = good_folder + folder = join(project_folder, good_folder) else: - folder = good_folder + folder = join(project_folder, good_folder) if bad_vlv is not None: # colorize good and bad points @@ -921,7 +1069,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv # grps = list(lal.groups.keys()) # bad_vlv.loc[lal.groups[grps[0]]] -def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder): +def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder, project_folder): """ Helper function to analyze AHU valves @@ -950,10 +1098,10 @@ def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder): print('No upstream sensor data available for coil in AHU {} for site {}'.format(row['equip'], row['site'])) #_make_tdiff_vs_vlvpo_plot(vlv_df, row, folder='./') else: - _analyze_vlv(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder) + _analyze_vlv(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) -def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time, good_folder, bad_folder): +def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time, good_folder, bad_folder, project_folder): """ Hi level analyze function that runs through each valve queried to detect passing valves @@ -988,7 +1136,7 @@ def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time vlv_df = clean_func(fetch_resp, row) # analyze for passing valves - analyze_func(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder) + analyze_func(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) except: print("Error try to debug") @@ -996,7 +1144,7 @@ def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time import pdb; pdb.set_trace() continue -def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder): +def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder, project_folder): """ Main function that runs all the steps of the application @@ -1028,16 +1176,22 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th ------- None """ + # check if holding folders exist + check_folder_exist(join(project_folder, bad_folder)) + check_folder_exist(join(project_folder, good_folder)) + check_folder_exist(join(project_folder, "air_flow_plots")) + check_folder_exist(join(project_folder, "csv_data")) + query = _query_and_qualify() fetch_resp = _fetch(query, eval_start_time, eval_end_time, window) # analyze VAV valves vav_metadata = fetch_resp['vav'].view('dnstream_ta') - _analyze(vav_metadata, fetch_resp['vav'], _clean_vav, _analyze_vlv, th_bad_vlv, th_time, good_folder, bad_folder) + _analyze(vav_metadata, fetch_resp['vav'], _clean_vav, _analyze_vlv, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) # analyze AHU valves ahu_metadata = reformat_ahu_view(fetch_resp['ahu']) - _analyze(ahu_metadata, fetch_resp['ahu'], _clean_ahu, _analyze_ahu, th_bad_vlv, th_time, good_folder, bad_folder) + _analyze(ahu_metadata, fetch_resp['ahu'], _clean_ahu, _analyze_ahu, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) if __name__ == '__main__': @@ -1047,8 +1201,9 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th window = 15 th_bad_vlv = 5 th_time = 45 - good_folder = './good_valves' - bad_folder = './bad_valves' + project_folder = './with_airflow_checks' + good_folder = 'good_valves' + bad_folder = 'bad_valves' # Run the app - detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder) \ No newline at end of file + detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) \ No newline at end of file From 1420d24797323cbce012de9ace16a145ba8f6306 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 30 Dec 2020 13:45:27 -0800 Subject: [PATCH 29/83] added functionality to return inflection point of sigmoid curve --- detect_passing_valves/app.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index b8d73b1..2dde0bf 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -588,6 +588,8 @@ def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff'): Returns ------- df_fit: Pandas dataframe object with y_fitted values to a logistic model + + popt: an array of the optimized parameters, slope and inflection point of the sigmoid function """ # fit the curve @@ -597,6 +599,7 @@ def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff'): # calculate fitted temp difference values est_k, est_x0 = popt + popt[1] = rescale_fit(popt[1], df[x_col]) y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), df[y_col]) y_fitted.name = 'y_fitted' @@ -604,17 +607,17 @@ def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff'): df_fit = pd.concat([df[x_col], y_fitted], axis=1) df_fit = df_fit.sort_values(by=x_col) - return df_fit + return df_fit, popt def try_limit_dat_fit_model(vlv_df, df_fraction): # calculate fit model nrows, ncols = vlv_df.shape some_pts = np.random.choice(nrows, int(nrows*df_fraction)) try: - df_fit = build_logistic_model(vlv_df.iloc[some_pts]) + df_fit, popt = build_logistic_model(vlv_df.iloc[some_pts]) except RuntimeError: try: - df_fit = build_logistic_model(vlv_df) + df_fit, popt = build_logistic_model(vlv_df) except RuntimeError: print("No regression found") df_fit = None From 57d92953a37b9e404bd55bab07ab5839102fc9c5 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 30 Dec 2020 17:31:58 -0800 Subject: [PATCH 30/83] added functionality to use air flow data to detect passing valves --- detect_passing_valves/app.py | 510 ++++++++++++++++++++++++----------- 1 file changed, 358 insertions(+), 152 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 2dde0bf..a713e86 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -365,7 +365,22 @@ def _clean_vav(fetch_resp_vav, row): def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5): """ - Drop dataframe data rows for timeseries that are during unoccupied hours + Drop data rows from dataframe for timeseries that are during unoccupied hours. Uses airflow + data if available else it uses building occupancy hours. + + Parameters + ---------- + df: Pandas dataframe object with timeseries data + + occ_str: float number indicating start of building occupancy + + occ_end: float number indicating end of building occupancy + + wkend_str: int number indicating start of weekend. 5 indicates Saturday and 6 indicates Sunday + + Returns + ------- + df: Pandas dataframe with data values during building occupancy hours """ if 'air_flow' in df.columns: @@ -565,7 +580,7 @@ def sigmoid(x, k, x0): ---------- x: independent variable k: slope of the sigmoid function - x0: midpoint of the sigmoid function + x0: midpoint/inflection point of the sigmoid function Returns ------- @@ -592,20 +607,24 @@ def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff'): popt: an array of the optimized parameters, slope and inflection point of the sigmoid function """ - # fit the curve - scaled_pos = scale_0to1(df[x_col]) - scaled_t = scale_0to1(df[y_col]) - popt, pcov = curve_fit(sigmoid, scaled_pos, scaled_t) - - # calculate fitted temp difference values - est_k, est_x0 = popt - popt[1] = rescale_fit(popt[1], df[x_col]) - y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), df[y_col]) - y_fitted.name = 'y_fitted' - - # sort values - df_fit = pd.concat([df[x_col], y_fitted], axis=1) - df_fit = df_fit.sort_values(by=x_col) + try: + # fit the curve + scaled_pos = scale_0to1(df[x_col]) + scaled_t = scale_0to1(df[y_col]) + popt, pcov = curve_fit(sigmoid, scaled_pos, scaled_t) + + # calculate fitted temp difference values + est_k, est_x0 = popt + popt[1] = rescale_fit(popt[1], df[x_col]) + y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), df[y_col]) + y_fitted.name = 'y_fitted' + + # sort values + df_fit = pd.concat([df[x_col], y_fitted], axis=1) + df_fit = df_fit.sort_values(by=x_col) + except RuntimeError: + print("Model unabled to be developed\n") + return None, None return df_fit, popt @@ -640,69 +659,76 @@ def check_folder_exist(folder): if not os.path.exists(folder): os.makedirs(folder) -def calc_long_t_diff(vlv_df, vlv_open=False, row=None): +def calc_long_t_diff(vlv_df): """ - Calculate statistic on difference between down- and up- - stream temperatures to determine the long term temperature difference. + Calculate statistics on difference between down- and up- + stream temperatures to determine the long term temperature difference + when valve is closed. Parameters ---------- vav_df: Pandas dataframe with valve timeseries data - vlv_open: boolean define if the statistics are performed on data that has - valve open (True) or closed (False) - Returns ------- long_t: dictionary object with statisitics of temperature difference """ - if vlv_open: - # long-term average when valve is open - df_vlv_close = vlv_df[vlv_df['vlv_open']] - else: - # long-term average when valve is closed and only values after th_time minutes - # after valve has closed is included in average - #df_vlv_close = vlv_df[~vlv_df['vlv_open']] - df_vlv_close = return_delayed_df(vlv_df, th_time=25, window=15) - - if df_vlv_close is None: - return None - - if row is not None: - _name = "{}-{}-{}_dat".format(row['site'], row['equip'], row['vlv']) - df_vlv_close.to_csv(join(project_folder, "csv_data", _name + '.csv')) + if vlv_df is None: + return None - df_vlv_close = df_vlv_close[np.logical_and(df_vlv_close['cons_ts_vlv_c'], df_vlv_close['steady'])] + df_vlv_close = vlv_df.loc[np.logical_and(df_vlv_close['cons_ts_vlv_c'], df_vlv_close['steady'])] + if df_vlv_close is None: + return None long_t = df_vlv_close['temp_diff'].describe() return long_t -def return_delayed_df(df_subset, th_time, window): +def analyze_timestamps(vlv_df, th_time, window, row=None): """ - Return dataframe with row values that are X time after a changed state + Analyze timestamps and valve operation in a pandas dataframe to determine which row values + are th_time minutes after a changed state e.g. determine which data corresponds + to steady-state and transient values. + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + th_time: length of time, in minutes, after the valve is closed to determine if + valve operating point is malfunctioning e.g. allow enough time for residue heat to + dissipate from the coil. Recommended time for reheat coils > 12 minutes. + + window : aggregation window, in minutes, to average the raw measurement data + + row: Pandas series object with metadata for the specific vav valve + + Returns + ------- + vav_df: same input pandas dataframe but with added columns indicating: + cons_ts: boolean indicating consecutive timestamps + cons_ts_vlv_c: boolean indicating consecutive timestamps when valve is commanded closed + same: boolean indicating group number of cons_ts_vlv_c + steady: boolean indicating if the timestamp is in steady state condition """ min_ts = int(th_time/window) + (th_time % window > 0) min_tst = pd.Timedelta(th_time, unit='min') # only get consecutive timestamps datapoints - ts = pd.Series(df_subset.index) + ts = pd.Series(vlv_df.index) ts_int = pd.Timedelta(window, unit='min') cons_ts = ((ts - ts.shift(-1)).abs() <= ts_int) | (ts.diff() <= ts_int) if (len(cons_ts) < min_ts) | ~(np.any(cons_ts)): return None - df_subset['cons_ts'] = np.array(cons_ts) - df_subset['cons_ts_vlv_c'] = np.logical_and(~df_subset['vlv_open'], df_subset['cons_ts']) - df_subset['same'] = df_subset['cons_ts_vlv_c'].astype(int).diff().ne(0).cumsum() - - df_cons_ts = df_subset.copy() + vlv_df.loc[:, 'cons_ts'] = np.array(cons_ts) + vlv_df.loc[:, 'cons_ts_vlv_c'] = np.logical_and(~vlv_df['vlv_open'], vlv_df['cons_ts']) + vlv_df.loc[:, 'same'] = vlv_df['cons_ts_vlv_c'].astype(int).diff().ne(0).cumsum() # subset by consecutive times that exceed th_time - lal = df_cons_ts.groupby('same') + lal = vlv_df.groupby('same') steady = [] for grp in lal.groups.keys(): @@ -710,14 +736,35 @@ def return_delayed_df(df_subset, th_time, window): init_ts = lal.groups[grp][0] steady.append(init_ts+min_tst < ts) - df_cons_ts['steady'] = np.array(steady) + vlv_df.loc[:, 'steady'] = np.array(steady) - return df_cons_ts + # save csv data if row is defined + if row is not None: + _name = "{}-{}-{}_dat".format(row['site'], row['equip'], row['vlv']) + vlv_df.to_csv(join(project_folder, "csv_data", _name + '.csv')) + + return vlv_df def return_extreme_points(dat, type_of_extreme=None, n_modes=None, sort=True): """ - Return the peak and troughs of a multimodal distribution + Return the peak and troughs of a multimodal distribution of a vector. + + Parameters + ---------- + dat: vector of data points to develop a distribution + + type_of_extreme: type of extremes to return. If None it will return minimum + and maximum points. + + n_modes: number of distribution peaks or troughs to return. If greater than 1 + it will return the largest or smallest. + + sort: sort the peak/trough values from smallest to largest. + + Returns + ------- + idx: indeces of the peaks or troughs of the multimodal distribution. """ a = np.diff(dat) @@ -847,7 +894,7 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): ax.set_xlabel('Air flow [cfm]') ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') - vlv_df['color_open'] = '#640064' + vlv_df.loc[:, 'color_open'] = '#640064' vlv_df.loc[vlv_df['vlv_open'], 'color_open'] = '#006400' ax.scatter(x=vlv_df['air_flow'], y=vlv_df['temp_diff'], color = vlv_df['color_open'], alpha=1/3, s=10) @@ -870,7 +917,22 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): def density_data(dat, rescale_dat=None): """ - + Create a kernel-density estimate using Gaussian kernels and rescale to + match the specific valve data. + + Parameters + ---------- + dat: vector of data points to develop a distribution + + rescale_dat: If not None, the data vector is used to determine the peak for rescaling + the y values of the density function. + If rescale_dat is define as 'norm', ys will be normalized from 0 to 1. + + Returns + ------- + xs: x values of the density function + + ys: y values of the density function """ #create data for density plot density = gaussian_kde(dat) @@ -879,18 +941,23 @@ def density_data(dat, rescale_dat=None): density.covariance_factor = lambda : 0.25 density._compute_covariance() + # unscaled y values of density + us_ys = density(xs) + + # rescale if rescale_dat is not None if isinstance(rescale_dat, (pd.Series, np.ndarray)): - ys = rescale_fit(scale_0to1(density(xs)), max_val=np.percentile(rescale_dat, 95), min_val=0) + ys = rescale_fit(scale_0to1(us_ys), max_val=np.percentile(rescale_dat, 95), min_val=0) elif isinstance(rescale_dat, str) and 'norm' in rescale_dat: - ys = scale_0to1(density(xs)) + ys = scale_0to1(us_ys) else: - ys = density(xs) + ys = us_ys return xs, ys -def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): +def find_bad_vlv_operation(vlv_df, long_t, window): """ - + Determine which timeseries values are data from probable passing valves and return + a pandas dataframe of only 'bad' values. Parameters ---------- @@ -899,48 +966,140 @@ def find_bad_vlv_operation(vlv_df, long_t, th_time=45, window=15): long_t: long-term temperature difference between down and up air streams when valve is commanded close for correct operation - th_time: length of time, in minutes, after the valve is closed to determine if - valve operating point is malfunctioning e.g. allow enough time for residue heat to - dissipate from the coil. - window : aggregation window, in minutes, to average the raw measurement data Returns ------- - bad_vlv: pandas dataframe object with the time intervals that the valve is malfunctioning + df_bad: pandas dataframe object with the time intervals that the valve is malfunctioning + + pass_type: dictionary with failure modes listed, if any. Possible failure modes are: + long_term_fail: long term valve failure if valve seems to be passing for more than number X + minutes defined in global parameter 'long_term_fail'. Default 300 minutes (5 hours). + short_term_fail: intermittent valve failure if valve seems to be passing for short periods + (X minutes defined in global parameter 'shrt_term_fail') due to control errors, + mechanical/electrical problems, or other. Default 60 minutes (1 hour). """ + pass_type = dict() + # find datapoints that exceed long-term temperature difference - min_ts = int(th_time/window) + (th_time % window > 0) - th_exceed = np.logical_and((vlv_df['temp_diff'] >= long_t), ~(vlv_df['vlv_open'])) + exceed_long_t = vlv_df['temp_diff'] >= long_t + + # TODO: Compare passing valve time to time it is actually open + # # determine time interval that valve is open + # open_grp = df_vlv_close.loc[np.logical_and(np.logical_and(vlv_df['vlv_open'], vlv_df['cons_ts']), exceed_long_t)].groupby('same') + # op_count_stats = open_grp['same'].count().describe() + + # subset data by consecutive steady state values when valve is commanded closed and + # exceeds long-term temperature difference + th_exceed = np.logical_and(np.logical_and(vlv_df['cons_ts_vlv_c'], vlv_df['steady']), exceed_long_t) df_bad = vlv_df[th_exceed] - # only get consecutive timestamps datapoints - ts = pd.Series(df_bad.index) - ts_int = pd.Timedelta(window, unit='min') - cons_ts = ((ts - ts.shift(-1)).abs() <= ts_int) | (ts.diff() <= ts_int) + if df_bad.empty: + return None, dict() - if (len(cons_ts) < min_ts) | ~(np.any(cons_ts)): - return None + # analyze 'bad' dataframe for possible passing valve + bad_grp = df_bad.groupby('same') + bad_grp_count = bad_grp['same'].count() - #df_bad['cons_ts'] = np.array(cons_ts) - df_bad['cons_ts'] = np.array(cons_ts) - df_bad['same'] = df_bad['cons_ts'].astype(int).diff().ne(0).cumsum() - #df_bad['same'] = df_bad['cons_ts'].astype(int).diff().ne(0).cumsum() + max_idx = np.argmax(bad_grp_count) + max_grp = bad_grp.groups[bad_grp_count.index[max_idx]] - df_cons_ts = df_bad[df_bad['cons_ts']] + if len(max_grp) > 1: + max_passing_time = max_grp[-1] - max_grp[0] + else: + max_passing_time = pd.Timedelta(0, unit='min') - # subset by consecutive times that exceed th_time - lal = df_cons_ts.groupby('same') - grp_exceed = lal['same'].count()[lal['same'].count() >= min_ts].index + # detect long term failures + if max_passing_time > pd.Timedelta(long_term_fail, unit='min'): + ts_seconds = max_passing_time.seconds + ts_days = max_passing_time.days * 3600 * 24 + pass_type['long_term_fail'] = (ts_days+ts_seconds)/60.0 - exceeded = [x in grp_exceed for x in df_cons_ts['same']] - bad_vlv = df_cons_ts[exceeded] + # detect short term failures + if any((bad_grp_count*window) > shrt_term_fail): + shrt_term_fail_times = bad_grp_count[(bad_grp_count*window) > shrt_term_fail]*window + pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count()) - return bad_vlv.drop(columns=['cons_ts']) + return df_bad, pass_type -def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valves', bad_folder='./bad_valves', project_folder='./'): +def print_passing_mgs(row): + """ + Print message to user when passing valve is probable + + Parameters + ---------- + row: Pandas series object with metadata for the specific valve + + Returns + ------- + None + """ + print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) + + +def analyze_only_open(vlv_df, row, th_bad_vlv, project_folder): + """ + Analyze valve data when there is only open valve data. + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + row: Pandas series object with metadata for the specific valve + + th_bad_vlv: temperature difference from long term temperature difference to consider an operating point as malfunctioning + + project_folder: name of path for the project and used to save the plot. + + Returns + ------- + pass_type: dictionary with failure modes listed, if any. Possible failure modes are: + non_responsive_fail: failure when the valve is open but the median temperature + difference when the valve is command open but never goes above the above + the th_bad_vlv threshold. + """ + pass_type = dict() + + long_to = vlv_df[vlv_df['vlv_open']]['temp_diff'].describe() + if long_to['50%'] < th_bad_vlv: + print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) + pass_type['non_responsive_fail'] = round(long_to['50%'] - th_bad_vlv, 2) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=join(project_folder, bad_folder)) + + return pass_type + +def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): + """ + Analyze valve data when there is only closed valve data. + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + row: Pandas series object with metadata for the specific valve + + th_bad_vlv: temperature difference from long term temperature difference to consider an operating point as malfunctioning + + project_folder: name of path for the project and used to save the plot. + + Returns + ------- + pass_type: dictionary with failure modes listed, if any. Possible failure modes are: + simple_fail: failure when the median temperature difference when the valve is commanded + closed if above the th_bad_vlv threshold. + """ + pass_type = dict() + long_tc = calc_long_t_diff(vlv_df) + if long_tc['50%'] > th_bad_vlv: + print_passing_mgs(row) + pass_type['simple_fail'] = round(long_tc['50%'] - th_bad_vlv, 2) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=join(project_folder, bad_folder)) + + return pass_type + +def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): """ Analyze each valve and detect for passing valves @@ -956,19 +1115,19 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv valve operating point is malfunctioning e.g. allow enough time for residue heat to dissipate from the coil. - good_folder: name of path showing the folder to save the plots of the correct operating valves - - bad_folder: name of path showing the folder to save the plots of the malfunction valves + project_folder: name of path for the project and used to save the plots and csv data. Returns ------- None """ + # container for holding types of faults + passing_type = dict() # check for empty dataframe if vlv_df.empty: print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) - return + return passing_type if 'air_flow' in vlv_df.columns: # plot temp diff vs air flow @@ -980,10 +1139,10 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv if vlv_df.empty: print("'{}' in site {} has no data after hours of \ occupancy check! Skipping...".format(row['vlv'], row['site'])) - return + return passing_type - # container for holding types of faults - bad_klass = [] + # Analyze timestamps and valve operation changes + vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row) # determine if valve datastream has open and closed data bool_type = vlv_df['vlv_open'].value_counts().index @@ -991,38 +1150,40 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv if len(bool_type) < 2: if bool_type[0]: # only open valve data - long_to = calc_long_t_diff(vlv_df, vlv_open=True) - if long_to['50%'] < th_bad_vlv: - print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=join(project_folder, bad_folder)) + passing_type = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) else: # only closed valve data - long_tc = calc_long_t_diff(vlv_df, row=row) - if long_tc['50%'] > th_bad_vlv: - print("Probable passing valve '{}' in site {}".format(row['vlv'], row['site'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=join(project_folder, bad_folder)) - return + passing_type = analyze_only_close(vlv_df, row, th_bad_vlv, th_time, window, project_folder) + + return passing_type # TODO: Figure out what to do if long_tc is None! # calculate long-term temp diff when valve is closed - long_tc = calc_long_t_diff(vlv_df, row=row) - long_to = calc_long_t_diff(vlv_df, vlv_open=True) + long_tc = calc_long_t_diff(vlv_df) + long_to = vlv_df[vlv_df['vlv_open']]['temp_diff'].describe() + + if long_tc is None and long_to is not None: + pass_type = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) + passing_type.update(pass_type) + return passing_type + # make simple comparison of long-term closed temp difference and user define threshold + if long_tc['50%'] > th_bad_vlv: + print_passing_mgs(row) + passing_type['simple_fail'] = round(long_tc['50%'] - th_bad_vlv, 2) - # make a simple comparison of between long-term open and long-term closed temp diff - if (long_tc['mean'] + long_tc['std']) > long_to['mean']: - print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) - bad_klass.append(True) + # make comparison between long-term open and long-term closed temp difference + long_tc_to_diff = (long_tc['mean'] + long_tc['std']) - (long_to['75%']) + if long_tc_to_diff > 0: + print_passing_mgs(row) + passing_type['tc_to_close_fail'] = round(long_tc_to_diff, 2) # assume a 0 deg difference at 0% open valve no_zeros_po = vlv_df.copy() - no_zeros_po.loc[no_zeros_po['vlv_po'] == 0, 'temp_diff'] = 0 + no_zeros_po.loc[~no_zeros_po['vlv_open'], 'temp_diff'] = 0 # make a logit regression model assuming that closed valves make a zero temp difference - try: - df_fit_nz = build_logistic_model(no_zeros_po) - except RuntimeError: - df_fit_nz = None + df_fit_nz, popt = build_logistic_model(no_zeros_po) # determine estimated long-term difference if df_fit_nz is not None: @@ -1031,8 +1192,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv est_lt_diff_nz = long_tc['25%'] # calculate bad valve instances vs overall dataframe - th_ratio = 20 - bad_vlv = find_bad_vlv_operation(vlv_df, est_lt_diff_nz, th_time, window) + bad_vlv, pass_type = find_bad_vlv_operation(vlv_df, est_lt_diff_nz, window) + + passing_type.update(pass_type) if bad_vlv is None: bad_ratio = 0 @@ -1041,28 +1203,25 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) long_tbad = bad_vlv['temp_diff'].describe()['mean'] + # estimate size of leak in terms of pct that valve is open if df_fit_nz is not None: est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() + if est_leak > popt[1] and bad_ratio > 5: + passing_type['leak_grtr_xovr_fail'] = est_leak + print_passing_mgs(row) else: est_leak = bad_ratio + import pdb; pdb.set_trace() - if est_leak > th_ratio: - bad_klass.append(True) - - if len(bad_klass) > 0: + failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail'] for x in passing_type.keys()] + if any(failure): folder = join(project_folder, bad_folder) - if bad_ratio > 5: - print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) - if len(bad_klass) > 1: - print("{} percentage of time is leaking!".format(bad_ratio)) - else: - folder = join(project_folder, good_folder) else: folder = join(project_folder, good_folder) if bad_vlv is not None: # colorize good and bad points - vlv_df['color'] = '#5ab300' + vlv_df.loc[:, 'color'] = '#5ab300' vlv_df.loc[bad_vlv.index, 'color'] = '#b3005a' _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) @@ -1072,7 +1231,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, good_folder='./good_valv # grps = list(lal.groups.keys()) # bad_vlv.loc[lal.groups[grps[0]]] -def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder, project_folder): + return passing_type + +def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, project_folder): """ Helper function to analyze AHU valves @@ -1088,23 +1249,21 @@ def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder, proj valve operating point is malfunctioning e.g. allow enough time for residue heat to dissipate from the coil. - good_folder: name of path showing the folder to save the plots of the correct operating valves - - bad_folder: name of path showing the folder to save the plots of the malfunction valves - Returns ------- None """ - if row['upstream_type'] != 'Mixed_Air_Temperature_Sensor': print('No upstream sensor data available for coil in AHU {} for site {}'.format(row['equip'], row['site'])) #_make_tdiff_vs_vlvpo_plot(vlv_df, row, folder='./') + passing_type = dict() else: - _analyze_vlv(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv, th_time, project_folder) + + return passing_type -def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time, good_folder, bad_folder, project_folder): +def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time, project_folder): """ Hi level analyze function that runs through each valve queried to detect passing valves @@ -1124,30 +1283,44 @@ def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time valve operating point is malfunctioning e.g. allow enough time for residue heat to dissipate from the coil. - good_folder: name of path showing the folder to save the plots of the correct operating valves - - bad_folder: name of path showing the folder to save the plots of the malfunction valves + project_folder: name of path for the project and used to save the plots and csv data. Returns ------- None """ + results = [] # analyze valves for idx, row in metadata.iterrows(): + vlv_dat = dict(row) try: # clean data vlv_df = clean_func(fetch_resp, row) # analyze for passing valves - analyze_func(vlv_df, row, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) + passing_type = analyze_func(vlv_df, row, th_bad_vlv, th_time, project_folder) except: - print("Error try to debug") - print(sys.exc_info()[0]) + import traceback + exc_type, exc_value, exc_traceback = sys.exc_info() + print("******Error try to debug") + print("{}: {}\n".format(exc_type, exc_value)) + print(''.join(traceback.format_tb(exc_traceback))) + passing_type = dict() import pdb; pdb.set_trace() continue -def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder, project_folder): + if passing_type is None: + import pdb; pdb.set_trace() + + vlv_dat.update(passing_type) + results.append(vlv_dat) + + final_df = pd.DataFrame.from_records(results) + + return final_df + +def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, project_folder): """ Main function that runs all the steps of the application @@ -1167,46 +1340,79 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th th_time: length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning e.g. allow enough time for residue heat to - dissipate from the coil. - - good_folder: name of path showing the folder to save the plots of the correct operating valves - - bad_folder: name of path showing the folder to save the plots of the malfunction valves - + dissipate from the coil. If two values are defined, the 1st is for reheat coils and the 2nd for ahu coils. + project_folder: name of path for the project and used to save the plots and csv data. Returns ------- None """ + # declare user hidden parameters + global long_term_fail # number of minutes to trigger an long-term passing valve failure + global shrt_term_fail # number of minutes to trigger an intermitten passing valve failure + global good_folder + global bad_folder + global air_flow_folder + global csv_folder + + # define user hidden parameters + long_term_fail = 5*60 # number of minutes to trigger an long-term passing valve failure + shrt_term_fail = 60 # number of minutes to trigger an intermitten passing valve failure + th_vlv_fail = 20 # equivalent percentage of valve open for determining failure. + + # define container folders + good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves + bad_folder = 'bad_valves' # name of path to the folder to save the plots of the malfunction valves + air_flow_folder = 'air_flow_plots' # name of path to the folder to save plots of the air flow values + csv_folder = 'csv_data' # name of path to the folder to save detailed valve data + # check if holding folders exist check_folder_exist(join(project_folder, bad_folder)) check_folder_exist(join(project_folder, good_folder)) - check_folder_exist(join(project_folder, "air_flow_plots")) - check_folder_exist(join(project_folder, "csv_data")) + check_folder_exist(join(project_folder, air_flow_folder)) + check_folder_exist(join(project_folder, csv_folder)) + + # split length of time for vav and ahus + if isinstance(th_time, (list, tuple)): + if len(th_time) == 2: + th_time_vav = th_time[0] + th_time_ahu = th_time[1] + else: + th_time_vav = th_time[0] + th_time_ahu = th_time[0] + else: + th_time_vav = th_time + th_time_ahu = th_time query = _query_and_qualify() fetch_resp = _fetch(query, eval_start_time, eval_end_time, window) # analyze VAV valves vav_metadata = fetch_resp['vav'].view('dnstream_ta') - _analyze(vav_metadata, fetch_resp['vav'], _clean_vav, _analyze_vlv, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) + results_vav = _analyze(vav_metadata, fetch_resp['vav'], _clean_vav, _analyze_vlv, th_bad_vlv, th_time_vav, project_folder) # analyze AHU valves ahu_metadata = reformat_ahu_view(fetch_resp['ahu']) - _analyze(ahu_metadata, fetch_resp['ahu'], _clean_ahu, _analyze_ahu, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) + results_ahu = _analyze(ahu_metadata, fetch_resp['ahu'], _clean_ahu, _analyze_ahu, th_bad_vlv, th_time_ahu, project_folder) + + # save results + final_df = pd.concat([results_vav, results_ahu]) + final_df = final_df.sort_values(by=['long_term_fail'], ascending=False) + final_df.to_csv(join(project_folder, "passing_valve_results" + ".csv"), ) if __name__ == '__main__': + # Disable options + pd.options.mode.chained_assignment = None + # define parameters eval_start_time = "2018-01-01T00:00:00Z" eval_end_time = "2018-06-30T00:00:00Z" window = 15 - th_bad_vlv = 5 - th_time = 45 + th_bad_vlv = 10 + th_time = [12, 45] project_folder = './with_airflow_checks' - good_folder = 'good_valves' - bad_folder = 'bad_valves' # Run the app - detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, good_folder, bad_folder, project_folder) \ No newline at end of file + detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, project_folder) \ No newline at end of file From 121cf424b392cb30365de64bdf38c7f067622629 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 30 Dec 2020 19:07:38 -0800 Subject: [PATCH 31/83] changed assessment to detect pass vlv by using valve open data --- detect_passing_valves/app.py | 38 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index a713e86..934fbe7 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -677,7 +677,7 @@ def calc_long_t_diff(vlv_df): if vlv_df is None: return None - df_vlv_close = vlv_df.loc[np.logical_and(df_vlv_close['cons_ts_vlv_c'], df_vlv_close['steady'])] + df_vlv_close = vlv_df.loc[np.logical_and(vlv_df['cons_ts_vlv_c'], vlv_df['steady'])] if df_vlv_close is None: return None @@ -954,7 +954,7 @@ def density_data(dat, rescale_dat=None): return xs, ys -def find_bad_vlv_operation(vlv_df, long_t, window): +def find_bad_vlv_operation(vlv_df, model, window): """ Determine which timeseries values are data from probable passing valves and return a pandas dataframe of only 'bad' values. @@ -981,14 +981,20 @@ def find_bad_vlv_operation(vlv_df, long_t, window): """ pass_type = dict() + long_to = vlv_df[vlv_df['vlv_open']]['temp_diff'].describe() + hi_diff = long_to['75%'] - # find datapoints that exceed long-term temperature difference - exceed_long_t = vlv_df['temp_diff'] >= long_t + if model is not None: + vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() - # TODO: Compare passing valve time to time it is actually open - # # determine time interval that valve is open - # open_grp = df_vlv_close.loc[np.logical_and(np.logical_and(vlv_df['vlv_open'], vlv_df['cons_ts']), exceed_long_t)].groupby('same') - # op_count_stats = open_grp['same'].count().describe() + # define temperature difference and valve position failure thresholds + vlv_po_th = vlv_po_hi_diff/2.0 + diff_vlv_po_th = model[model['vlv_po'] <= vlv_po_th]['y_fitted'].max() + else: + diff_vlv_po_th = hi_diff/4.0 + + # find datapoints that exceed long-term temperature difference + exceed_long_t = vlv_df['temp_diff'] >= diff_vlv_po_th # subset data by consecutive steady state values when valve is commanded closed and # exceeds long-term temperature difference @@ -1019,7 +1025,8 @@ def find_bad_vlv_operation(vlv_df, long_t, window): # detect short term failures if any((bad_grp_count*window) > shrt_term_fail): shrt_term_fail_times = bad_grp_count[(bad_grp_count*window) > shrt_term_fail]*window - pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count()) + if shrt_term_fail_times.count() > 2: + pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count()) return df_bad, pass_type @@ -1153,7 +1160,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): passing_type = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) else: # only closed valve data - passing_type = analyze_only_close(vlv_df, row, th_bad_vlv, th_time, window, project_folder) + passing_type = analyze_only_close(vlv_df, row, th_bad_vlv, project_folder) return passing_type @@ -1185,15 +1192,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): # make a logit regression model assuming that closed valves make a zero temp difference df_fit_nz, popt = build_logistic_model(no_zeros_po) - # determine estimated long-term difference - if df_fit_nz is not None: - est_lt_diff_nz = df_fit_nz[df_fit_nz['vlv_po'] == 0]['y_fitted'].mean() - else: - est_lt_diff_nz = long_tc['25%'] - # calculate bad valve instances vs overall dataframe - bad_vlv, pass_type = find_bad_vlv_operation(vlv_df, est_lt_diff_nz, window) - + bad_vlv, pass_type = find_bad_vlv_operation(vlv_df, df_fit_nz, window) passing_type.update(pass_type) if bad_vlv is None: @@ -1208,13 +1208,13 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() if est_leak > popt[1] and bad_ratio > 5: passing_type['leak_grtr_xovr_fail'] = est_leak - print_passing_mgs(row) else: est_leak = bad_ratio import pdb; pdb.set_trace() failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail'] for x in passing_type.keys()] if any(failure): + print_passing_mgs(row) folder = join(project_folder, bad_folder) else: folder = join(project_folder, good_folder) From 4dca0aef902f3753fef3b5fc159053745026e6fb Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 4 Jan 2021 12:48:24 -0800 Subject: [PATCH 32/83] added function to check duplicate plot files --- detect_passing_valves/app.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 934fbe7..5f19ef5 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -867,10 +867,32 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N ax.text(.2, 0.95*y_max, "Bad ratio={:.1f}%".format(bad_ratio)) plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) - plt.savefig(join(folder, plt_name + '.png')) + full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) + plt.savefig(full_path) plt.close() +def rename_existing(path, idx, row): + """ + Check if the file path exists, if it does, then rename. + + Parameters + ---------- + path: name of path to check + + idx: index of duplicate file + + row: Pandas series object with metadata for the specific valve + """ + if os.path.exists(path): + print('REPEATED EQUIP for {}-{}-{}'.format(row['site'], row['equip'], row['vlv'])) + idx+=1 + head, tail = os.path.split(path) + tail = "R" + str(idx) + "-" + tail + path = rename_existing(join(head, tail), idx, row) + + return path + def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): """ Create temperature difference versus air flow plots @@ -911,7 +933,8 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35) plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) - plt.savefig(join(folder, plt_name + '.png')) + full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) + plt.savefig(full_path) plt.close() From 69a888ffbee8f1c30f2b1e48469dca4a3376a23b Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 4 Jan 2021 12:58:54 -0800 Subject: [PATCH 33/83] added conditions for when airflow density is unimodal --- detect_passing_valves/app.py | 64 +++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 5f19ef5..1a6f586 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -388,7 +388,12 @@ def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5): xs, ys = density_data(df['air_flow'], rescale_dat=df['temp_diff']) min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) - df = df.loc[df['air_flow'] > xs[min_idx[0]]] + if min_idx is not None: + min_air_flow = xs[min_idx[0]] + else: + min_air_flow = np.percentile(xs, 5) + + df = df.loc[df['air_flow'] > min_air_flow] else: # drop values outside occupancy hours df = occupied_hours_subset(df, occ_str, occ_end, wkend_str) @@ -781,34 +786,37 @@ def return_extreme_points(dat, type_of_extreme=None, n_modes=None, sort=True): idx = np.delete(idx, len(dat)-1) idx_num = len(idx) - type_of = [] - if dat[idx[0]] > dat[idx[1]]: - # if true then starting inflection point is a maximum - type_of = np.array(['max']*idx_num) - type_of[1:][::2] = 'min' - elif dat[idx[0]] < dat[idx[1]]: - # if true then starting inflection point is a minimum - type_of = np.array(['min']*idx_num) - type_of[1:][::2] = 'max' - - # return requested inflection points - if type_of_extreme == 'max': - idx = idx[type_of == 'max'] - elif type_of_extreme == 'min': - idx = idx[type_of == 'min'] + if idx_num < 2: + return None else: - print('Returning all inflection points') - - if sort or n_modes is not None: - idx = idx[np.argsort(dat[idx])] - - if n_modes is not None: + type_of = [] + if dat[idx[0]] > dat[idx[1]]: + # if true then starting inflection point is a maximum + type_of = np.array(['max']*idx_num) + type_of[1:][::2] = 'min' + elif dat[idx[0]] < dat[idx[1]]: + # if true then starting inflection point is a minimum + type_of = np.array(['min']*idx_num) + type_of[1:][::2] = 'max' + + # return requested inflection points if type_of_extreme == 'max': - idx = idx[(-1*n_modes):] + idx = idx[type_of == 'max'] elif type_of_extreme == 'min': - idx = idx[:n_modes] + idx = idx[type_of == 'min'] + else: + print('Returning all inflection points') + + if sort or n_modes is not None: + idx = idx[np.argsort(dat[idx])] + + if n_modes is not None: + if type_of_extreme == 'max': + idx = idx[(-1*n_modes):] + elif type_of_extreme == 'min': + idx = idx[:n_modes] - return idx + return idx def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): @@ -929,8 +937,10 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): max_idx = return_extreme_points(ys, type_of_extreme='max', n_modes=2) min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) - ax.scatter(x=xs[max_idx], y=ys[max_idx], color = '#ff0000', alpha=1, s=35) - ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35) + if max_idx is not None: + ax.scatter(x=xs[max_idx], y=ys[max_idx], color = '#ff0000', alpha=1, s=35) + if max_idx is not None: + ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35) plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) From eb2d45151cebb1d9b954b3ea18940d367b88c348 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 4 Jan 2021 13:00:15 -0800 Subject: [PATCH 34/83] changed the conditions for detecting passing valve --- detect_passing_valves/app.py | 53 ++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 1a6f586..22f796b 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1014,17 +1014,16 @@ def find_bad_vlv_operation(vlv_df, model, window): """ pass_type = dict() - long_to = vlv_df[vlv_df['vlv_open']]['temp_diff'].describe() - hi_diff = long_to['75%'] + hi_diff = np.percentile(vlv_df.loc[vlv_df['vlv_open'], 'temp_diff'], 95) if model is not None: vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() # define temperature difference and valve position failure thresholds vlv_po_th = vlv_po_hi_diff/2.0 - diff_vlv_po_th = model[model['vlv_po'] <= vlv_po_th]['y_fitted'].max() + diff_vlv_po_th = max(model[model['vlv_po'] <= vlv_po_th]['y_fitted'].max(), 5) else: - diff_vlv_po_th = hi_diff/4.0 + diff_vlv_po_th = max(hi_diff/4.0, 5) # find datapoints that exceed long-term temperature difference exceed_long_t = vlv_df['temp_diff'] >= diff_vlv_po_th @@ -1041,19 +1040,25 @@ def find_bad_vlv_operation(vlv_df, model, window): bad_grp = df_bad.groupby('same') bad_grp_count = bad_grp['same'].count() - max_idx = np.argmax(bad_grp_count) - max_grp = bad_grp.groups[bad_grp_count.index[max_idx]] + # max_idx = np.argmax(bad_grp_count) + # max_grp = bad_grp.groups[bad_grp_count.index[max_idx]] - if len(max_grp) > 1: - max_passing_time = max_grp[-1] - max_grp[0] - else: - max_passing_time = pd.Timedelta(0, unit='min') + # if len(max_grp) > 1: + # max_passing_time = max_grp[-1] - max_grp[0] + # else: + # max_passing_time = pd.Timedelta(0, unit='min') + + # # detect long term failures + # if max_passing_time > pd.Timedelta(long_term_fail, unit='min'): + # ts_seconds = max_passing_time.seconds + # ts_days = max_passing_time.days * 3600 * 24 + # pass_type['long_term_fail'] = (ts_days+ts_seconds)/60.0 # detect long term failures - if max_passing_time > pd.Timedelta(long_term_fail, unit='min'): - ts_seconds = max_passing_time.seconds - ts_days = max_passing_time.days * 3600 * 24 - pass_type['long_term_fail'] = (ts_days+ts_seconds)/60.0 + if any((bad_grp_count*window) > long_term_fail): + long_term_fail_times = bad_grp_count[(bad_grp_count*window) > long_term_fail]*window + if long_term_fail_times.count() > 2 or long_term_fail_times.index[-1] == vlv_df['same'].max(): + pass_type['long_term_fail'] = (long_term_fail_times.mean(), long_term_fail_times.count()) # detect short term failures if any((bad_grp_count*window) > shrt_term_fail): @@ -1184,6 +1189,11 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): # Analyze timestamps and valve operation changes vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row) + if vlv_df.empty: + print("'{}' in site {} has no data after analyzing \ + consecutive timestamps! Skipping...".format(row['vlv'], row['site'])) + return passing_type + # determine if valve datastream has open and closed data bool_type = vlv_df['vlv_open'].value_counts().index @@ -1242,10 +1252,12 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): if est_leak > popt[1] and bad_ratio > 5: passing_type['leak_grtr_xovr_fail'] = est_leak else: - est_leak = bad_ratio - import pdb; pdb.set_trace() + if bad_vlv is not None: + est_leak = bad_vlv['temp_diff'].mean() + if bad_ratio > 5 and est_leak > th_bad_vlv: + passing_type['leak_grtr_threshold_fail'] = est_leak - failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail'] for x in passing_type.keys()] + failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail', 'leak_grtr_threshold_fail'] for x in passing_type.keys()] if any(failure): print_passing_mgs(row) folder = join(project_folder, bad_folder) @@ -1429,10 +1441,11 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th ahu_metadata = reformat_ahu_view(fetch_resp['ahu']) results_ahu = _analyze(ahu_metadata, fetch_resp['ahu'], _clean_ahu, _analyze_ahu, th_bad_vlv, th_time_ahu, project_folder) - # save results + # clean report and save results final_df = pd.concat([results_vav, results_ahu]) - final_df = final_df.sort_values(by=['long_term_fail'], ascending=False) - final_df.to_csv(join(project_folder, "passing_valve_results" + ".csv"), ) + final_df = final_df.loc[np.logical_or(~final_df['long_term_fail'].isnull(), ~final_df['short_term_fail'].isnull())] + final_df = final_df.sort_values(by=['long_term_fail', 'short_term_fail'], ascending=False) + final_df.to_csv(join(project_folder, "passing_valve_results" + ".csv")) if __name__ == '__main__': From 038c3d0a67f736e342f680df7d919bb789c829c0 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 4 Jan 2021 14:30:33 -0800 Subject: [PATCH 35/83] fixed bugs when df is none and missing arg --- detect_passing_valves/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 22f796b..c89350b 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -746,7 +746,9 @@ def analyze_timestamps(vlv_df, th_time, window, row=None): # save csv data if row is defined if row is not None: _name = "{}-{}-{}_dat".format(row['site'], row['equip'], row['vlv']) - vlv_df.to_csv(join(project_folder, "csv_data", _name + '.csv')) + + full_path = rename_existing(join(project_folder, "csv_data", _name + '.csv'), idx=0, row=row) + vlv_df.to_csv(full_path) return vlv_df @@ -1189,7 +1191,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): # Analyze timestamps and valve operation changes vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row) - if vlv_df.empty: + if vlv_df is None: print("'{}' in site {} has no data after analyzing \ consecutive timestamps! Skipping...".format(row['vlv'], row['site'])) return passing_type From e4693ec402736f85da101d5e27f5183d47f6c0d2 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 6 Jan 2021 10:43:18 -0800 Subject: [PATCH 36/83] added function to clean up final report of passing valves --- detect_passing_valves/app.py | 58 ++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index c89350b..0c0e696 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1060,13 +1060,15 @@ def find_bad_vlv_operation(vlv_df, model, window): if any((bad_grp_count*window) > long_term_fail): long_term_fail_times = bad_grp_count[(bad_grp_count*window) > long_term_fail]*window if long_term_fail_times.count() > 2 or long_term_fail_times.index[-1] == vlv_df['same'].max(): - pass_type['long_term_fail'] = (long_term_fail_times.mean(), long_term_fail_times.count()) + dates = [(bad_grp.groups[ky][0], bad_grp.groups[ky][-1]) for ky in long_term_fail_times.index] + pass_type['long_term_fail'] = (long_term_fail_times.mean(), long_term_fail_times.count(), dates) # detect short term failures if any((bad_grp_count*window) > shrt_term_fail): shrt_term_fail_times = bad_grp_count[(bad_grp_count*window) > shrt_term_fail]*window if shrt_term_fail_times.count() > 2: - pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count()) + dates = [(bad_grp.groups[ky][0], bad_grp.groups[ky][-1]) for ky in shrt_term_fail_times.index] + pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count(), dates) return df_bad, pass_type @@ -1086,6 +1088,39 @@ def print_passing_mgs(row): print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) +def clean_final_report(final_df): + """ + Clean final report and sort by greatest number of minutes that fault was detected + + Parameters + ---------- + final_df: pandas dataframe with valve metadata along with failure types detected + + Returns + ------- + final_df: cleaned and sorted report + """ + if 'long_term_fail' in final_df.columns: + final_df = final_df.loc[np.logical_or(~final_df['long_term_fail'].isnull(), ~final_df['short_term_fail'].isnull())] + + # separate data into multiple columns + final_df['long_term_fail_avg_minutes'] = final_df.long_term_fail.str[0] + final_df['long_term_fail_num_times_detected'] = final_df.long_term_fail.str[1] + final_df['long_term_fail_str_end_dates'] = final_df.long_term_fail.str[2] + + final_df['short_term_fail_avg_minutes'] = final_df.short_term_fail.str[0] + final_df['short_term_fail_num_times_detected'] = final_df.short_term_fail.str[1] + final_df['short_term_fail_str_end_dates'] = final_df.short_term_fail.str[2] + + # sort by highest value faults + final_df = final_df.sort_values(by=['long_term_fail_avg_minutes', 'short_term_fail_avg_minutes'], ascending=False) + + # drop redundant columns + final_df = final_df.drop(columns=['long_term_fail', 'short_term_fail']) + + return final_df + + def analyze_only_open(vlv_df, row, th_bad_vlv, project_folder): """ Analyze valve data when there is only open valve data. @@ -1180,14 +1215,6 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): # plot temp diff vs air flow _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots')) - # drop data that occurs during unoccupied hours - vlv_df = drop_unoccupied_dat(vlv_df, occ_str=6, occ_end=18, wkend_str=5) - - if vlv_df.empty: - print("'{}' in site {} has no data after hours of \ - occupancy check! Skipping...".format(row['vlv'], row['site'])) - return passing_type - # Analyze timestamps and valve operation changes vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row) @@ -1196,6 +1223,14 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): consecutive timestamps! Skipping...".format(row['vlv'], row['site'])) return passing_type + # drop data that occurs during unoccupied hours + vlv_df = drop_unoccupied_dat(vlv_df, occ_str=6, occ_end=18, wkend_str=5) + + if vlv_df.empty: + print("'{}' in site {} has no data after hours of \ + occupancy check! Skipping...".format(row['vlv'], row['site'])) + return passing_type + # determine if valve datastream has open and closed data bool_type = vlv_df['vlv_open'].value_counts().index @@ -1445,8 +1480,7 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th # clean report and save results final_df = pd.concat([results_vav, results_ahu]) - final_df = final_df.loc[np.logical_or(~final_df['long_term_fail'].isnull(), ~final_df['short_term_fail'].isnull())] - final_df = final_df.sort_values(by=['long_term_fail', 'short_term_fail'], ascending=False) + final_df = clean_final_report(final_df) final_df.to_csv(join(project_folder, "passing_valve_results" + ".csv")) From 8f691fecf1358641234459f309f6a154dfa07ada Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 7 Jan 2021 09:24:28 -0800 Subject: [PATCH 37/83] added function to make timeseries plots of valve data that was detected as passing valve --- detect_passing_valves/plot_data.py | 140 +++++++++++++++++++++++++ detect_passing_valves/requirements.txt | 1 + 2 files changed, 141 insertions(+) create mode 100644 detect_passing_valves/plot_data.py diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py new file mode 100644 index 0000000..76d7902 --- /dev/null +++ b/detect_passing_valves/plot_data.py @@ -0,0 +1,140 @@ +import pandas as pd +import numpy as np +import re +import os + +from os.path import join + +from bokeh.io import show, save, output_file +from bokeh.layouts import column +from bokeh.models import ColumnDataSource, RangeTool, LinearAxis, Range1d, BoxAnnotation +from bokeh.plotting import figure + + +def parse_list_timestamps(str_ts_list, tz='UTC'): + """ + Convert a list of pandas timestamps represented as a string to a + readable format i.e. pandas timestamp object + """ + # convert string dates to readable time stamps + fault_dates = str_ts_list.strip('][') + fault_dates = re.split(r'(?<=\)\)), ', fault_dates) + f_dates = [re.split(r'(?<=\)), ', ts[1:-1]) for ts in fault_dates] + + f_dts = [pd.to_datetime(ts, format="Timestamp('%Y-%m-%d %H:%M:%S%z', tz='{}')".format(tz)) for ts in f_dates] + + return f_dts + + +def plot_fault_valve_data(df_row, fig_folder): + """ + Plot timeseries data for valves that were detected as passing valves + """ + + # plot detected passing valve data + fault_dates = df_row['long_term_fail_str_end_dates'] + fault_dates = parse_list_timestamps(fault_dates) + + vlv_name = "{}-{}-{}".format(df_row['site'], df_row['equip'], df_row['vlv']) + + csv_names = [f for f in all_csv_files if vlv_name in f] + + for csv in csv_names: + vlv_dat = pd.read_csv(join(vlv_project_folder, csv), index_col=0, parse_dates=True) + + # define plot data and parameters + y_overlimit = 0.05 + left_y_max = np.ceil(max(vlv_dat['upstream_ta'].max(), vlv_dat['dnstream_ta'].max())) + left_y_min = np.floor(min(vlv_dat['upstream_ta'].min(), vlv_dat['dnstream_ta'].min())) + + right_sec_y_max = np.ceil(vlv_dat['temp_diff'].max()) + right_sec_y_min = np.floor(vlv_dat['temp_diff'].min()) + + sub_cols = ['upstream_ta', 'dnstream_ta', 'vlv_po', 'temp_diff'] + if 'air_flow' in vlv_dat.columns: + right_y_max = np.ceil(vlv_dat['air_flow'].max()) + right_y_min = np.floor(vlv_dat['air_flow'].min()) + sub_cols.append('air_flow') + + src = ColumnDataSource(vlv_dat.loc[:, sub_cols]) + + # make the plot + p = figure(plot_height=300, plot_width=800, tools='xpan', toolbar_location=None, + x_axis_type='datetime', x_axis_location='above', + x_range=(vlv_dat.index[0], vlv_dat.index[480]), + y_range = Range1d(start=left_y_min*(1-y_overlimit), end=left_y_max*(1+y_overlimit)), + background_fill_color='#ffffff' + ) + p.yaxis.axis_label = 'Temperature [F]' + + # highlight problem areas + fault_hilight = [] + for ts in fault_dates: + box_ann = BoxAnnotation(left=ts[0], right=ts[1], fill_color='#db8370', fill_alpha=0.15) + fault_hilight.append(box_ann) + p.add_layout(box_ann) + + # line plots + p.step('index', 'upstream_ta', source=src, color='#7093db', line_width=2) + p.step('index', 'dnstream_ta', source=src, color='#db7093', line_width=2) + + if 'air_flow' in vlv_dat.columns: + xtr_y_axis = {"airFlow": Range1d(start=right_y_min*(1-y_overlimit), end=right_y_max*(1+y_overlimit))} + p.extra_y_ranges = xtr_y_axis + p.add_layout(LinearAxis(y_range_name='airFlow', axis_label='Air flow [cfm]'), 'right') + p.step('index', 'air_flow', source=src, color='#93db70', y_range_name='airFlow', line_width=2) + + # range selector tool + range_tool = RangeTool(x_range=p.x_range) + range_tool.overlay.fill_color = "navy" + range_tool.overlay.fill_alpha = 0.2 + + select = figure(title="Orange highlight area represent passing valve operation", + plot_height=100, plot_width=800, + y_range=Range1d(start=-1, end=101), + x_axis_type="datetime", + tools="", toolbar_location=None, background_fill_color="#ffffff") + select.yaxis.axis_label = 'Valve' + + select.step('index', 'vlv_po', source=src, color='#70dbb8') + select.ygrid.grid_line_color = None + select.add_tools(range_tool) + select.toolbar.active_multi = range_tool + + select.extra_y_ranges = {"tempDiff": Range1d(start=right_sec_y_min*(1-y_overlimit), end=right_sec_y_max*(1+y_overlimit))} + select.add_layout(LinearAxis(y_range_name='tempDiff', axis_label='TDiff'), 'right') + select.step('index', 'temp_diff', source=src, color='#b870db', y_range_name='tempDiff') + + for box_ann in fault_hilight: + select.add_layout(box_ann) + + # save plot + plot_name = '{}-timeseries.html'.format(csv.split('.csv')[0]) + output_file(join(fig_folder, plot_name)) + save(column(p, select)) + + +if __name__ == '__main__': + + # define data sources + project_folder = join("./", "with_airflow_checks_year_end") + vlv_project_folder = join(project_folder, "csv_data") + all_csv_files = os.listdir(vlv_project_folder) + + fig_folder = join(project_folder, 'timeseries_valve_faults') + + if not os.path.exists(fig_folder): + os.makedirs(fig_folder) + + # read csv file with detected passing valves + fault_dat = pd.read_csv(join(project_folder, "passing_valve_results.csv"), index_col=False) + + # plot data from valves that were detect with passing valve operation + for idx, df_row in fault_dat.iterrows(): + if pd.notnull(df_row['long_term_fail_str_end_dates']): + plot_fault_valve_data(df_row, fig_folder) + + print('-------Finished processing plots-----') + + + diff --git a/detect_passing_valves/requirements.txt b/detect_passing_valves/requirements.txt index e4fa98d..7ea8f8c 100644 --- a/detect_passing_valves/requirements.txt +++ b/detect_passing_valves/requirements.txt @@ -3,3 +3,4 @@ pandas>=1.1 numpy>=1.19.0 matplotlib>=3.0.3 scipy>=1.5.2 +bokeh>=2.2.3 From 7f2e3d1650015a309bc5bab877511865d703d187 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 7 Jan 2021 13:44:31 -0800 Subject: [PATCH 38/83] refactored plot function to make plots for good operation valve data --- detect_passing_valves/plot_data.py | 223 ++++++++++++++++++----------- 1 file changed, 137 insertions(+), 86 deletions(-) diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index 76d7902..0e90718 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -2,6 +2,7 @@ import numpy as np import re import os +import random from os.path import join @@ -26,115 +27,165 @@ def parse_list_timestamps(str_ts_list, tz='UTC'): return f_dts -def plot_fault_valve_data(df_row, fig_folder): +def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): """ - Plot timeseries data for valves that were detected as passing valves + Plot valve data from mortar """ - # plot detected passing valve data - fault_dates = df_row['long_term_fail_str_end_dates'] - fault_dates = parse_list_timestamps(fault_dates) - - vlv_name = "{}-{}-{}".format(df_row['site'], df_row['equip'], df_row['vlv']) - - csv_names = [f for f in all_csv_files if vlv_name in f] - - for csv in csv_names: - vlv_dat = pd.read_csv(join(vlv_project_folder, csv), index_col=0, parse_dates=True) - - # define plot data and parameters - y_overlimit = 0.05 - left_y_max = np.ceil(max(vlv_dat['upstream_ta'].max(), vlv_dat['dnstream_ta'].max())) - left_y_min = np.floor(min(vlv_dat['upstream_ta'].min(), vlv_dat['dnstream_ta'].min())) - - right_sec_y_max = np.ceil(vlv_dat['temp_diff'].max()) - right_sec_y_min = np.floor(vlv_dat['temp_diff'].min()) - - sub_cols = ['upstream_ta', 'dnstream_ta', 'vlv_po', 'temp_diff'] - if 'air_flow' in vlv_dat.columns: - right_y_max = np.ceil(vlv_dat['air_flow'].max()) - right_y_min = np.floor(vlv_dat['air_flow'].min()) - sub_cols.append('air_flow') - - src = ColumnDataSource(vlv_dat.loc[:, sub_cols]) - - # make the plot - p = figure(plot_height=300, plot_width=800, tools='xpan', toolbar_location=None, - x_axis_type='datetime', x_axis_location='above', - x_range=(vlv_dat.index[0], vlv_dat.index[480]), - y_range = Range1d(start=left_y_min*(1-y_overlimit), end=left_y_max*(1+y_overlimit)), - background_fill_color='#ffffff' - ) - p.yaxis.axis_label = 'Temperature [F]' - - # highlight problem areas + # read csv + vlv_dat = pd.read_csv(csv_path, index_col=0, parse_dates=True) + + # define plot data and parameters + y_overlimit = 0.05 + left_y_max = np.ceil(max(vlv_dat['upstream_ta'].max(), vlv_dat['dnstream_ta'].max())) + left_y_min = np.floor(min(vlv_dat['upstream_ta'].min(), vlv_dat['dnstream_ta'].min())) + + right_sec_y_max = np.ceil(vlv_dat['temp_diff'].max()) + right_sec_y_min = np.floor(vlv_dat['temp_diff'].min()) + + sub_cols = ['upstream_ta', 'dnstream_ta', 'vlv_po', 'temp_diff'] + if 'air_flow' in vlv_dat.columns: + right_y_max = np.ceil(vlv_dat['air_flow'].max()) + right_y_min = np.floor(vlv_dat['air_flow'].min()) + sub_cols.append('air_flow') + + src = ColumnDataSource(vlv_dat.loc[:, sub_cols]) + + # make the plot + max_date_idx = min(480, len(vlv_dat.index)) + p = figure(plot_height=300, plot_width=800, tools='xpan', toolbar_location=None, + x_axis_type='datetime', x_axis_location='above', + x_range=(vlv_dat.index[0], vlv_dat.index[max_date_idx]), + y_range = Range1d(start=left_y_min*(1-y_overlimit), end=left_y_max*(1+y_overlimit)), + background_fill_color='#ffffff' + ) + p.yaxis.axis_label = 'Temperature [F]' + + # highlight problem areas + if fault_dates is not None: fault_hilight = [] for ts in fault_dates: box_ann = BoxAnnotation(left=ts[0], right=ts[1], fill_color='#db8370', fill_alpha=0.15) fault_hilight.append(box_ann) p.add_layout(box_ann) - # line plots - p.step('index', 'upstream_ta', source=src, color='#7093db', line_width=2) - p.step('index', 'dnstream_ta', source=src, color='#db7093', line_width=2) - - if 'air_flow' in vlv_dat.columns: - xtr_y_axis = {"airFlow": Range1d(start=right_y_min*(1-y_overlimit), end=right_y_max*(1+y_overlimit))} - p.extra_y_ranges = xtr_y_axis - p.add_layout(LinearAxis(y_range_name='airFlow', axis_label='Air flow [cfm]'), 'right') - p.step('index', 'air_flow', source=src, color='#93db70', y_range_name='airFlow', line_width=2) - - # range selector tool - range_tool = RangeTool(x_range=p.x_range) - range_tool.overlay.fill_color = "navy" - range_tool.overlay.fill_alpha = 0.2 - - select = figure(title="Orange highlight area represent passing valve operation", - plot_height=100, plot_width=800, - y_range=Range1d(start=-1, end=101), - x_axis_type="datetime", - tools="", toolbar_location=None, background_fill_color="#ffffff") - select.yaxis.axis_label = 'Valve' - - select.step('index', 'vlv_po', source=src, color='#70dbb8') - select.ygrid.grid_line_color = None - select.add_tools(range_tool) - select.toolbar.active_multi = range_tool - - select.extra_y_ranges = {"tempDiff": Range1d(start=right_sec_y_min*(1-y_overlimit), end=right_sec_y_max*(1+y_overlimit))} - select.add_layout(LinearAxis(y_range_name='tempDiff', axis_label='TDiff'), 'right') - select.step('index', 'temp_diff', source=src, color='#b870db', y_range_name='tempDiff') - + # line plots + p.step('index', 'upstream_ta', source=src, color='#7093db', line_width=2) + p.step('index', 'dnstream_ta', source=src, color='#db7093', line_width=2) + + if 'air_flow' in vlv_dat.columns: + p.extra_y_ranges = {"vlvPos": Range1d(start=-1, end=101), + "airFlow": Range1d(start=right_y_min*(1-y_overlimit), end=right_y_max*(1+y_overlimit)) + } + p.add_layout(LinearAxis(y_range_name='airFlow', axis_label='Air flow [cfm]'), 'right') + p.step('index', 'air_flow', source=src, color='#93db70', y_range_name='airFlow', line_width=2) + else: + p.extra_y_ranges = {"vlvPos": Range1d(start=-1, end=101)} + + p.add_layout(LinearAxis(y_range_name='vlvPos', axis_label='Valve position [%]'), 'left') + p.step('index', 'vlv_po', source=src, color='#9a9a9a', line_width=0.5, line_dash='4 4', y_range_name='vlvPos') + + # range selector tool + range_tool = RangeTool(x_range=p.x_range) + range_tool.overlay.fill_color = "navy" + range_tool.overlay.fill_alpha = 0.2 + + select = figure(title="Orange highlight area represent passing valve operation", + plot_height=100, plot_width=800, + y_range=Range1d(start=-1, end=101), + x_axis_type="datetime", + tools="", toolbar_location=None, background_fill_color="#ffffff") + select.yaxis.axis_label = 'Valve' + + select.step('index', 'vlv_po', source=src, color='#70dbb8') + select.ygrid.grid_line_color = None + select.add_tools(range_tool) + select.toolbar.active_multi = range_tool + + select.extra_y_ranges = {"tempDiff": Range1d(start=right_sec_y_min*(1-y_overlimit), end=right_sec_y_max*(1+y_overlimit))} + select.add_layout(LinearAxis(y_range_name='tempDiff', axis_label='TDiff'), 'right') + select.step('index', 'temp_diff', source=src, color='#b870db', y_range_name='tempDiff') + + if fault_dates is not None: for box_ann in fault_hilight: select.add_layout(box_ann) - # save plot - plot_name = '{}-timeseries.html'.format(csv.split('.csv')[0]) - output_file(join(fig_folder, plot_name)) - save(column(p, select)) - + # save plot + head, tail = os.path.split(csv_path) + plot_name = '{}-timeseries.html'.format(tail.split('.csv')[0]) + output_file(join(fig_folder, plot_name)) + save(column(p, select)) -if __name__ == '__main__': - - # define data sources - project_folder = join("./", "with_airflow_checks_year_end") - vlv_project_folder = join(project_folder, "csv_data") - all_csv_files = os.listdir(vlv_project_folder) - fig_folder = join(project_folder, 'timeseries_valve_faults') +def plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder): + """ + Plot timeseries data for valves that were detected as passing valves + """ if not os.path.exists(fig_folder): os.makedirs(fig_folder) + # get file paths of csvs + all_csv_files = os.listdir(vlv_dat_folder) + # read csv file with detected passing valves - fault_dat = pd.read_csv(join(project_folder, "passing_valve_results.csv"), index_col=False) + fault_dat = pd.read_csv(fault_dat_path, index_col=False) - # plot data from valves that were detect with passing valve operation for idx, df_row in fault_dat.iterrows(): if pd.notnull(df_row['long_term_fail_str_end_dates']): - plot_fault_valve_data(df_row, fig_folder) + fault_dates = df_row['long_term_fail_str_end_dates'] + fault_dates = parse_list_timestamps(fault_dates) + vlv_name = "{}-{}-{}".format(df_row['site'], df_row['equip'], df_row['vlv']) + csv_names = [f for f in all_csv_files if vlv_name in f] + + for csv in csv_names: + # plot fault data + csv_path = join(vlv_dat_folder, csv) + plot_valve_data(csv_path, fault_dates, fig_folder) + + print('-------Finished processing passing valve plots-----') + + +def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): + """ + Plot timeseries data for valves that have normal operation + """ + if not os.path.exists(fig_folder): + os.makedirs(fig_folder) + + # get file paths of csvs + all_csv_files = os.listdir(vlv_dat_folder) + good_valve_files = os.listdir(join(vlv_dat_folder, '../', 'good_valves')) + good_valve_files = [tail.split('.png')[0] for tail in good_valve_files] + + # sample define number of files + sample_files = random.sample(good_valve_files, sample_size) + + for sf in sample_files: + # plot fault data + csv_path = join(vlv_dat_folder,'{}_dat.csv'.format(sf)) + if os.path.exists(csv_path): + plot_valve_data(csv_path, fig_folder=fig_folder) + else: + print('{} valve csv file not found'.format(sf)) + + print('-------Finished processing good valve plots-----') + + +if __name__ == '__main__': + + # define data sources + project_folder = join("./", "with_airflow_checks_year_end") + vlv_dat_folder = join(project_folder, "csv_data") + + # fault data plots + fig_folder_faults = join(project_folder, 'timeseries_valve_faults') + fault_dat_path = join(project_folder, "passing_valve_results.csv") - print('-------Finished processing plots-----') + plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder_faults) + # good data plots + fig_folder_good = join(project_folder, 'timeseries_valve_good') + plot_good_valves(vlv_dat_folder, sample_size=20, fig_folder=fig_folder_good) From 2a8f0776d2a696d99c4ab8dcd8d9617edf21762f Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 29 Apr 2021 08:51:00 -0700 Subject: [PATCH 39/83] processed a different timeframe --- detect_passing_valves/app.py | 11 ++++++++--- ...3B.RM239B_LAB.Zone_Reheat_Valve_Command.png | Bin 147059 -> 0 bytes ...4A.RM041C_LAB.Zone_Reheat_Valve_Command.png | Bin 105774 -> 0 bytes 3 files changed, 8 insertions(+), 3 deletions(-) delete mode 100644 detect_passing_valves/bad_valves/brig-VAVRM239B_LAB-BRIG.ZONE.AHU03B.RM239B_LAB.Zone_Reheat_Valve_Command.png delete mode 100644 detect_passing_valves/good_valves/brig-VAVRM041C_LAB-BRIG.ZONE.AHU04A.RM041C_LAB.Zone_Reheat_Valve_Command.png diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 0c0e696..a1caff1 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1206,6 +1206,11 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): # container for holding types of faults passing_type = dict() + # files = os.listdir(join("with_airflow_checks", bad_folder)) + # vav_oi = [entry.split("-")[1] for entry in files] + # if row['equip'] in ['VAVRM2323']: + # import pdb; pdb.set_trace() + # check for empty dataframe if vlv_df.empty: print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) @@ -1489,12 +1494,12 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th pd.options.mode.chained_assignment = None # define parameters - eval_start_time = "2018-01-01T00:00:00Z" - eval_end_time = "2018-06-30T00:00:00Z" + eval_start_time = "2018-07-01T00:00:00Z" + eval_end_time = "2018-12-31T23:59:00Z" window = 15 th_bad_vlv = 10 th_time = [12, 45] - project_folder = './with_airflow_checks' + project_folder = './with_airflow_checks_year_end' # Run the app detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th_time, project_folder) \ No newline at end of file diff --git a/detect_passing_valves/bad_valves/brig-VAVRM239B_LAB-BRIG.ZONE.AHU03B.RM239B_LAB.Zone_Reheat_Valve_Command.png b/detect_passing_valves/bad_valves/brig-VAVRM239B_LAB-BRIG.ZONE.AHU03B.RM239B_LAB.Zone_Reheat_Valve_Command.png deleted file mode 100644 index f9f20a06fb10a94182968ea237b205c3806655c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147059 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¨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 Date: Fri, 5 Nov 2021 12:59:08 -0700 Subject: [PATCH 40/83] added option for printout and conditions if faults don't exist --- detect_passing_valves/app.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index a1caff1..cbddd8c 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1088,7 +1088,7 @@ def print_passing_mgs(row): print("Probable passing valve '{}' in site {}\n".format(row['vlv'], row['site'])) -def clean_final_report(final_df): +def clean_final_report(final_df, drop_null=True): """ Clean final report and sort by greatest number of minutes that fault was detected @@ -1096,28 +1096,36 @@ def clean_final_report(final_df): ---------- final_df: pandas dataframe with valve metadata along with failure types detected + drop_null: boolean to drop rows where no short or long term faults exist for valves. + Returns ------- final_df: cleaned and sorted report """ if 'long_term_fail' in final_df.columns: - final_df = final_df.loc[np.logical_or(~final_df['long_term_fail'].isnull(), ~final_df['short_term_fail'].isnull())] + if drop_null: + final_df = final_df.loc[np.logical_or(~final_df['long_term_fail'].isnull(), ~final_df['short_term_fail'].isnull())] + + if 'long_term_fail' in final_df.columns: + # separate data into multiple columns + final_df['long_term_fail_avg_minutes'] = final_df.long_term_fail.str[0] + final_df['long_term_fail_num_times_detected'] = final_df.long_term_fail.str[1] + final_df['long_term_fail_str_end_dates'] = final_df.long_term_fail.str[2] - # separate data into multiple columns - final_df['long_term_fail_avg_minutes'] = final_df.long_term_fail.str[0] - final_df['long_term_fail_num_times_detected'] = final_df.long_term_fail.str[1] - final_df['long_term_fail_str_end_dates'] = final_df.long_term_fail.str[2] + # drop redundant columns + final_df = final_df.drop(columns=['long_term_fail']) - final_df['short_term_fail_avg_minutes'] = final_df.short_term_fail.str[0] - final_df['short_term_fail_num_times_detected'] = final_df.short_term_fail.str[1] - final_df['short_term_fail_str_end_dates'] = final_df.short_term_fail.str[2] + if 'short_term_fail' in final_df.columns: + final_df['short_term_fail_avg_minutes'] = final_df.short_term_fail.str[0] + final_df['short_term_fail_num_times_detected'] = final_df.short_term_fail.str[1] + final_df['short_term_fail_str_end_dates'] = final_df.short_term_fail.str[2] + + # drop redundant columns + final_df = final_df.drop(columns=['short_term_fail']) # sort by highest value faults final_df = final_df.sort_values(by=['long_term_fail_avg_minutes', 'short_term_fail_avg_minutes'], ascending=False) - # drop redundant columns - final_df = final_df.drop(columns=['long_term_fail', 'short_term_fail']) - return final_df From f0f3d14158552c46a68b61d3eea793d35f97c586 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 5 Nov 2021 13:09:19 -0700 Subject: [PATCH 41/83] added parameters to function so it can run with Brick or mortar data --- detect_passing_valves/app.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index cbddd8c..6da6e99 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -690,7 +690,7 @@ def calc_long_t_diff(vlv_df): return long_t -def analyze_timestamps(vlv_df, th_time, window, row=None): +def analyze_timestamps(vlv_df, th_time, window, row=None, project_folder='./'): """ Analyze timestamps and valve operation in a pandas dataframe to determine which row values are th_time minutes after a changed state e.g. determine which data corresponds @@ -708,6 +708,8 @@ def analyze_timestamps(vlv_df, th_time, window, row=None): row: Pandas series object with metadata for the specific vav valve + project_folder: name of path for the project and used to save the plot. + Returns ------- vav_df: same input pandas dataframe but with added columns indicating: @@ -1189,7 +1191,7 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): return pass_type -def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): +def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folder='./', detection_params=None): """ Analyze each valve and detect for passing valves @@ -1205,12 +1207,21 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): valve operating point is malfunctioning e.g. allow enough time for residue heat to dissipate from the coil. + window: aggregation window, in minutes, to average the raw measurement data + project_folder: name of path for the project and used to save the plots and csv data. + detection_params: dictionary of parameters that control the behavior of the application + Returns ------- None """ + + # update variables + if detection_params is not None: + globals().update(detection_params) + # container for holding types of faults passing_type = dict() @@ -1229,7 +1240,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./'): _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots')) # Analyze timestamps and valve operation changes - vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row) + vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row, project_folder=project_folder) if vlv_df is None: print("'{}' in site {} has no data after analyzing \ From 3cc775da8f2f0aeffcdddc93b4253a140cf3ef7b Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 5 Nov 2021 13:10:33 -0700 Subject: [PATCH 42/83] modified rules for detecting long and short term passing vlvs --- detect_passing_valves/app.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 6da6e99..20d3c8d 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1059,16 +1059,19 @@ def find_bad_vlv_operation(vlv_df, model, window): # pass_type['long_term_fail'] = (ts_days+ts_seconds)/60.0 # detect long term failures - if any((bad_grp_count*window) > long_term_fail): - long_term_fail_times = bad_grp_count[(bad_grp_count*window) > long_term_fail]*window - if long_term_fail_times.count() > 2 or long_term_fail_times.index[-1] == vlv_df['same'].max(): + long_term_fail_bool = (bad_grp_count*window) > long_term_fail + if any(long_term_fail_bool): + long_term_fail_times = bad_grp_count[long_term_fail_bool]*window + if long_term_fail_times.count() >= 1 or long_term_fail_times.index[-1] == vlv_df['same'].max(): dates = [(bad_grp.groups[ky][0], bad_grp.groups[ky][-1]) for ky in long_term_fail_times.index] pass_type['long_term_fail'] = (long_term_fail_times.mean(), long_term_fail_times.count(), dates) # detect short term failures - if any((bad_grp_count*window) > shrt_term_fail): - shrt_term_fail_times = bad_grp_count[(bad_grp_count*window) > shrt_term_fail]*window - if shrt_term_fail_times.count() > 2: + bad_grp_left_over = bad_grp_count[~long_term_fail_bool] + short_term_fail_bool = (bad_grp_left_over*window) > shrt_term_fail + if any(short_term_fail_bool): + shrt_term_fail_times = bad_grp_left_over[short_term_fail_bool]*window + if shrt_term_fail_times.count() >= 2 or (shrt_term_fail_times.count() >= 1 and any(long_term_fail_bool)): dates = [(bad_grp.groups[ky][0], bad_grp.groups[ky][-1]) for ky in shrt_term_fail_times.index] pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count(), dates) From 88323df242cbc1d1f864b269da812e6fde4b9f7f Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 5 Nov 2021 13:12:11 -0700 Subject: [PATCH 43/83] delete NAs in airflow data before calculating density line --- detect_passing_valves/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 20d3c8d..50acf84 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -972,7 +972,11 @@ def density_data(dat, rescale_dat=None): ys: y values of the density function """ #create data for density plot - density = gaussian_kde(dat) + try: + density = gaussian_kde(dat) + except: + print("NAs exist is airflow data. Will delete them.") + density = gaussian_kde(dat[~dat.isna()]) xs = np.linspace(0, max(dat), 200) density.covariance_factor = lambda : 0.25 From 1da24414a3b5ea0113edbcf62e9c8220b32e6744 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 5 Nov 2021 13:13:29 -0700 Subject: [PATCH 44/83] added message for when there is no airflow data --- detect_passing_valves/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 50acf84..e17b8e0 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -396,6 +396,7 @@ def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5): df = df.loc[df['air_flow'] > min_air_flow] else: # drop values outside occupancy hours + print("No airflow data, using explicit occupancy hours to do analysis.") df = occupied_hours_subset(df, occ_str, occ_end, wkend_str) return df From fbaa0e2939794245d4701a522a518a799a4ce9e7 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 5 Nov 2021 13:14:28 -0700 Subject: [PATCH 45/83] added legends to timeseries plots --- detect_passing_valves/plot_data.py | 60 +++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index 0e90718..213cc0a 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -8,11 +8,11 @@ from bokeh.io import show, save, output_file from bokeh.layouts import column -from bokeh.models import ColumnDataSource, RangeTool, LinearAxis, Range1d, BoxAnnotation +from bokeh.models import ColumnDataSource, RangeTool, LinearAxis, Range1d, BoxAnnotation, Legend from bokeh.plotting import figure -def parse_list_timestamps(str_ts_list, tz='UTC'): +def parse_list_timestamps(str_ts_list, time_format="Timestamp('%Y-%m-%d %H:%M:%S%z', tz='UTC')"): """ Convert a list of pandas timestamps represented as a string to a readable format i.e. pandas timestamp object @@ -22,7 +22,7 @@ def parse_list_timestamps(str_ts_list, tz='UTC'): fault_dates = re.split(r'(?<=\)\)), ', fault_dates) f_dates = [re.split(r'(?<=\)), ', ts[1:-1]) for ts in fault_dates] - f_dts = [pd.to_datetime(ts, format="Timestamp('%Y-%m-%d %H:%M:%S%z', tz='{}')".format(tz)) for ts in f_dates] + f_dts = [pd.to_datetime(ts, format=time_format) for ts in f_dates] return f_dts @@ -49,7 +49,9 @@ def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): right_y_min = np.floor(vlv_dat['air_flow'].min()) sub_cols.append('air_flow') - src = ColumnDataSource(vlv_dat.loc[:, sub_cols]) + subset_dat = vlv_dat.loc[:, sub_cols] + src = ColumnDataSource(subset_dat) + index_name = subset_dat.index.name # make the plot max_date_idx = min(480, len(vlv_dat.index)) @@ -70,20 +72,30 @@ def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): p.add_layout(box_ann) # line plots - p.step('index', 'upstream_ta', source=src, color='#7093db', line_width=2) - p.step('index', 'dnstream_ta', source=src, color='#db7093', line_width=2) + p.step(index_name, 'upstream_ta', source=src, color='#7093db', line_width=2, legend_label="Upstream temp") + p.step(index_name, 'dnstream_ta', source=src, color='#db7093', line_width=2, legend_label="Downstream temp") if 'air_flow' in vlv_dat.columns: p.extra_y_ranges = {"vlvPos": Range1d(start=-1, end=101), "airFlow": Range1d(start=right_y_min*(1-y_overlimit), end=right_y_max*(1+y_overlimit)) } p.add_layout(LinearAxis(y_range_name='airFlow', axis_label='Air flow [cfm]'), 'right') - p.step('index', 'air_flow', source=src, color='#93db70', y_range_name='airFlow', line_width=2) + p.step(index_name, 'air_flow', source=src, color='#93db70', y_range_name='airFlow', line_width=2, legend_label="Airflow rate") else: p.extra_y_ranges = {"vlvPos": Range1d(start=-1, end=101)} + # add valve data p.add_layout(LinearAxis(y_range_name='vlvPos', axis_label='Valve position [%]'), 'left') - p.step('index', 'vlv_po', source=src, color='#9a9a9a', line_width=0.5, line_dash='4 4', y_range_name='vlvPos') + p.step(index_name, 'vlv_po', source=src, color='#9a9a9a', line_width=0.5, line_dash='4 4', y_range_name='vlvPos', legend_label="Valve position") + + # add legend + p.add_layout(Legend(), 'right') + p.legend.click_policy = "hide" + + p.legend.label_text_font_size = "9px" + p.legend.label_height = 5 + p.legend.glyph_height = 5 + p.legend.spacing = 5 # range selector tool range_tool = RangeTool(x_range=p.x_range) @@ -97,14 +109,23 @@ def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): tools="", toolbar_location=None, background_fill_color="#ffffff") select.yaxis.axis_label = 'Valve' - select.step('index', 'vlv_po', source=src, color='#70dbb8') + select.step(index_name, 'vlv_po', source=src, color='#70dbb8', legend_label="Valve position") select.ygrid.grid_line_color = None select.add_tools(range_tool) select.toolbar.active_multi = range_tool select.extra_y_ranges = {"tempDiff": Range1d(start=right_sec_y_min*(1-y_overlimit), end=right_sec_y_max*(1+y_overlimit))} - select.add_layout(LinearAxis(y_range_name='tempDiff', axis_label='TDiff'), 'right') - select.step('index', 'temp_diff', source=src, color='#b870db', y_range_name='tempDiff') + select.add_layout(LinearAxis(y_range_name='tempDiff', axis_label='TDiff [F]'), 'right') + select.step(index_name, 'temp_diff', source=src, color='#b870db', y_range_name='tempDiff', legend_label="Temp Difference") + + # add legend + select.add_layout(Legend(), 'right') + select.legend.click_policy = "hide" + + select.legend.label_text_font_size = "9px" + select.legend.label_height = 5 + select.legend.glyph_height = 5 + select.legend.spacing = 5 if fault_dates is not None: for box_ann in fault_hilight: @@ -117,7 +138,7 @@ def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): save(column(p, select)) -def plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder): +def plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder, time_format="Timestamp('%Y-%m-%d %H:%M:%S%z', tz='UTC')"): """ Plot timeseries data for valves that were detected as passing valves """ @@ -134,7 +155,7 @@ def plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder): for idx, df_row in fault_dat.iterrows(): if pd.notnull(df_row['long_term_fail_str_end_dates']): fault_dates = df_row['long_term_fail_str_end_dates'] - fault_dates = parse_list_timestamps(fault_dates) + fault_dates = parse_list_timestamps(fault_dates, time_format=time_format) vlv_name = "{}-{}-{}".format(df_row['site'], df_row['equip'], df_row['vlv']) csv_names = [f for f in all_csv_files if vlv_name in f] @@ -159,7 +180,12 @@ def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): good_valve_files = [tail.split('.png')[0] for tail in good_valve_files] # sample define number of files - sample_files = random.sample(good_valve_files, sample_size) + if sample_size == 'all': + sample_files = good_valve_files + elif sample_size > len(good_valve_files): + sample_files = good_valve_files + else: + sample_files = random.sample(good_valve_files, sample_size) for sf in sample_files: # plot fault data @@ -175,17 +201,17 @@ def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): if __name__ == '__main__': # define data sources - project_folder = join("./", "with_airflow_checks_year_end") + project_folder = join('./', 'external_analysis', 'lg_4hr_shrt_1hr_updated_no_off_period') vlv_dat_folder = join(project_folder, "csv_data") # fault data plots fig_folder_faults = join(project_folder, 'timeseries_valve_faults') fault_dat_path = join(project_folder, "passing_valve_results.csv") - plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder_faults) + plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder_faults, time_format="Timestamp('%Y-%m-%d %H:%M:%S')") # good data plots fig_folder_good = join(project_folder, 'timeseries_valve_good') - plot_good_valves(vlv_dat_folder, sample_size=20, fig_folder=fig_folder_good) + plot_good_valves(vlv_dat_folder, sample_size='all', fig_folder=fig_folder_good) From 2853a8cb167d8424a50b36e8258df7604e5ce258 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 5 Nov 2021 13:24:53 -0700 Subject: [PATCH 46/83] adjusted position of 'bad statistic' text within plot --- detect_passing_valves/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index e17b8e0..45c471c 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -852,11 +852,16 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N ------- None """ + + # plot parametes + y_max = vlv_df['temp_diff'].max() + # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Valve opened [%]') ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') + ax.set_ylim((0, np.ceil(y_max*1.05))) if 'color' in vlv_df.columns: ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = vlv_df['color'], alpha=1/3, s=10) @@ -876,7 +881,6 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N if bad_ratio is not None: # add ratio where presumably passing valve - y_max = vlv_df['temp_diff'].max() ax.text(.2, 0.95*y_max, "Bad ratio={:.1f}%".format(bad_ratio)) plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) From 38f5eb6c2065c596239b3a17813404e544f6ebc4 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 10 Nov 2021 16:06:40 -0800 Subject: [PATCH 47/83] modified method to remove missing stream timestamps --- detect_passing_valves/app.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 45c471c..2fdebfd 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -355,7 +355,7 @@ def _clean_vav(fetch_resp_vav, row): vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] # drop na - vav_df = vav_df.dropna() + # vav_df = vav_df.dropna() # drop values where vav supply air is less than ahu supply air vav_df = vav_df[vav_df['temp_diff'] >= 0] @@ -363,7 +363,7 @@ def _clean_vav(fetch_resp_vav, row): return vav_df -def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5): +def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=False): """ Drop data rows from dataframe for timeseries that are during unoccupied hours. Uses airflow data if available else it uses building occupancy hours. @@ -394,6 +394,8 @@ def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5): min_air_flow = np.percentile(xs, 5) df = df.loc[df['air_flow'] > min_air_flow] + elif 'air_flow' not in df.columns and air_flow_required: + df = pd.DataFrame() else: # drop values outside occupancy hours print("No airflow data, using explicit occupancy hours to do analysis.") @@ -510,7 +512,7 @@ def _clean_ahu(fetch_resp_ahu, row): ahu_df['temp_diff'] = ahu_df['dnstream_ta'] - ahu_df['upstream_ta'] # drop na - ahu_df = ahu_df.dropna() + # ahu_df = ahu_df.dropna() # drop values where vav supply air is less than ahu supply air #ahu_df = ahu_df[ahu_df['temp_diff'] >= 0] @@ -750,9 +752,12 @@ def analyze_timestamps(vlv_df, th_time, window, row=None, project_folder='./'): if row is not None: _name = "{}-{}-{}_dat".format(row['site'], row['equip'], row['vlv']) - full_path = rename_existing(join(project_folder, "csv_data", _name + '.csv'), idx=0, row=row) + full_path = rename_existing(join(project_folder, csv_folder, _name + '.csv'), idx=0, row=row) vlv_df.to_csv(full_path) + # drop rows of data where valve position is unknown + vlv_df = vlv_df.dropna(subset=['vlv_po']) + return vlv_df @@ -1260,7 +1265,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde return passing_type # drop data that occurs during unoccupied hours - vlv_df = drop_unoccupied_dat(vlv_df, occ_str=6, occ_end=18, wkend_str=5) + vlv_df = drop_unoccupied_dat(vlv_df, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=air_flow_required) if vlv_df.empty: print("'{}' in site {} has no data after hours of \ From 63daa95601c1d1eac1ff24750032f9e9c96e75a5 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 10 Nov 2021 16:07:41 -0800 Subject: [PATCH 48/83] added legends to plots --- detect_passing_valves/app.py | 70 +++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 2fdebfd..4bd0a85 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -861,6 +861,9 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N # plot parametes y_max = vlv_df['temp_diff'].max() + good_oper_color = '#5ab300' + bad_oper_color = '#b3005a' + # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') @@ -868,25 +871,36 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') ax.set_ylim((0, np.ceil(y_max*1.05))) - if 'color' in vlv_df.columns: - ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = vlv_df['color'], alpha=1/3, s=10) - else: - ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = '#005ab3', alpha=1/3, s=10) + + if any(~vlv_df['good_oper_cat']): + ax.scatter(x=vlv_df.loc[~vlv_df['good_oper_cat'], 'vlv_po'], y=vlv_df.loc[~vlv_df['good_oper_cat'], 'temp_diff'], color = bad_oper_color, alpha=1/3, s=10, label='Pred. bad operation') + + if any(vlv_df['good_oper_cat']): + ax.scatter(x=vlv_df.loc[vlv_df['good_oper_cat'], 'vlv_po'], y=vlv_df.loc[vlv_df['good_oper_cat'], 'temp_diff'], color = good_oper_color, alpha=1/3, s=10, label='Pred. good operation') + + # if 'color' in vlv_df.columns: + # ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = vlv_df['color'], alpha=1/3, s=10) + # else: + # ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = '#005ab3', alpha=1/3, s=10) if df_fit is not None: # add fit line - ax.plot(df_fit['vlv_po'], df_fit['y_fitted'], '--', label='fitted', color='#5900b3') + ax.plot(df_fit['vlv_po'], df_fit['y_fitted'], '--', label='Fitted valve model', color='#5900b3') if long_t is not None: # add long-term temperature diff - ax.axhline(y=long_t, color='#00b3b3') + ax.axhline(y=long_t, color='#00b3b3', label='Est. Td (closed valve-good)') if long_tbad is not None: - ax.axhline(y=long_tbad, color='#ff8cc6') + ax.axhline(y=long_tbad, color='#ff8cc6', label='Est. Td (closed valve-bad)') if bad_ratio is not None: # add ratio where presumably passing valve - ax.text(.2, 0.95*y_max, "Bad ratio={:.1f}%".format(bad_ratio)) + ax.text(.2, 0.95*y_max, "Bad operation ratio={:.1f}%".format(bad_ratio)) + + # legend + # ax.legend(fontsize=8, markerscale=1, borderaxespad=0., ncol=2, loc='upper right', bbox_to_anchor=(0.15, 1.05, 1., .102)) + ax.legend(fontsize=6, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.55, 1.02), loc='lower left') plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) @@ -932,16 +946,25 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): None """ + # plot parametes + closed_vlv_color = '#640064' + open_vlv_color = '#006400' + + vlv_df.loc[:, 'color_open'] = closed_vlv_color + vlv_df.loc[vlv_df['vlv_open'], 'color_open'] = open_vlv_color + # plot temperature difference vs valve position fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Air flow [cfm]') ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') - vlv_df.loc[:, 'color_open'] = '#640064' - vlv_df.loc[vlv_df['vlv_open'], 'color_open'] = '#006400' + if any(~vlv_df['vlv_open']): + ax.scatter(x=vlv_df.loc[~vlv_df['vlv_open'], 'air_flow'], y=vlv_df.loc[~vlv_df['vlv_open'], 'temp_diff'], color = closed_vlv_color, alpha=1/3, s=10, label='Closed valve') - ax.scatter(x=vlv_df['air_flow'], y=vlv_df['temp_diff'], color = vlv_df['color_open'], alpha=1/3, s=10) + if any(vlv_df['vlv_open']): + ax.scatter(x=vlv_df.loc[vlv_df['vlv_open'], 'air_flow'], y=vlv_df.loc[vlv_df['vlv_open'], 'temp_diff'], color = open_vlv_color, alpha=1/3, s=10, label='Open valve') + # ax.scatter(x=vlv_df['air_flow'], y=vlv_df['temp_diff'], color = vlv_df['color_open'], alpha=1/3, s=10) # create density plot for air flow xs, ys = density_data(vlv_df['air_flow'], rescale_dat=vlv_df['temp_diff']) @@ -956,6 +979,9 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): if max_idx is not None: ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35) + # Legend + ax.legend(markerscale=2) + plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) plt.savefig(full_path) @@ -1175,7 +1201,13 @@ def analyze_only_open(vlv_df, row, th_bad_vlv, project_folder): if long_to['50%'] < th_bad_vlv: print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) pass_type['non_responsive_fail'] = round(long_to['50%'] - th_bad_vlv, 2) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=join(project_folder, bad_folder)) + folder = join(project_folder, bad_folder) + import pdb; pdb.set_trace() + else: + vlv_df.loc[:, 'good_oper_cat'] = True + folder = join(project_folder, good_folder) + + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=folder) return pass_type @@ -1204,7 +1236,13 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): if long_tc['50%'] > th_bad_vlv: print_passing_mgs(row) pass_type['simple_fail'] = round(long_tc['50%'] - th_bad_vlv, 2) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=join(project_folder, bad_folder)) + folder = join(project_folder, bad_folder) + import pdb; pdb.set_trace() + else: + vlv_df.loc[:, 'good_oper_cat'] = True + folder = join(project_folder, good_folder) + + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=folder) return pass_type @@ -1342,10 +1380,10 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde else: folder = join(project_folder, good_folder) + # categorized good and bad points + vlv_df.loc[:, 'good_oper_cat'] = True if bad_vlv is not None: - # colorize good and bad points - vlv_df.loc[:, 'color'] = '#5ab300' - vlv_df.loc[bad_vlv.index, 'color'] = '#b3005a' + vlv_df.loc[bad_vlv.index, 'good_oper_cat'] = False _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) From 13a4748d17c1e1da5393065cac14a81179fa9e82 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 10 Nov 2021 16:08:41 -0800 Subject: [PATCH 49/83] added helper script to prepare external non-mortar data for analysis --- detect_passing_valves/ext_dat_app_run.py | 227 ++++++++++++++ detect_passing_valves/ext_dat_app_run_gt.py | 317 ++++++++++++++++++++ detect_passing_valves/plot_data.py | 2 +- 3 files changed, 545 insertions(+), 1 deletion(-) create mode 100644 detect_passing_valves/ext_dat_app_run.py create mode 100644 detect_passing_valves/ext_dat_app_run_gt.py diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py new file mode 100644 index 0000000..1dcf8c9 --- /dev/null +++ b/detect_passing_valves/ext_dat_app_run.py @@ -0,0 +1,227 @@ +""" +Run the detect passing valve algorithm on external building data. +The data needed to run application are the following: + timestamp + upstream vav air temperature + downstream vav air temperature + vav valve position + vav airflow rate +""" + +import pandas as pd +import numpy as np +import os +import time + +from os.path import join +import matplotlib.pyplot as plt +from scipy.optimize import curve_fit +from scipy.stats import gaussian_kde + +from app import _analyze_vlv, check_folder_exist, clean_final_report + +# from app import _make_tdiff_vs_aflow_plot, \ +# analyze_timestamps, rename_existing, drop_unoccupied_dat, calc_long_t_diff, \ +# build_logistic_model, find_bad_vlv_operation, print_passing_mgs, _make_tdiff_vs_vlvpo_plot + + +def read_multi_csvs(csv_list): + """ + Read and combine multiple csv file containing the same + data streams e.g. 1 file per month, year, or other time interval + """ + + dfs = [] + for csv_file in csv_list: + csv = pd.read_csv(csv_file, index_col=0, parse_dates=True) + + # verify that columns are numeric + for col in csv.columns: + csv.loc[:, col] = pd.to_numeric(csv.loc[:, col], errors="coerce") + + dfs.append(csv) + + df_merge = pd.concat(dfs) + + # aggregate any repeated timestamps + df_merge = df_merge.sort_index() + df_merge = df_merge.groupby(level=0).mean() + + return df_merge + + +def clean_df(df): + """ + prepare data for the algorithm + """ + vav_datastream_labels = df.columns[4:] + + vav_equip = [lab.split("/")[0] for lab in vav_datastream_labels.values] + stream_type = [lab.split("/")[1] for lab in vav_datastream_labels.values] + + # map to standard names in the app + com_stream_map = { + 'upstream_ta': 'ahu-3/sa_temp', + 'oat': 'OA Temp' + } + + ind_stream_map = { + 'dnstream_ta': 'da_temp', + 'vlv_po': 'hw_valve', + 'air_flow': 'flow_tn', + 'zone_temp': 'zone_temp' + } + + vavs = {} + for vav in np.unique(vav_equip): + common_keys = list(com_stream_map.keys()) + vlv_dat = df.loc[:, com_stream_map[common_keys[0]]] + vlv_dat.name = common_keys[0] + + if len(common_keys) > 1: + for com_pt in common_keys[1:]: + vlv_dat = pd.concat([vlv_dat, df.loc[:, com_stream_map[com_pt]]], axis=1) + vlv_dat = vlv_dat.rename(columns={com_stream_map[com_pt]: com_pt}) + + for stream in ind_stream_map.keys(): + vav_stream = '/'.join([vav, ind_stream_map[stream]]) + if vav_stream in vav_datastream_labels.values: + new_stream = df.loc[:, vav_stream] + + vlv_dat = pd.concat([vlv_dat, new_stream], axis=1) + vlv_dat = vlv_dat.rename(columns={vav_stream: stream}) + + # verify that all necessary data points are available + stream_avail = [col in vlv_dat.columns for col in ['upstream_ta', 'dnstream_ta', 'vlv_po']] + + if not all(stream_avail): + print(f"VAV={vav} does not have all required data streams\n") + print(f"Missing {3 - np.count_nonzero(stream_avail)} streams, please check.\n") + continue + + # save in a dictionary + vavs[vav] = { + 'vlv_dat': vlv_dat, + 'row': { + 'vlv': 'vlv_' + vav, + 'site': 'bldg_trc_rs', + 'equip': vav, + 'upstream_type': None, + } + } + + return vavs + + +def calc_add_features(vav_df, drop_na=False): + """ + Calculate additional features needed for application + """ + # identify when valve is open + vav_df['vlv_open'] = vav_df['vlv_po'] > 0 + + # calculate temperature difference between downstream and upstream air + vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] + + # drop na + if drop_na: + vav_df = vav_df.dropna() + + # drop values where vav supply air is less than ahu supply air + vav_df = vav_df[vav_df['temp_diff'] >= 0] + + return vav_df + + +def CountFrequency(my_list): + + # Creating an empty dictionary + freq = {} + for items in np.unique(my_list): + freq[items] = my_list.count(items) + + return freq + + +def exclude_time_interval(df, int_str, int_end): + """ + Exclude time interval where data is not representative + """ + + time_interval = pd.to_datetime([int_str, int_end]) + within_interval = np.logical_and(df.index > time_interval[0], df.index < time_interval[1]) + + return df.loc[~within_interval, :] + + +if __name__ == '__main__': + # NOTE: HW plant off from August 31 to October 7 2021 + dat_folder = join('./', 'external_data', 'bldg_trc_rs') + project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov9_no_off_period') + + csv_list = [ + join(dat_folder, 'zone trends, September 2021.csv'), + join(dat_folder, 'zone trends, October 2021.csv'), + join(dat_folder, 'Schoellkopf zone trends 20211103_A.csv'), + join(dat_folder, 'Schoellkopf zone trends 20211103_B.csv'), + join(dat_folder, 'Schoellkopf zone trends 20211109.csv'), + ] + + + # define container folders + good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves + bad_folder = 'bad_valves' # name of path to the folder to save the plots of the malfunction valves + air_flow_folder = 'air_flow_plots' # name of path to the folder to save plots of the air flow values + csv_folder = 'csv_data' # name of path to the folder to save detailed valve data + + # check if holding folders exist + check_folder_exist(join(project_folder, bad_folder)) + check_folder_exist(join(project_folder, good_folder)) + check_folder_exist(join(project_folder, air_flow_folder)) + check_folder_exist(join(project_folder, csv_folder)) + + # define user parameters + detection_params = { + "th_bad_vlv": 5, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning + "window": 15, # aggregation window, in minutes, to average the raw measurement data + "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure + "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure + "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. + "air_flow_required": True, # boolean indicated is air flow rate data should strictly be used. + "good_folder": good_folder, + "bad_folder": bad_folder, + "air_flow_folder": air_flow_folder, + "csv_folder": csv_folder, + } + + df = read_multi_csvs(csv_list) + + vavs_df = clean_df(df) + + results = [] + for key in vavs_df.keys(): + vavs_df[key]['vlv_dat'] = calc_add_features(vavs_df[key]['vlv_dat']) + vlv_df = vavs_df[key]['vlv_dat'] + row = vavs_df[key]['row'] + + # remove data when heat system was off + off_str = '08/31/2021' + off_end = '10/07/2021' + + vlv_df = exclude_time_interval(vlv_df, off_str, off_end) + + # define variables + vlv_dat = dict(row) + # run passing valve detection algorithm + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=5, project_folder=project_folder, detection_params=detection_params) + + # save results + vlv_dat.update(passing_type) + results.append(vlv_dat) + + final_df = pd.DataFrame.from_records(results) + final_df = clean_final_report(final_df, drop_null=False) + final_df.to_csv(join(project_folder, "passing_valve_results.csv")) + + import pdb; pdb.set_trace() \ No newline at end of file diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py new file mode 100644 index 0000000..a145515 --- /dev/null +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -0,0 +1,317 @@ +""" +Run the detect passing valve algorithm on external building data. +The data needed to run application are the following: + timestamp + upstream vav air temperature + downstream vav air temperature + vav valve position + vav airflow rate +""" + +import pandas as pd +import numpy as np +import os +import time + +from os.path import join +import matplotlib.pyplot as plt +from scipy.optimize import curve_fit +from scipy.stats import gaussian_kde +from itertools import combinations +from copy import deepcopy + +from app import _analyze_vlv, check_folder_exist, clean_final_report + +# from app import _make_tdiff_vs_aflow_plot, \ +# analyze_timestamps, rename_existing, drop_unoccupied_dat, calc_long_t_diff, \ +# build_logistic_model, find_bad_vlv_operation, print_passing_mgs, _make_tdiff_vs_vlvpo_plot + + +def read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file): + """ + Read and combine multiple csv files which contain the same + data stream types within one files e.g. all discharge air temps + within 1 file. + """ + + discharge_temp = pd.read_csv(discharge_temp_file, index_col=0, parse_dates=True) + vlv_pos = pd.read_csv(vlv_pos_file, index_col=0, parse_dates=True) + airflow = pd.read_csv(airflow_rate_file, index_col=0, parse_dates=True) + + col_labels = { + "dnstream_ta": [vav.split(":")[0] for vav in discharge_temp.columns.values], + "vlv_po": [vav.split(":")[0] for vav in vlv_pos.columns.values], + "air_flow": [vav.split(":")[0] for vav in airflow.columns.values], + } + + dat_stream_map = { + "dnstream_ta": discharge_temp, + "vlv_po": vlv_pos, + "air_flow": airflow, + } + + + # start to match columns + dat_streams_list = col_labels + dat_stream_comb = list(combinations(dat_streams_list, 2)) + matched_streams = {} + + for i, dat_comb in enumerate(dat_stream_comb): + for j, col in enumerate(col_labels[dat_comb[0]]): + if col in matched_streams.keys(): + matched_streams[col].update({dat_comb[0]: j}) + else: + matched_streams[col] = {dat_comb[0]: j} + key = dat_comb[1] + if col in col_labels[key]: + match_stream = [(vav, k) for k, vav in enumerate(col_labels[key]) if col == vav] + if len(match_stream) == 1: + matched_streams[match_stream[0][0]].update({key: match_stream[0][1]}) + else: + print("More than two streams matched") + import pdb; pdb.set_trace() + + # remove complete data from dictionary container + complete_vav = [] + for vav in matched_streams.keys(): + if len(matched_streams[vav]) == 3: + complete_vav.append(matched_streams[vav]) + + # retreive full column names based on index of column + full_col_names_matched_streams = deepcopy(matched_streams) + for vav in matched_streams: + for stream in matched_streams[vav].keys(): + col_idx = matched_streams[vav][stream] + full_col_name = dat_stream_map[stream].columns[col_idx] + full_col_names_matched_streams[vav][stream] = full_col_name + + matched_streams_summary = pd.DataFrame.from_records(full_col_names_matched_streams).transpose() + matched_streams_summary = matched_streams_summary.sort_values(by=list(col_labels.keys())) + matched_streams_summary.to_csv(join(dat_folder, 'matched_streams_summary.csv'), index_label='vav_label') + + tags = ['Equip Fail', 'Scan Off'] + + ## Alternative way to match columns + + # # figure out which file has the most columns + # max_label = (0, '') + # for key in col_labels: + # col_len = len(col_labels[key]) + # if col_len > max_label[0]: + # max_label = (col_len, key) + + # matched_streams = [] + # other_streams = [key for key in col_labels.keys() if key not in max_label[1]] + # for i, col in enumerate(col_labels[max_label[1]]): + # cur_vav_unit = [(max_label[1], col, i)] + # for key in other_streams: + # if col in col_labels[key]: + # match_stream = [(vav, j) for j, vav in enumerate(col_labels[key]) if col == vav] + # if len(match_stream) == 1: + # cur_vav_unit.append((key, match_stream[0][0], match_stream[0][1])) + # else: + # print("More than two streams matched") + # import pdb; pdb.set_trace() + # matched_streams.append(cur_vav_unit) + + # # determine which streams have completed data + # complete_vav = [] + # for vav in matched_streams: + # if len(vav) == 3: + # complete_vav.append(vav) + # for stream in vav: + # col_labels[stream[0]].remove(stream[1]) + + + + + + + dfs = [] + for csv_file in csv_list: + csv = pd.read_csv(csv_file, index_col=0, parse_dates=True) + + # verify that columns are numeric + for col in csv.columns: + csv.loc[:, col] = pd.to_numeric(csv.loc[:, col], errors="coerce") + + dfs.append(csv) + + df_merge = pd.concat(dfs) + + # aggregate any repeated timestamps + df_merge = df_merge.sort_index() + df_merge = df_merge.groupby(level=0).mean() + + return df_merge + + +def clean_df(df): + """ + prepare data for the algorithm + """ + vav_datastream_labels = df.columns[4:] + + vav_equip = [lab.split("/")[0] for lab in vav_datastream_labels.values] + stream_type = [lab.split("/")[1] for lab in vav_datastream_labels.values] + + # map to standard names in the app + com_stream_map = { + 'upstream_ta': 'ahu-3/sa_temp', + 'oat': 'OA Temp' + } + + ind_stream_map = { + 'dnstream_ta': 'da_temp', + 'vlv_po': 'hw_valve', + 'air_flow': 'flow_tn', + 'zone_temp': 'zone_temp' + } + + vavs = {} + for vav in np.unique(vav_equip): + common_keys = list(com_stream_map.keys()) + vlv_dat = df.loc[:, com_stream_map[common_keys[0]]] + vlv_dat.name = common_keys[0] + + if len(common_keys) > 1: + for com_pt in common_keys[1:]: + vlv_dat = pd.concat([vlv_dat, df.loc[:, com_stream_map[com_pt]]], axis=1) + vlv_dat = vlv_dat.rename(columns={com_stream_map[com_pt]: com_pt}) + + for stream in ind_stream_map.keys(): + vav_stream = '/'.join([vav, ind_stream_map[stream]]) + if vav_stream in vav_datastream_labels.values: + new_stream = df.loc[:, vav_stream] + + vlv_dat = pd.concat([vlv_dat, new_stream], axis=1) + vlv_dat = vlv_dat.rename(columns={vav_stream: stream}) + + # verify that all necessary data points are available + stream_avail = [col in vlv_dat.columns for col in ['upstream_ta', 'dnstream_ta', 'vlv_po']] + + if not all(stream_avail): + print(f"VAV={vav} does not have all required data streams\n") + print(f"Missing {3 - np.count_nonzero(stream_avail)} streams, please check.\n") + continue + + # save in a dictionary + vavs[vav] = { + 'vlv_dat': vlv_dat, + 'row': { + 'vlv': 'vlv_' + vav, + 'site': 'bldg_trc_rs', + 'equip': vav, + 'upstream_type': None, + } + } + + return vavs + + +def calc_add_features(vav_df, drop_na=False): + """ + Calculate additional features needed for application + """ + # identify when valve is open + vav_df['vlv_open'] = vav_df['vlv_po'] > 0 + + # calculate temperature difference between downstream and upstream air + vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] + + # drop na + if drop_na: + vav_df = vav_df.dropna() + + # drop values where vav supply air is less than ahu supply air + vav_df = vav_df[vav_df['temp_diff'] >= 0] + + return vav_df + + +def CountFrequency(my_list): + + # Creating an empty dictionary + freq = {} + for items in np.unique(my_list): + freq[items] = my_list.count(items) + + return freq + + +def exclude_time_interval(df, int_str, int_end): + """ + Exclude time interval where data is not representative + """ + + time_interval = pd.to_datetime([int_str, int_end]) + within_interval = np.logical_and(df.index > time_interval[0], df.index < time_interval[1]) + + return df.loc[~within_interval, :] + + +if __name__ == '__main__': + dat_folder = join('./', 'external_data', 'bldg_gt_pr') + project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_off_period') + + # read files + discharge_temp_file = join(dat_folder, 'B44-B45 Discharge Air Temp Sensor Readings - 01MAY2021 to 04NOV2021.csv') + airflow_rate_file = join(dat_folder, 'INC5088495 AIR VOL.csv') + vlv_pos_file = join(dat_folder, 'INC5088495 VLV CMD.csv') + + # define container folders + good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves + bad_folder = 'bad_valves' # name of path to the folder to save the plots of the malfunction valves + air_flow_folder = 'air_flow_plots' # name of path to the folder to save plots of the air flow values + csv_folder = 'csv_data' # name of path to the folder to save detailed valve data + + # check if holding folders exist + check_folder_exist(join(project_folder, bad_folder)) + check_folder_exist(join(project_folder, good_folder)) + check_folder_exist(join(project_folder, air_flow_folder)) + check_folder_exist(join(project_folder, csv_folder)) + + # define user parameters + detection_params = { + "th_bad_vlv": 5, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning + "window": 15, # aggregation window, in minutes, to average the raw measurement data + "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure + "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure + "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. + "good_folder": good_folder, + "bad_folder": bad_folder, + "air_flow_folder": air_flow_folder, + "csv_folder": csv_folder, + } + + df = read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file) + + vavs_df = clean_df(df) + + results = [] + for key in vavs_df.keys(): + vavs_df[key]['vlv_dat'] = calc_add_features(vavs_df[key]['vlv_dat']) + vlv_df = vavs_df[key]['vlv_dat'] + row = vavs_df[key]['row'] + + # remove data when heat system was off + off_str = '08/31/2021' + off_end = '10/07/2021' + + vlv_df = exclude_time_interval(vlv_df, off_str, off_end) + + # define variables + vlv_dat = dict(row) + # run passing valve detection algorithm + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=5, project_folder=project_folder, detection_params=detection_params) + + # save results + vlv_dat.update(passing_type) + results.append(vlv_dat) + + final_df = pd.DataFrame.from_records(results) + final_df = clean_final_report(final_df, drop_null=False) + final_df.to_csv(join(project_folder, "passing_valve_results.csv")) + + import pdb; pdb.set_trace() \ No newline at end of file diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index 213cc0a..564a69e 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -201,7 +201,7 @@ def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): if __name__ == '__main__': # define data sources - project_folder = join('./', 'external_analysis', 'lg_4hr_shrt_1hr_updated_no_off_period') + project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov9_no_off_period') vlv_dat_folder = join(project_folder, "csv_data") # fault data plots From 7bba3215759e5075b628a3a3a105d58c3c8783ff Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 22 Nov 2021 09:09:26 -0800 Subject: [PATCH 50/83] working script for gt bldgs --- detect_passing_valves/app.py | 2 +- detect_passing_valves/ext_dat_app_run_gt.py | 204 ++++++++------------ 2 files changed, 78 insertions(+), 128 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 4bd0a85..c7d4843 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1202,7 +1202,7 @@ def analyze_only_open(vlv_df, row, th_bad_vlv, project_folder): print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) pass_type['non_responsive_fail'] = round(long_to['50%'] - th_bad_vlv, 2) folder = join(project_folder, bad_folder) - import pdb; pdb.set_trace() + vlv_df.loc[:, 'good_oper_cat'] = False else: vlv_df.loc[:, 'good_oper_cat'] = True folder = join(project_folder, good_folder) diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index a145515..9e4a68d 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -27,7 +27,7 @@ # build_logistic_model, find_bad_vlv_operation, print_passing_mgs, _make_tdiff_vs_vlvpo_plot -def read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file): +def read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file, room_temp_file, dat_folder): """ Read and combine multiple csv files which contain the same data stream types within one files e.g. all discharge air temps @@ -37,20 +37,22 @@ def read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file): discharge_temp = pd.read_csv(discharge_temp_file, index_col=0, parse_dates=True) vlv_pos = pd.read_csv(vlv_pos_file, index_col=0, parse_dates=True) airflow = pd.read_csv(airflow_rate_file, index_col=0, parse_dates=True) + rm_temp = pd.read_csv(room_temp_file, index_col=0, parse_dates=True) col_labels = { "dnstream_ta": [vav.split(":")[0] for vav in discharge_temp.columns.values], "vlv_po": [vav.split(":")[0] for vav in vlv_pos.columns.values], "air_flow": [vav.split(":")[0] for vav in airflow.columns.values], + "rm_temp": [vav.split(":")[0] for vav in rm_temp.columns.values], } dat_stream_map = { "dnstream_ta": discharge_temp, "vlv_po": vlv_pos, "air_flow": airflow, + "rm_temp": rm_temp, } - # start to match columns dat_streams_list = col_labels dat_stream_comb = list(combinations(dat_streams_list, 2)) @@ -70,12 +72,6 @@ def read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file): else: print("More than two streams matched") import pdb; pdb.set_trace() - - # remove complete data from dictionary container - complete_vav = [] - for vav in matched_streams.keys(): - if len(matched_streams[vav]) == 3: - complete_vav.append(matched_streams[vav]) # retreive full column names based on index of column full_col_names_matched_streams = deepcopy(matched_streams) @@ -89,126 +85,76 @@ def read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file): matched_streams_summary = matched_streams_summary.sort_values(by=list(col_labels.keys())) matched_streams_summary.to_csv(join(dat_folder, 'matched_streams_summary.csv'), index_label='vav_label') - tags = ['Equip Fail', 'Scan Off'] - - ## Alternative way to match columns - - # # figure out which file has the most columns - # max_label = (0, '') - # for key in col_labels: - # col_len = len(col_labels[key]) - # if col_len > max_label[0]: - # max_label = (col_len, key) - - # matched_streams = [] - # other_streams = [key for key in col_labels.keys() if key not in max_label[1]] - # for i, col in enumerate(col_labels[max_label[1]]): - # cur_vav_unit = [(max_label[1], col, i)] - # for key in other_streams: - # if col in col_labels[key]: - # match_stream = [(vav, j) for j, vav in enumerate(col_labels[key]) if col == vav] - # if len(match_stream) == 1: - # cur_vav_unit.append((key, match_stream[0][0], match_stream[0][1])) - # else: - # print("More than two streams matched") - # import pdb; pdb.set_trace() - # matched_streams.append(cur_vav_unit) - - # # determine which streams have completed data - # complete_vav = [] - # for vav in matched_streams: - # if len(vav) == 3: - # complete_vav.append(vav) - # for stream in vav: - # col_labels[stream[0]].remove(stream[1]) - - - - + # remove complete data from dictionary container + complete_vav = [] + for vav in full_col_names_matched_streams.keys(): + if len(full_col_names_matched_streams[vav]) == len(dat_streams_list.keys()): + complete_vav.append(full_col_names_matched_streams[vav]) + ####### + ## Combine each vavs data streams + ####### + vavs = {} + for i, vav in matched_streams_summary.iterrows(): + vav_name = vav.name + vlv_dat = pd.DataFrame() + for stream in vav.keys(): + # subset specific stream + cur_col = vav[stream] + if pd.isna(cur_col): + continue + cur_df = dat_stream_map[stream].loc[:, cur_col] + # make sure it is numeric + cur_df = pd.to_numeric(cur_df, errors='coerce') + cur_df = cur_df.to_frame(name=stream) + + # add it to the container + vlv_dat = vlv_dat.merge(cur_df, how='outer', left_index=True, right_index=True) + + # add vav data to container + vavs[vav_name] = { + 'vlv_dat': vlv_dat, + 'row': { + 'vlv': 'vlv_' + vav_name, + 'site': 'bldg_gt_pr', + 'equip': vav_name, + 'upstream_type': None, + } + } - dfs = [] - for csv_file in csv_list: - csv = pd.read_csv(csv_file, index_col=0, parse_dates=True) - - # verify that columns are numeric - for col in csv.columns: - csv.loc[:, col] = pd.to_numeric(csv.loc[:, col], errors="coerce") - - dfs.append(csv) - - df_merge = pd.concat(dfs) + return vavs - # aggregate any repeated timestamps - df_merge = df_merge.sort_index() - df_merge = df_merge.groupby(level=0).mean() - return df_merge +def merge_down_up_stream_dat(vavs, ahu_file, matched_ahu_vav_file): + ahu_df = pd.read_csv(ahu_file, index_col=0, parse_dates=True) + matched_df = pd.read_csv(matched_ahu_vav_file, index_col=0, parse_dates=True) -def clean_df(df): - """ - prepare data for the algorithm - """ - vav_datastream_labels = df.columns[4:] + update_stream = 'upstream_ta' - vav_equip = [lab.split("/")[0] for lab in vav_datastream_labels.values] - stream_type = [lab.split("/")[1] for lab in vav_datastream_labels.values] + for i, vav in matched_df.iterrows(): + vav_name = vav.name + upstream_ta = vav[update_stream] + if pd.isna(upstream_ta): + continue + - # map to standard names in the app - com_stream_map = { - 'upstream_ta': 'ahu-3/sa_temp', - 'oat': 'OA Temp' - } + upstream_ta_df = ahu_df.loc[:, upstream_ta + '.SA.T'] + upstream_ta_df = pd.to_numeric(upstream_ta_df, errors='coerce') + upstream_ta_df = upstream_ta_df.to_frame(name=update_stream) - ind_stream_map = { - 'dnstream_ta': 'da_temp', - 'vlv_po': 'hw_valve', - 'air_flow': 'flow_tn', - 'zone_temp': 'zone_temp' - } + # get timestamp interval from existing streams + vlv_dat = vavs[vav_name]['vlv_dat'] - vavs = {} - for vav in np.unique(vav_equip): - common_keys = list(com_stream_map.keys()) - vlv_dat = df.loc[:, com_stream_map[common_keys[0]]] - vlv_dat.name = common_keys[0] - - if len(common_keys) > 1: - for com_pt in common_keys[1:]: - vlv_dat = pd.concat([vlv_dat, df.loc[:, com_stream_map[com_pt]]], axis=1) - vlv_dat = vlv_dat.rename(columns={com_stream_map[com_pt]: com_pt}) - - for stream in ind_stream_map.keys(): - vav_stream = '/'.join([vav, ind_stream_map[stream]]) - if vav_stream in vav_datastream_labels.values: - new_stream = df.loc[:, vav_stream] - - vlv_dat = pd.concat([vlv_dat, new_stream], axis=1) - vlv_dat = vlv_dat.rename(columns={vav_stream: stream}) - - # verify that all necessary data points are available - stream_avail = [col in vlv_dat.columns for col in ['upstream_ta', 'dnstream_ta', 'vlv_po']] - - if not all(stream_avail): - print(f"VAV={vav} does not have all required data streams\n") - print(f"Missing {3 - np.count_nonzero(stream_avail)} streams, please check.\n") - continue + vlv_dat = vlv_dat.merge(upstream_ta_df, how='outer', left_index=True, right_index=True) + vlv_dat = vlv_dat.resample('15min').mean() - # save in a dictionary - vavs[vav] = { - 'vlv_dat': vlv_dat, - 'row': { - 'vlv': 'vlv_' + vav, - 'site': 'bldg_trc_rs', - 'equip': vav, - 'upstream_type': None, - } - } + vavs[vav_name]['vlv_dat'] = vlv_dat return vavs + def calc_add_features(vav_df, drop_na=False): """ Calculate additional features needed for application @@ -251,13 +197,17 @@ def exclude_time_interval(df, int_str, int_end): if __name__ == '__main__': - dat_folder = join('./', 'external_data', 'bldg_gt_pr') + dat_folder = join('./', 'external_data', 'bldg_gt_pr', '20211118') project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_off_period') # read files - discharge_temp_file = join(dat_folder, 'B44-B45 Discharge Air Temp Sensor Readings - 01MAY2021 to 04NOV2021.csv') - airflow_rate_file = join(dat_folder, 'INC5088495 AIR VOL.csv') + discharge_temp_file = join(dat_folder, 'B44-B45 Discharge Air Temp Sensor Readings - 01MAY2021 to 10NOV2021.csv') + airflow_rate_file = join(dat_folder, 'B44-B45 Air Volume Sensor Readings - 01MAY2021 to 10NOV2021.csv') vlv_pos_file = join(dat_folder, 'INC5088495 VLV CMD.csv') + room_temp_file = join(dat_folder, 'B44-B45 Room Temp Sensor Readings - 01MAY2021 to 10NOV2021.csv') + + ahu_file = join(dat_folder, 'zooms-20211111-13.10.16.csv') + matched_ahu_vav_file = join(dat_folder, 'matched_streams_summary_with_ahu.csv') # define container folders good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves @@ -279,32 +229,32 @@ def exclude_time_interval(df, int_str, int_end): "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. + "air_flow_required": True, # boolean indicated is air flow rate data should strictly be used. "good_folder": good_folder, "bad_folder": bad_folder, "air_flow_folder": air_flow_folder, "csv_folder": csv_folder, } - df = read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file) - - vavs_df = clean_df(df) + vavs_df = read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file, room_temp_file, dat_folder) + vavs_df = merge_down_up_stream_dat(vavs_df, ahu_file, matched_ahu_vav_file) results = [] for key in vavs_df.keys(): - vavs_df[key]['vlv_dat'] = calc_add_features(vavs_df[key]['vlv_dat']) + cur_vlv_df = vavs_df[key]['vlv_dat'] + required_streams = [stream in cur_vlv_df.columns for stream in ['dnstream_ta', 'upstream_ta', 'vlv_po']] + if not all(required_streams): + print("Skipping VAV = {} because all required streams are not available".format(key)) + continue + + vavs_df[key]['vlv_dat'] = calc_add_features(cur_vlv_df) vlv_df = vavs_df[key]['vlv_dat'] row = vavs_df[key]['row'] - # remove data when heat system was off - off_str = '08/31/2021' - off_end = '10/07/2021' - - vlv_df = exclude_time_interval(vlv_df, off_str, off_end) - # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=5, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=15, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) From 360d79d3ee8d95d790a132f0b2f10a064c70a25b Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 30 Nov 2021 09:47:17 -0800 Subject: [PATCH 51/83] added new data file --- detect_passing_valves/ext_dat_app_run.py | 3 ++- detect_passing_valves/plot_data.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index 1dcf8c9..03d5d63 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -157,7 +157,7 @@ def exclude_time_interval(df, int_str, int_end): if __name__ == '__main__': # NOTE: HW plant off from August 31 to October 7 2021 dat_folder = join('./', 'external_data', 'bldg_trc_rs') - project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov9_no_off_period') + project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov22_no_off_period') csv_list = [ join(dat_folder, 'zone trends, September 2021.csv'), @@ -165,6 +165,7 @@ def exclude_time_interval(df, int_str, int_end): join(dat_folder, 'Schoellkopf zone trends 20211103_A.csv'), join(dat_folder, 'Schoellkopf zone trends 20211103_B.csv'), join(dat_folder, 'Schoellkopf zone trends 20211109.csv'), + join(dat_folder, 'Schoellkopf zone trends Nov2021 downloaded 20211122.csv') ] diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index 564a69e..19ccab3 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -201,7 +201,7 @@ def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): if __name__ == '__main__': # define data sources - project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov9_no_off_period') + project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov22_no_off_period') vlv_dat_folder = join(project_folder, "csv_data") # fault data plots From 7efdcde9b76b60538e2a7d479b82ba574c1c7ce9 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 9 Dec 2021 10:09:34 -0800 Subject: [PATCH 52/83] added parameter to estimate accuarcy of airflow measurement --- detect_passing_valves/app.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index c7d4843..44a5b41 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -363,7 +363,7 @@ def _clean_vav(fetch_resp_vav, row): return vav_df -def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=False): +def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=False, af_accu_factor=0.5): """ Drop data rows from dataframe for timeseries that are during unoccupied hours. Uses airflow data if available else it uses building occupancy hours. @@ -378,20 +378,39 @@ def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5, air_flow_require wkend_str: int number indicating start of weekend. 5 indicates Saturday and 6 indicates Sunday + air_flow_required: boolean indicating if airflow rate measurements are required in the analysis + + af_accu_factor: float number indicating the degree of airflow rate measurement accuracy e.g. + a 1 represent highly accurate airflow rate sensor measurement and 0 highly inaccurate measurement. + Default to 0.5. + Returns ------- df: Pandas dataframe with data values during building occupancy hours """ + if 'air_flow' in df.columns: # drop values where there is no air flow xs, ys = density_data(df['air_flow'], rescale_dat=df['temp_diff']) min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) + max_idx = return_extreme_points(ys, type_of_extreme='max', sort=False) if min_idx is not None: - min_air_flow = xs[min_idx[0]] + low_air_flow = xs[min_idx[0]] + else: + low_air_flow = np.percentile(xs, 5) + + if max_idx is not None: + zero_air_flow = xs[max_idx[0]] + else: + zero_air_flow = 0 + + # take into account the accuracy of the airflow rate measurement if known + if af_accu_factor is not None: + min_air_flow = (1-af_accu_factor)*(low_air_flow-zero_air_flow) + zero_air_flow else: - min_air_flow = np.percentile(xs, 5) + min_air_flow = low_air_flow df = df.loc[df['air_flow'] > min_air_flow] elif 'air_flow' not in df.columns and air_flow_required: From ef27b9db1f1ee1919b68b5c45242f079698f69dd Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 11 Feb 2022 11:28:36 -0800 Subject: [PATCH 53/83] updated to latest brick syntax --- detect_passing_valves/app.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 44a5b41..b33165e 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -35,20 +35,20 @@ def _query_and_qualify(): vav_query = """SELECT * WHERE { ?equip rdf:type/rdfs:subClassOf? brick:VAV . - ?equip bf:isFedBy+ ?ahu . + ?equip brick:isFedBy+ ?ahu . ?vlv rdf:type ?vlv_type . - ?ahu bf:hasPoint ?upstream_ta . - ?equip bf:hasPoint ?dnstream_ta . + ?ahu brick:hasPoint ?upstream_ta . + ?equip brick:hasPoint ?dnstream_ta . ?upstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . ?dnstream_ta rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?equip bf:hasPoint ?vlv . + ?equip brick:hasPoint ?vlv . ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . };""" fan_query = """SELECT * WHERE { ?equip rdf:type/rdfs:subClassOf? brick:VAV . - ?equip bf:hasPoint ?air_flow . + ?equip brick:hasPoint ?air_flow . ?air_flow rdf:type/rdfs:subClassOf* brick:Supply_Air_Flow_Sensor . };""" @@ -57,10 +57,10 @@ def _query_and_qualify(): WHERE { ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . ?vlv rdf:type ?vlv_type . - ?equip bf:hasPoint ?vlv . + ?equip brick:hasPoint ?vlv . ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Supply_Air_Temperature_Sensor . - ?equip bf:hasPoint ?air_temps . + ?equip brick:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" @@ -68,10 +68,10 @@ def _query_and_qualify(): WHERE { ?vlv rdf:type/rdfs:subClassOf* brick:Valve_Command . ?vlv rdf:type ?vlv_type . - ?equip bf:hasPoint ?vlv . + ?equip brick:hasPoint ?vlv . ?equip rdf:type/rdfs:subClassOf* brick:Air_Handling_Unit . ?air_temps rdf:type/rdfs:subClassOf* brick:Return_Air_Temperature_Sensor . - ?equip bf:hasPoint ?air_temps . + ?equip brick:hasPoint ?air_temps . ?air_temps rdf:type ?temp_type . };""" From 48cf875b02f7ac73ba0b215a307d322ea033bb40 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 11 Feb 2022 12:02:56 -0800 Subject: [PATCH 54/83] refactored code to avoid duplicates --- detect_passing_valves/app.py | 45 ++++++++++++++------- detect_passing_valves/ext_dat_app_run.py | 21 ---------- detect_passing_valves/ext_dat_app_run_gt.py | 22 ---------- 3 files changed, 30 insertions(+), 58 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index b33165e..7739c4e 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -348,6 +348,28 @@ def _clean_vav(fetch_resp_vav, row): vav_df = pd.concat([ahu_sa, vav_sa, vlv_po], axis=1) vav_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] + return vav_df + + +def calc_add_features(vav_df, drop_na=False, drop_neg_diff=False): + """ + Calculate additional features needed to run the analysis + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + drop_na: Boolean to indicate if NAs in dataframe need to drop + + drop_neg_diff: Boolean to indicate if negative difference between + downstream and upstream need to be dropped + + Returns + ------- + vav_df: Pandas dataframe with valve timeseries data with additional + features + """ + # identify when valve is open vav_df['vlv_open'] = vav_df['vlv_po'] > 0 @@ -355,10 +377,12 @@ def _clean_vav(fetch_resp_vav, row): vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] # drop na - # vav_df = vav_df.dropna() + if drop_na: + vav_df = vav_df.dropna() - # drop values where vav supply air is less than ahu supply air - vav_df = vav_df[vav_df['temp_diff'] >= 0] + # drop values where vav supply air temperature is less than ahu supply air + if drop_neg_diff: + vav_df = vav_df[vav_df['temp_diff'] >= 0] return vav_df @@ -524,18 +548,6 @@ def _clean_ahu(fetch_resp_ahu, row): ahu_df = pd.concat([upstream, dnstream, vlv_po], axis=1) ahu_df.columns = ['upstream_ta', 'dnstream_ta', 'vlv_po'] - # identify when valve is open - ahu_df['vlv_open'] = ahu_df['vlv_po'] > 0 - - # calculate temperature difference between downstream and upstream air - ahu_df['temp_diff'] = ahu_df['dnstream_ta'] - ahu_df['upstream_ta'] - - # drop na - # ahu_df = ahu_df.dropna() - - # drop values where vav supply air is less than ahu supply air - #ahu_df = ahu_df[ahu_df['temp_diff'] >= 0] - return ahu_df @@ -1304,6 +1316,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde # if row['equip'] in ['VAVRM2323']: # import pdb; pdb.set_trace() + # calculate additional parameters for analysis + vlv_df = calc_add_features(vlv_df, drop_na=False) + # check for empty dataframe if vlv_df.empty: print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index 03d5d63..0f437c6 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -113,26 +113,6 @@ def clean_df(df): return vavs -def calc_add_features(vav_df, drop_na=False): - """ - Calculate additional features needed for application - """ - # identify when valve is open - vav_df['vlv_open'] = vav_df['vlv_po'] > 0 - - # calculate temperature difference between downstream and upstream air - vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] - - # drop na - if drop_na: - vav_df = vav_df.dropna() - - # drop values where vav supply air is less than ahu supply air - vav_df = vav_df[vav_df['temp_diff'] >= 0] - - return vav_df - - def CountFrequency(my_list): # Creating an empty dictionary @@ -202,7 +182,6 @@ def exclude_time_interval(df, int_str, int_end): results = [] for key in vavs_df.keys(): - vavs_df[key]['vlv_dat'] = calc_add_features(vavs_df[key]['vlv_dat']) vlv_df = vavs_df[key]['vlv_dat'] row = vavs_df[key]['row'] diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index 9e4a68d..be69aa2 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -154,27 +154,6 @@ def merge_down_up_stream_dat(vavs, ahu_file, matched_ahu_vav_file): return vavs - -def calc_add_features(vav_df, drop_na=False): - """ - Calculate additional features needed for application - """ - # identify when valve is open - vav_df['vlv_open'] = vav_df['vlv_po'] > 0 - - # calculate temperature difference between downstream and upstream air - vav_df['temp_diff'] = vav_df['dnstream_ta'] - vav_df['upstream_ta'] - - # drop na - if drop_na: - vav_df = vav_df.dropna() - - # drop values where vav supply air is less than ahu supply air - vav_df = vav_df[vav_df['temp_diff'] >= 0] - - return vav_df - - def CountFrequency(my_list): # Creating an empty dictionary @@ -247,7 +226,6 @@ def exclude_time_interval(df, int_str, int_end): print("Skipping VAV = {} because all required streams are not available".format(key)) continue - vavs_df[key]['vlv_dat'] = calc_add_features(cur_vlv_df) vlv_df = vavs_df[key]['vlv_dat'] row = vavs_df[key]['row'] From fed94f9fa4c83a1b830e6138a7ee088bc6a0037c Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 15 Feb 2022 09:44:15 -0800 Subject: [PATCH 55/83] added logger outputs --- detect_passing_valves/app.py | 39 +++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 7739c4e..6967f8d 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -10,6 +10,8 @@ from scipy.optimize import curve_fit from scipy.stats import gaussian_kde +log_details = True + def _query_and_qualify(): """ Build query to return control valves, up- and down- stream air temperatures relative to the @@ -698,6 +700,30 @@ def check_folder_exist(folder): if not os.path.exists(folder): os.makedirs(folder) + +def setup_logging(outfolder='./'): + """ + Setup logging if enabled + Parameters + ---------- + log_details: Boolean indicating if detailed logging should be enabled + """ + import logging + # Setup logging + log_file_name = join(outfolder, "vav_app_details.log") + logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) + + global logger + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + + c_handler = logging.StreamHandler(sys.stdout) + f_handler = logging.FileHandler(log_file_name) + + logger.addHandler(c_handler) + logger.addHandler(f_handler) + + def calc_long_t_diff(vlv_df): """ Calculate statistics on difference between down- and up- @@ -1268,10 +1294,11 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): print_passing_mgs(row) pass_type['simple_fail'] = round(long_tc['50%'] - th_bad_vlv, 2) folder = join(project_folder, bad_folder) - import pdb; pdb.set_trace() + vlv_df.loc[:, 'good_oper_cat'] = False else: vlv_df.loc[:, 'good_oper_cat'] = True folder = join(project_folder, good_folder) + if log_details: logger.info("[{}] is only closed with good operation data".format(row['vlv'])) _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=folder) @@ -1303,6 +1330,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde ------- None """ + if log_details: + setup_logging(outfolder=project_folder) # update variables if detection_params is not None: @@ -1316,17 +1345,17 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde # if row['equip'] in ['VAVRM2323']: # import pdb; pdb.set_trace() + # calculate additional parameters for analysis vlv_df = calc_add_features(vlv_df, drop_na=False) # check for empty dataframe if vlv_df.empty: - print("'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site'])) + message = "'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site']) + print(message) + if log_details: logger.info(message) return passing_type - if 'air_flow' in vlv_df.columns: - # plot temp diff vs air flow - _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots')) # Analyze timestamps and valve operation changes vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row, project_folder=project_folder) From 0e73996ed5c889ff19999260b580bae64551e8fd Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 15 Feb 2022 09:46:04 -0800 Subject: [PATCH 56/83] fixed plotting bugs --- detect_passing_valves/app.py | 2 +- detect_passing_valves/plot_data.py | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 6967f8d..bfbc48f 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -810,7 +810,7 @@ def analyze_timestamps(vlv_df, th_time, window, row=None, project_folder='./'): _name = "{}-{}-{}_dat".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(project_folder, csv_folder, _name + '.csv'), idx=0, row=row) - vlv_df.to_csv(full_path) + vlv_df.to_csv(full_path, index_label="Time") # drop rows of data where valve position is unknown vlv_df = vlv_df.dropna(subset=['vlv_po']) diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index 19ccab3..5c59848 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -53,6 +53,11 @@ def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): src = ColumnDataSource(subset_dat) index_name = subset_dat.index.name + if index_name == "" or index_name is None: + index_name = "Time" + subset_dat.index.name = index_name + src = ColumnDataSource(subset_dat) + # make the plot max_date_idx = min(480, len(vlv_dat.index)) p = figure(plot_height=300, plot_width=800, tools='xpan', toolbar_location=None, @@ -133,7 +138,7 @@ def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): # save plot head, tail = os.path.split(csv_path) - plot_name = '{}-timeseries.html'.format(tail.split('.csv')[0]) + plot_name = '{}-ts.html'.format(tail.split('.csv')[0]) output_file(join(fig_folder, plot_name)) save(column(p, select)) @@ -167,7 +172,7 @@ def plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder, time_format="T print('-------Finished processing passing valve plots-----') -def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): +def plot_valve_ts_streams(vlv_dat_folder, valve_plot_files ,sample_size, fig_folder): """ Plot timeseries data for valves that have normal operation """ @@ -176,7 +181,7 @@ def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): # get file paths of csvs all_csv_files = os.listdir(vlv_dat_folder) - good_valve_files = os.listdir(join(vlv_dat_folder, '../', 'good_valves')) + good_valve_files = os.listdir(valve_plot_files) good_valve_files = [tail.split('.png')[0] for tail in good_valve_files] # sample define number of files @@ -201,17 +206,17 @@ def plot_good_valves(vlv_dat_folder, sample_size, fig_folder): if __name__ == '__main__': # define data sources - project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov22_no_off_period') + project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_with_airflow_req') vlv_dat_folder = join(project_folder, "csv_data") # fault data plots - fig_folder_faults = join(project_folder, 'timeseries_valve_faults') + fig_folder_faults = join(project_folder, 'ts_valve_faults') fault_dat_path = join(project_folder, "passing_valve_results.csv") - plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder_faults, time_format="Timestamp('%Y-%m-%d %H:%M:%S')") + plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder_faults, time_format="Timestamp('%Y-%m-%d %H:%M:%S%z', tz='UTC')") # good data plots - fig_folder_good = join(project_folder, 'timeseries_valve_good') - plot_good_valves(vlv_dat_folder, sample_size='all', fig_folder=fig_folder_good) + fig_folder_good = join(project_folder, 'ts_valve_good') + plot_valve_ts_streams(vlv_dat_folder, (join(project_folder, 'good_valves')), sample_size='all', fig_folder=fig_folder_good) From e273d07ab31729d1d96bc7514835da2aa627b9e5 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 15 Feb 2022 09:48:37 -0800 Subject: [PATCH 57/83] verify that sensors are reporting variation in measurements --- detect_passing_valves/app.py | 40 ++++- .../ext_data_app_run_mortar.py | 138 ++++++++++++++++++ detect_passing_valves/fault_tests.py | 38 +++++ 3 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 detect_passing_valves/ext_data_app_run_mortar.py create mode 100644 detect_passing_valves/fault_tests.py diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index bfbc48f..947d88c 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -10,6 +10,8 @@ from scipy.optimize import curve_fit from scipy.stats import gaussian_kde +from fault_tests import fault_sensor_inactivity + log_details = True def _query_and_qualify(): @@ -389,7 +391,7 @@ def calc_add_features(vav_df, drop_na=False, drop_neg_diff=False): return vav_df -def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=False, af_accu_factor=0.5): +def drop_unoccupied_dat(df, row=None, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=False, af_accu_factor=0.5): """ Drop data rows from dataframe for timeseries that are during unoccupied hours. Uses airflow data if available else it uses building occupancy hours. @@ -438,6 +440,10 @@ def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5, air_flow_require else: min_air_flow = low_air_flow + # return calculated results + if row is not None: + row.update({"minimum_air_flow_cutoff": round(min_air_flow,1)}) + df = df.loc[df['air_flow'] > min_air_flow] elif 'air_flow' not in df.columns and air_flow_required: df = pd.DataFrame() @@ -446,7 +452,10 @@ def drop_unoccupied_dat(df, occ_str=6, occ_end=18, wkend_str=5, air_flow_require print("No airflow data, using explicit occupancy hours to do analysis.") df = occupied_hours_subset(df, occ_str, occ_end, wkend_str) - return df + if row is not None: + return df, row + else: + return df def get_vav_flow(fetch_resp_vav, row, fillna=None): @@ -1365,8 +1374,18 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde consecutive timestamps! Skipping...".format(row['vlv'], row['site'])) return passing_type + + # check that sensors are not reporting constant numbers + vlv_df, passing_type = fault_sensor_inactivity(vlv_df, passing_type) + + + if 'air_flow' in vlv_df.columns: + # plot temp diff vs air flow + _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots')) + + # drop data that occurs during unoccupied hours - vlv_df = drop_unoccupied_dat(vlv_df, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=air_flow_required) + vlv_df, row = drop_unoccupied_dat(vlv_df, row=row, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=air_flow_required) if vlv_df.empty: print("'{}' in site {} has no data after hours of \ @@ -1437,13 +1456,15 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde passing_type['leak_grtr_threshold_fail'] = est_leak failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail', 'leak_grtr_threshold_fail'] for x in passing_type.keys()] - if any(failure): + if len([x for x in passing_type.keys() if 'sensor_fault' in x]): + folder = join(project_folder, sensor_fault_folder) + elif any(failure): print_passing_mgs(row) folder = join(project_folder, bad_folder) else: folder = join(project_folder, good_folder) - # categorized good and bad points + # categorized good and bad points vlv_df.loc[:, 'good_oper_cat'] = True if bad_vlv is not None: vlv_df.loc[bad_vlv.index, 'good_oper_cat'] = False @@ -1455,6 +1476,11 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde # grps = list(lal.groups.keys()) # bad_vlv.loc[lal.groups[grps[0]]] + if True: + with open(join(project_folder, 'minimum_airflow_values.txt'), 'a') as f: + f.write(str(row)) + f.write(',\n') + return passing_type def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, project_folder): @@ -1572,11 +1598,13 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th ------- None """ + # declare user hidden parameters global long_term_fail # number of minutes to trigger an long-term passing valve failure global shrt_term_fail # number of minutes to trigger an intermitten passing valve failure global good_folder global bad_folder + global sensor_fault_folder global air_flow_folder global csv_folder @@ -1588,12 +1616,14 @@ def detect_passing_valves(eval_start_time, eval_end_time, window, th_bad_vlv, th # define container folders good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves bad_folder = 'bad_valves' # name of path to the folder to save the plots of the malfunction valves + sensor_fault_folder = 'sensor_fault'# name of path to the folder to save plots of equipment with sensor faults air_flow_folder = 'air_flow_plots' # name of path to the folder to save plots of the air flow values csv_folder = 'csv_data' # name of path to the folder to save detailed valve data # check if holding folders exist check_folder_exist(join(project_folder, bad_folder)) check_folder_exist(join(project_folder, good_folder)) + check_folder_exist(join(project_folder, sensor_fault_folder)) check_folder_exist(join(project_folder, air_flow_folder)) check_folder_exist(join(project_folder, csv_folder)) diff --git a/detect_passing_valves/ext_data_app_run_mortar.py b/detect_passing_valves/ext_data_app_run_mortar.py new file mode 100644 index 0000000..59346c3 --- /dev/null +++ b/detect_passing_valves/ext_data_app_run_mortar.py @@ -0,0 +1,138 @@ +""" +Run the detect passing valve algorithm on external building data from Mortar. +Data was downloaded before if was down. +The data needed to run application are the following: + timestamp + upstream vav air temperature + downstream vav air temperature + vav valve position + vav airflow rate +""" + +import pandas as pd +import numpy as np +import os +import glob +from os.path import join + +from app import _analyze_vlv, check_folder_exist, clean_final_report +from plot_data import * + +def read_files(folder, sample_num=None): + """ + Read individual files that contain all datastream from one VAV + """ + analysis_cols = ['upstream_ta', 'dnstream_ta', 'vlv_po', 'air_flow'] + + vav_files = glob.glob(join(folder, "*.csv")) + + if sample_num is not None: + from random import sample + vav_files = sample(vav_files, sample_num) + + vavs = {} + for vfile in vav_files: + cur_vav_name = os.path.basename(vfile).split(".csv")[0] + + try: + site, vav, vlv = cur_vav_name.split("-", 2) + except ValueError: + import pdb; pdb.set_trace() + + vlv_dat = pd.read_csv(vfile, index_col=0, parse_dates=True) + + # check that the columns are available + df_cols = vlv_dat.columns + avail_cols = [ac for ac in analysis_cols if ac in df_cols] + + vlv_dat = vlv_dat.loc[:, avail_cols] + + # add vav data to container + vavs[cur_vav_name] = { + 'vlv_dat': vlv_dat, + 'row': { + 'vlv': vlv, + 'site': site, + 'equip': vav, + 'upstream_type': None, + } + } + return vavs + + +if __name__ == '__main__': + dat_folder = join('with_airflow_checks_year_start', 'csv_data') + project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req_sensor_fault') + + # define container folders + good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves + bad_folder = 'bad_valves' # name of path to the folder to save the plots of the malfunction valves + sensor_fault_folder = 'sensor_fault'# name of path to the folder to save plots of equipment with sensor faults + air_flow_folder = 'air_flow_plots' # name of path to the folder to save plots of the air flow values + csv_folder = 'csv_data' # name of path to the folder to save detailed valve data + + # check if holding folders exist + check_folder_exist(join(project_folder, bad_folder)) + check_folder_exist(join(project_folder, good_folder)) + check_folder_exist(join(project_folder, sensor_fault_folder)) + check_folder_exist(join(project_folder, air_flow_folder)) + check_folder_exist(join(project_folder, csv_folder)) + + # define user parameters + detection_params = { + "th_bad_vlv": 5, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning + "window": 15, # aggregation window, in minutes, to average the raw measurement data + "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure + "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure + "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. + "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. + "good_folder": good_folder, + "bad_folder": bad_folder, + "sensor_fault_folder": sensor_fault_folder, + "air_flow_folder": air_flow_folder, + "csv_folder": csv_folder, + } + + vavs_df = read_files(dat_folder) + + results = [] + for key in vavs_df.keys(): + cur_vlv_df = vavs_df[key]['vlv_dat'] + required_streams = [stream in cur_vlv_df.columns for stream in ['dnstream_ta', 'upstream_ta', 'vlv_po']] + if not all(required_streams): + print("Skipping VAV = {} because all required streams are not available".format(key)) + continue + + vlv_df = vavs_df[key]['vlv_dat'] + row = vavs_df[key]['row'] + + # define variables + vlv_dat = dict(row) + + # run passing valve detection algorithm + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=15, project_folder=project_folder, detection_params=detection_params) + + # save results + vlv_dat.update(passing_type) + results.append(vlv_dat) + + + # report and plot + # define fault folders + fault_dat_path = join(project_folder, "passing_valve_results.csv") + fig_folder_faults = join(project_folder, "ts_valve_faults") + fig_folder_good = join(project_folder, "ts_valve_good") + post_process_vlv_dat = join(project_folder, "csv_data") + + final_df = pd.DataFrame.from_records(results) + final_df = clean_final_report(final_df, drop_null=False) + final_df.to_csv(fault_dat_path) + + # create timeseries plots of the data + plot_fault_valves(post_process_vlv_dat, fault_dat_path, fig_folder_faults, time_format="Timestamp('%Y-%m-%d %H:%M:%S%z', tz='UTC')") + plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, sensor_fault_folder), sample_size='all', fig_folder=fig_folder_faults) + + # plot good vav operation timeseries + plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, good_folder), sample_size='all', fig_folder=fig_folder_good) + import pdb; pdb.set_trace() \ No newline at end of file diff --git a/detect_passing_valves/fault_tests.py b/detect_passing_valves/fault_tests.py new file mode 100644 index 0000000..1bab87f --- /dev/null +++ b/detect_passing_valves/fault_tests.py @@ -0,0 +1,38 @@ +""" +Functions to analyze faults when detecting passing valves +in hot water hydronic systems. + +@author Carlos Duarte +""" + +def fault_sensor_inactivity(vlv_df, passing_type): + """ + Check that sensors are taking measurements and not + just reporting a constant number + """ + pct_threshold = 1.5 + + analysis_cols = ['upstream_ta', 'dnstream_ta', 'air_flow'] + df_cols = vlv_df.columns + avail_cols = [ac for ac in analysis_cols if ac in df_cols] + + for col in avail_cols: + col_dat = vlv_df.loc[:, col] + col_max = max(col_dat) + col_min = min(col_dat) + + # check if values are constant + if col_max == col_min: + passing_type["sensor_fault_CONSTANT_{}".format(col)] = (col_min, col_max) + return vlv_df, passing_type + + col_dat = (col_dat-col_min)/(col_max-col_min) + col_stats = abs(col_dat.diff(periods=-1).loc[vlv_df['cons_ts']]).describe() + + try: + if round(col_stats["std"]*100,1) < pct_threshold: + passing_type["sensor_fault_{}".format(col)] = [(key, col_stats[key]) for key in col_stats.keys()] + except ValueError: + import pdb; pdb.set_trace() + + return vlv_df, passing_type \ No newline at end of file From b273e45eb535ab98ae3d6e1e00ab68042297d087 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 16 Feb 2022 10:31:00 -0800 Subject: [PATCH 58/83] modification to plots --- detect_passing_valves/app.py | 44 ++++++++++++++++++++---------- detect_passing_valves/plot_data.py | 2 +- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 947d88c..c59e1d5 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -895,7 +895,7 @@ def return_extreme_points(dat, type_of_extreme=None, n_modes=None, sort=True): return idx -def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=None, bad_ratio=None, folder='./'): +def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, long_to=None, df_fit=None, bad_ratio=None, folder='./'): """ Make plot showing the correct and bad operating points of the valve control along with helper annotations e.g. long term average for correct and malfunction operating points when valve is commanded off, model fit, and @@ -934,15 +934,15 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Valve opened [%]') - ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') + ax.set_title("Site = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left') ax.set_ylim((0, np.ceil(y_max*1.05))) - if any(~vlv_df['good_oper_cat']): - ax.scatter(x=vlv_df.loc[~vlv_df['good_oper_cat'], 'vlv_po'], y=vlv_df.loc[~vlv_df['good_oper_cat'], 'temp_diff'], color = bad_oper_color, alpha=1/3, s=10, label='Pred. bad operation') - if any(vlv_df['good_oper_cat']): - ax.scatter(x=vlv_df.loc[vlv_df['good_oper_cat'], 'vlv_po'], y=vlv_df.loc[vlv_df['good_oper_cat'], 'temp_diff'], color = good_oper_color, alpha=1/3, s=10, label='Pred. good operation') + ax.scatter(x=vlv_df.loc[vlv_df['good_oper_cat'], 'vlv_po'], y=vlv_df.loc[vlv_df['good_oper_cat'], 'temp_diff'], color = good_oper_color, alpha=1/3, s=10, label='Pred. no fault operation') + + if any(~vlv_df['good_oper_cat']): + ax.scatter(x=vlv_df.loc[~vlv_df['good_oper_cat'], 'vlv_po'], y=vlv_df.loc[~vlv_df['good_oper_cat'], 'temp_diff'], color = bad_oper_color, alpha=1/3, s=10, label='Pred. fault operation') # if 'color' in vlv_df.columns: # ax.scatter(x=vlv_df['vlv_po'], y=vlv_df['temp_diff'], color = vlv_df['color'], alpha=1/3, s=10) @@ -955,18 +955,22 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, df_fit=N if long_t is not None: # add long-term temperature diff - ax.axhline(y=long_t, color='#00b3b3', label='Est. Td (closed valve-good)') + ax.axhline(y=long_t, color='#00b3b3', linestyle = ':', label='Median Tdiff (closed valve-no fault)') + + if long_to is not None: + # add long-term temperature diff when valve is only open + ax.axhline(y=long_to, color='#00b3b3', linestyle = ':', label='Median Tdiff (open valve-no fault)') if long_tbad is not None: - ax.axhline(y=long_tbad, color='#ff8cc6', label='Est. Td (closed valve-bad)') + ax.axhline(y=long_tbad, color='#ff8cc6', linestyle = '-.', label='Median Tdiff (closed valve-fault)') if bad_ratio is not None: # add ratio where presumably passing valve - ax.text(.2, 0.95*y_max, "Bad operation ratio={:.1f}%".format(bad_ratio)) + ax.text(.25, 0.98*y_max, "Fault operation proportion={:.1f}%".format(bad_ratio)) # legend # ax.legend(fontsize=8, markerscale=1, borderaxespad=0., ncol=2, loc='upper right', bbox_to_anchor=(0.15, 1.05, 1., .102)) - ax.legend(fontsize=6, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.55, 1.02), loc='lower left') + ax.legend(fontsize=6, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.48, 1.02), loc='lower left') plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) @@ -1023,7 +1027,7 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Air flow [cfm]') - ax.set_title("Valve = {}\nEquip. = {}".format(row['vlv'], row['equip']), loc='left') + ax.set_title("Valve = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left') if any(~vlv_df['vlv_open']): ax.scatter(x=vlv_df.loc[~vlv_df['vlv_open'], 'air_flow'], y=vlv_df.loc[~vlv_df['vlv_open'], 'temp_diff'], color = closed_vlv_color, alpha=1/3, s=10, label='Closed valve') @@ -1269,13 +1273,18 @@ def analyze_only_open(vlv_df, row, th_bad_vlv, project_folder): pass_type['non_responsive_fail'] = round(long_to['50%'] - th_bad_vlv, 2) folder = join(project_folder, bad_folder) vlv_df.loc[:, 'good_oper_cat'] = False + long_t = None + long_tbad = long_to['50%'] else: vlv_df.loc[:, 'good_oper_cat'] = True folder = join(project_folder, good_folder) + long_t = long_to['50%'] + long_tbad = None - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_to['50%'], folder=folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_to=long_t, long_tbad=long_tbad, folder=folder) + row.update({'long_to': round(long_to['50%'], 2), 'folder': folder}) - return pass_type + return pass_type, row def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): """ @@ -1304,14 +1313,19 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): pass_type['simple_fail'] = round(long_tc['50%'] - th_bad_vlv, 2) folder = join(project_folder, bad_folder) vlv_df.loc[:, 'good_oper_cat'] = False + long_t = None + long_tbad = long_tc['50%'] else: vlv_df.loc[:, 'good_oper_cat'] = True folder = join(project_folder, good_folder) + long_t = long_tc['50%'] + long_tbad = None if log_details: logger.info("[{}] is only closed with good operation data".format(row['vlv'])) - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], folder=folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_t, long_tbad=long_tbad, folder=folder) + row.update({'long_t': round(long_tc['50%'], 2), 'folder': folder}) - return pass_type + return pass_type, row def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folder='./', detection_params=None): """ diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index 5c59848..aef2b36 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -59,7 +59,7 @@ def plot_valve_data(csv_path, fault_dates=None, fig_folder='./'): src = ColumnDataSource(subset_dat) # make the plot - max_date_idx = min(480, len(vlv_dat.index)) + max_date_idx = min(480, len(vlv_dat.index)-1) p = figure(plot_height=300, plot_width=800, tools='xpan', toolbar_location=None, x_axis_type='datetime', x_axis_location='above', x_range=(vlv_dat.index[0], vlv_dat.index[max_date_idx]), From 8b4e2fbd134162deb2bc15bbdce5e4e0a7b28381 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 16 Feb 2022 10:32:23 -0800 Subject: [PATCH 59/83] modification to output of report --- detect_passing_valves/app.py | 47 +++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index c59e1d5..5e9db80 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1353,6 +1353,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde ------- None """ + log_rows_info = True if log_details: setup_logging(outfolder=project_folder) @@ -1377,6 +1378,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde message = "'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site']) print(message) if log_details: logger.info(message) + row.update({'output_details': 'no data after calc features'}) + if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type @@ -1386,6 +1389,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde if vlv_df is None: print("'{}' in site {} has no data after analyzing \ consecutive timestamps! Skipping...".format(row['vlv'], row['site'])) + row.update({'output_details': 'no data after timestamp analysis'}) + if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type @@ -1404,6 +1409,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde if vlv_df.empty: print("'{}' in site {} has no data after hours of \ occupancy check! Skipping...".format(row['vlv'], row['site'])) + row.update({'output_details': 'no data after occupancy check'}) + if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type # determine if valve datastream has open and closed data @@ -1412,11 +1419,13 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde if len(bool_type) < 2: if bool_type[0]: # only open valve data - passing_type = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) + passing_type, row = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) + row.update({'output_details': 'only open valve data'}) else: # only closed valve data - passing_type = analyze_only_close(vlv_df, row, th_bad_vlv, project_folder) - + passing_type, row = analyze_only_close(vlv_df, row, th_bad_vlv, project_folder) + row.update({'output_details': 'only closed valve data'}) + if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type # TODO: Figure out what to do if long_tc is None! @@ -1425,8 +1434,10 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde long_to = vlv_df[vlv_df['vlv_open']]['temp_diff'].describe() if long_tc is None and long_to is not None: - pass_type = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) + pass_type, row = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) passing_type.update(pass_type) + row.update({'output_details': 'no consecutive timestamps and steady state conditions when valve is closed'}) + if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type # make simple comparison of long-term closed temp difference and user define threshold @@ -1453,26 +1464,26 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde if bad_vlv is None: bad_ratio = 0 - long_tbad = long_tc['mean'] + long_tbad = None# long_tc['50%'] else: bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) - long_tbad = bad_vlv['temp_diff'].describe()['mean'] + long_tbad = bad_vlv['temp_diff'].describe()['50%'] # estimate size of leak in terms of pct that valve is open if df_fit_nz is not None: est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() - if est_leak > popt[1] and bad_ratio > 5: + if est_leak > popt[1]: passing_type['leak_grtr_xovr_fail'] = est_leak else: if bad_vlv is not None: est_leak = bad_vlv['temp_diff'].mean() - if bad_ratio > 5 and est_leak > th_bad_vlv: + if est_leak > th_bad_vlv: passing_type['leak_grtr_threshold_fail'] = est_leak failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail', 'leak_grtr_threshold_fail'] for x in passing_type.keys()] if len([x for x in passing_type.keys() if 'sensor_fault' in x]): folder = join(project_folder, sensor_fault_folder) - elif any(failure): + elif any(failure) and bad_ratio > 5: print_passing_mgs(row) folder = join(project_folder, bad_folder) else: @@ -1483,20 +1494,28 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde if bad_vlv is not None: vlv_df.loc[bad_vlv.index, 'good_oper_cat'] = False - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['25%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + row.update({'long_t': round(long_tc['50%'], 2), 'long_tbad': None if long_tbad is None else round(long_tbad, 2), 'bad_ratio': round(bad_ratio, 2), 'folder': folder}) # TODO get a detailed report of the when valve is malfunctioning # lal = bad_vlv.groupby('same') # grps = list(lal.groups.keys()) # bad_vlv.loc[lal.groups[grps[0]]] - if True: - with open(join(project_folder, 'minimum_airflow_values.txt'), 'a') as f: - f.write(str(row)) - f.write(',\n') + if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type + +def log_row_details(row, outfolder): + """ + Log information out to external file + """ + with open(outfolder, 'a') as f: + f.write(str(row)) + f.write(',\n') + + def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, project_folder): """ Helper function to analyze AHU valves From aa8b719fea69033310d6280e6b3764c60378c49c Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 16 Feb 2022 10:34:37 -0800 Subject: [PATCH 60/83] added analysis to datasets --- detect_passing_valves/app.py | 5 +- .../ext_data_app_run_mortar.py | 59 ++++++++++++++++++- detect_passing_valves/fault_tests.py | 43 +++++++++++++- detect_passing_valves/plot_data.py | 2 +- 4 files changed, 102 insertions(+), 7 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 5e9db80..860851c 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -10,7 +10,7 @@ from scipy.optimize import curve_fit from scipy.stats import gaussian_kde -from fault_tests import fault_sensor_inactivity +from fault_tests import fault_sensor_inactivity, fault_sensor_out_of_range log_details = True @@ -1397,6 +1397,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde # check that sensors are not reporting constant numbers vlv_df, passing_type = fault_sensor_inactivity(vlv_df, passing_type) + # check that sensors are within range + vlv_df, passing_type = fault_sensor_out_of_range(vlv_df, passing_type) + if 'air_flow' in vlv_df.columns: # plot temp diff vs air flow diff --git a/detect_passing_valves/ext_data_app_run_mortar.py b/detect_passing_valves/ext_data_app_run_mortar.py index 59346c3..983abad 100644 --- a/detect_passing_valves/ext_data_app_run_mortar.py +++ b/detect_passing_valves/ext_data_app_run_mortar.py @@ -10,6 +10,7 @@ """ import pandas as pd +import pickle import numpy as np import os import glob @@ -32,7 +33,7 @@ def read_files(folder, sample_num=None): vavs = {} for vfile in vav_files: - cur_vav_name = os.path.basename(vfile).split(".csv")[0] + cur_vav_name = os.path.basename(vfile).split(".csv")[0].replace('_dat', '') try: site, vav, vlv = cur_vav_name.split("-", 2) @@ -60,9 +61,19 @@ def read_files(folder, sample_num=None): return vavs +def parse_dict_list_file(line): + + dictionary = dict() + pairs = line.strip().strip(",").strip('{}').split(', ') + for pr in pairs: + pair = pr.split(': ') + dictionary[pair[0].strip('\'\'\"\"')] = pair[1].strip('\'\'\"\"') + + return dictionary + if __name__ == '__main__': dat_folder = join('with_airflow_checks_year_start', 'csv_data') - project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req_sensor_fault') + project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req') # define container folders good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves @@ -80,7 +91,7 @@ def read_files(folder, sample_num=None): # define user parameters detection_params = { - "th_bad_vlv": 5, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_bad_vlv": 10, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning "window": 15, # aggregation window, in minutes, to average the raw measurement data "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure @@ -97,6 +108,7 @@ def read_files(folder, sample_num=None): vavs_df = read_files(dat_folder) results = [] + vav_count_summary = [] for key in vavs_df.keys(): cur_vlv_df = vavs_df[key]['vlv_dat'] required_streams = [stream in cur_vlv_df.columns for stream in ['dnstream_ta', 'upstream_ta', 'vlv_po']] @@ -106,6 +118,7 @@ def read_files(folder, sample_num=None): vlv_df = vavs_df[key]['vlv_dat'] row = vavs_df[key]['row'] + vav_count_summary.append({'site': row['site'], 'equip': row['equip']}) # define variables vlv_dat = dict(row) @@ -124,15 +137,55 @@ def read_files(folder, sample_num=None): fig_folder_faults = join(project_folder, "ts_valve_faults") fig_folder_good = join(project_folder, "ts_valve_good") post_process_vlv_dat = join(project_folder, "csv_data") + vav_count_file = join(project_folder, 'vav_count_summary.csv') + raw_analyzed_data = join(project_folder, 'raw_analyzed_data.pkl') + raw_analyzed_results = join(project_folder, 'raw_analyzed_results.pkl') final_df = pd.DataFrame.from_records(results) final_df = clean_final_report(final_df, drop_null=False) final_df.to_csv(fault_dat_path) + vav_count_summary = pd.DataFrame.from_records(vav_count_summary) + vav_count_summary.to_csv(vav_count_file) + + raw_df = open(raw_analyzed_data, "wb") + pickle.dump(vavs_df, raw_df) + raw_df.close() + + raw_result = open(raw_analyzed_results, "wb") + pickle.dump(results, raw_result) + raw_result.close() + + # raw_df = open(raw_analyzed_data, "rb") + # object_file = pickle.load(raw_df) + # raw_df.close() + # create timeseries plots of the data plot_fault_valves(post_process_vlv_dat, fault_dat_path, fig_folder_faults, time_format="Timestamp('%Y-%m-%d %H:%M:%S%z', tz='UTC')") plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, sensor_fault_folder), sample_size='all', fig_folder=fig_folder_faults) # plot good vav operation timeseries plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, good_folder), sample_size='all', fig_folder=fig_folder_good) + import pdb; pdb.set_trace() + + # Perform additional analysis + f = open(join(project_folder, 'minimum_airflow_values.txt'), 'r') + lines = f.readlines() + f.close() + + vav_results = [] + for line in lines: + vav_results.append(parse_dict_list_file(line)) + + vav_results = pd.DataFrame.from_records(vav_results) + + numeric_cols = ['minimum_air_flow_cutoff', 'long_t', 'long_tbad', 'bad_ratio', 'long_to'] + vav_results[numeric_cols] = vav_results[numeric_cols].apply(pd.to_numeric, errors='coerce') + vav_results['folder_short'] = vav_results['folder'].apply(os.path.basename) + + # summary statistics for each site + vav_results_grp = vav_results.groupby(['site', 'folder_short']) + vav_results_grp['long_t'].describe() + + import pdb; pdb.set_trace() \ No newline at end of file diff --git a/detect_passing_valves/fault_tests.py b/detect_passing_valves/fault_tests.py index 1bab87f..28b751e 100644 --- a/detect_passing_valves/fault_tests.py +++ b/detect_passing_valves/fault_tests.py @@ -31,8 +31,47 @@ def fault_sensor_inactivity(vlv_df, passing_type): try: if round(col_stats["std"]*100,1) < pct_threshold: - passing_type["sensor_fault_{}".format(col)] = [(key, col_stats[key]) for key in col_stats.keys()] + passing_type["sensor_fault_NORANGE_{}".format(col)] = [(key, col_stats[key]) for key in col_stats.keys()] except ValueError: import pdb; pdb.set_trace() - return vlv_df, passing_type \ No newline at end of file + return vlv_df, passing_type + + +def fault_sensor_out_of_range(vlv_df, passing_type): + """ + Check that sensors are in the right value range + """ + temp_max = 200 + temp_min = 20 + air_max = 2500 + air_min = -50 + pos_max = 100 + pos_min = 0 + + analysis_cols = { + 'upstream_ta': [temp_max, temp_min], + 'dnstream_ta': [temp_max, temp_min], + 'air_flow': [air_max, air_min], + 'vlv_po': [pos_max, pos_min], + } + + df_cols = vlv_df.columns + for col in analysis_cols: + if col in df_cols: + col_dat = vlv_df.loc[:, col] + val_max = analysis_cols[col][0] + val_min = analysis_cols[col][1] + + vals_too_hi = sum(col_dat > val_max)/len(col_dat) + vals_too_lo = sum(col_dat < val_min)/len(col_dat) + + if vals_too_hi > 0.05: + passing_type["sensor_fault_HIVAL_{}".format(col)] = (vals_too_hi) + + if vals_too_lo > 0.05: + passing_type["sensor_fault_LOVAL_{}".format(col)] = (vals_too_lo) + + return vlv_df, passing_type + + diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index aef2b36..3a2f53d 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -206,7 +206,7 @@ def plot_valve_ts_streams(vlv_dat_folder, valve_plot_files ,sample_size, fig_fol if __name__ == '__main__': # define data sources - project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_with_airflow_req') + project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req_sensor_fault') vlv_dat_folder = join(project_folder, "csv_data") # fault data plots From 13097c51a3f90b396b96bba63181b0ffb591fbea Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 18 Feb 2022 14:00:25 -0800 Subject: [PATCH 61/83] added variables in the find bad vlv operation function --- detect_passing_valves/app.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 860851c..0a0816f 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -622,7 +622,7 @@ def rescale_fit(scaled_vals, vals=None, max_val=None, min_val=None): return unscaled_vals -def sigmoid(x, k, x0): +def sigmoid(x, k, x0, y_max=1, y_min=0): """ Sigmoid function curve to do a logistic model @@ -631,14 +631,25 @@ def sigmoid(x, k, x0): x: independent variable k: slope of the sigmoid function x0: midpoint/inflection point of the sigmoid function + y_max: maximum value of data + y_min: minimum value of data Returns ------- y: value of the function at point x """ - return 1.0 / (1 + np.exp(-k * (x - x0))) + return y_min + ((y_max - y_min) / (1 + np.exp(-k * (x - x0)))) -def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff'): + +def make_sigmoid_func(y_max=1, y_min=0): + + def sigmoid(x, k, x0): + return y_min + (y_max - y_min) / (1 + np.exp(-k * (x - x0))) + + return sigmoid + + +def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff', scaled=True): """ Build a logistic model with data provided @@ -1101,7 +1112,7 @@ def density_data(dat, rescale_dat=None): return xs, ys -def find_bad_vlv_operation(vlv_df, model, window): +def find_bad_vlv_operation(vlv_df, row, model, popt, window): """ Determine which timeseries values are data from probable passing valves and return a pandas dataframe of only 'bad' values. @@ -1113,6 +1124,8 @@ def find_bad_vlv_operation(vlv_df, model, window): long_t: long-term temperature difference between down and up air streams when valve is commanded close for correct operation + popt: an array of the optimized parameters, slope and inflection point of the sigmoid function + window : aggregation window, in minutes, to average the raw measurement data Returns @@ -1462,7 +1475,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde df_fit_nz, popt = build_logistic_model(no_zeros_po) # calculate bad valve instances vs overall dataframe - bad_vlv, pass_type = find_bad_vlv_operation(vlv_df, df_fit_nz, window) + bad_vlv, pass_type = find_bad_vlv_operation(vlv_df, row, df_fit_nz, popt, window) passing_type.update(pass_type) if bad_vlv is None: @@ -1486,6 +1499,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail', 'leak_grtr_threshold_fail'] for x in passing_type.keys()] if len([x for x in passing_type.keys() if 'sensor_fault' in x]): folder = join(project_folder, sensor_fault_folder) + elif 'long_term_fail' in passing_type.keys(): + print_passing_mgs(row) + folder = join(project_folder, bad_folder) elif any(failure) and bad_ratio > 5: print_passing_mgs(row) folder = join(project_folder, bad_folder) From 33c5c2d0ffd38c50bb219fb3150a972cecf895cd Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 18 Feb 2022 14:03:14 -0800 Subject: [PATCH 62/83] additional analysis --- detect_passing_valves/app.py | 4 +- detect_passing_valves/ext_dat_app_run.py | 79 +++++++++++++++++++++--- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 0a0816f..72876d1 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -622,7 +622,7 @@ def rescale_fit(scaled_vals, vals=None, max_val=None, min_val=None): return unscaled_vals -def sigmoid(x, k, x0, y_max=1, y_min=0): +def sigmoid(x, k, x0): """ Sigmoid function curve to do a logistic model @@ -638,6 +638,8 @@ def sigmoid(x, k, x0, y_max=1, y_min=0): ------- y: value of the function at point x """ + y_max=1 + y_min=0 return y_min + ((y_max - y_min) / (1 + np.exp(-k * (x - x0)))) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index 0f437c6..46129c3 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -12,13 +12,12 @@ import numpy as np import os import time +import pickle from os.path import join -import matplotlib.pyplot as plt -from scipy.optimize import curve_fit -from scipy.stats import gaussian_kde from app import _analyze_vlv, check_folder_exist, clean_final_report +from plot_data import * # from app import _make_tdiff_vs_aflow_plot, \ # analyze_timestamps, rename_existing, drop_unoccupied_dat, calc_long_t_diff, \ @@ -104,7 +103,7 @@ def clean_df(df): 'vlv_dat': vlv_dat, 'row': { 'vlv': 'vlv_' + vav, - 'site': 'bldg_trc_rs', + 'site': 'bear', 'equip': vav, 'upstream_type': None, } @@ -134,10 +133,20 @@ def exclude_time_interval(df, int_str, int_end): return df.loc[~within_interval, :] +def parse_dict_list_file(line): + + dictionary = dict() + pairs = line.strip().strip(",").strip('{}').split(', ') + for pr in pairs: + pair = pr.split(': ') + dictionary[pair[0].strip('\'\'\"\"')] = pair[1].strip('\'\'\"\"') + + return dictionary + if __name__ == '__main__': # NOTE: HW plant off from August 31 to October 7 2021 dat_folder = join('./', 'external_data', 'bldg_trc_rs') - project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_Nov22_no_off_period') + project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_test_no_aflw_req') csv_list = [ join(dat_folder, 'zone trends, September 2021.csv'), @@ -152,26 +161,29 @@ def exclude_time_interval(df, int_str, int_end): # define container folders good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves bad_folder = 'bad_valves' # name of path to the folder to save the plots of the malfunction valves + sensor_fault_folder = 'sensor_fault'# name of path to the folder to save plots of equipment with sensor faults air_flow_folder = 'air_flow_plots' # name of path to the folder to save plots of the air flow values csv_folder = 'csv_data' # name of path to the folder to save detailed valve data # check if holding folders exist check_folder_exist(join(project_folder, bad_folder)) check_folder_exist(join(project_folder, good_folder)) + check_folder_exist(join(project_folder, sensor_fault_folder)) check_folder_exist(join(project_folder, air_flow_folder)) check_folder_exist(join(project_folder, csv_folder)) # define user parameters detection_params = { - "th_bad_vlv": 5, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_bad_vlv": 10, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning "window": 15, # aggregation window, in minutes, to average the raw measurement data "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. - "air_flow_required": True, # boolean indicated is air flow rate data should strictly be used. + "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. "good_folder": good_folder, "bad_folder": bad_folder, + "sensor_fault_folder": sensor_fault_folder, "air_flow_folder": air_flow_folder, "csv_folder": csv_folder, } @@ -181,9 +193,11 @@ def exclude_time_interval(df, int_str, int_end): vavs_df = clean_df(df) results = [] + vav_count_summary = [] for key in vavs_df.keys(): vlv_df = vavs_df[key]['vlv_dat'] row = vavs_df[key]['row'] + vav_count_summary.append({'site': row['site'], 'equip': row['equip']}) # remove data when heat system was off off_str = '08/31/2021' @@ -200,8 +214,57 @@ def exclude_time_interval(df, int_str, int_end): vlv_dat.update(passing_type) results.append(vlv_dat) + # report and plot + # define fault folders + fault_dat_path = join(project_folder, "passing_valve_results.csv") + fig_folder_faults = join(project_folder, "ts_valve_faults") + fig_folder_good = join(project_folder, "ts_valve_good") + post_process_vlv_dat = join(project_folder, "csv_data") + vav_count_file = join(project_folder, 'vav_count_summary.csv') + raw_analyzed_data = join(project_folder, 'raw_analyzed_data.pkl') + raw_analyzed_results = join(project_folder, 'raw_analyzed_results.pkl') + final_df = pd.DataFrame.from_records(results) final_df = clean_final_report(final_df, drop_null=False) - final_df.to_csv(join(project_folder, "passing_valve_results.csv")) + final_df.to_csv(fault_dat_path) + + vav_count_summary = pd.DataFrame.from_records(vav_count_summary) + vav_count_summary.to_csv(vav_count_file) + + raw_df = open(raw_analyzed_data, "wb") + pickle.dump(vavs_df, raw_df) + raw_df.close() + + raw_result = open(raw_analyzed_results, "wb") + pickle.dump(results, raw_result) + raw_result.close() + + # create timeseries plots of the data + plot_fault_valves(post_process_vlv_dat, fault_dat_path, fig_folder_faults, time_format="Timestamp('%Y-%m-%d %H:%M:%S')") + plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, sensor_fault_folder), sample_size='all', fig_folder=fig_folder_faults) + + # plot good vav operation timeseries + plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, good_folder), sample_size='all', fig_folder=fig_folder_good) + + import pdb; pdb.set_trace() + # Perform additional analysis + f = open(join(project_folder, 'minimum_airflow_values.txt'), 'r') + lines = f.readlines() + f.close() + + vav_results = [] + for line in lines: + vav_results.append(parse_dict_list_file(line)) + + vav_results = pd.DataFrame.from_records(vav_results) + + numeric_cols = ['minimum_air_flow_cutoff', 'long_t', 'long_tbad', 'bad_ratio', 'long_to'] + vav_results[numeric_cols] = vav_results[numeric_cols].apply(pd.to_numeric, errors='coerce') + vav_results['folder_short'] = vav_results['folder'].apply(os.path.basename) + + # summary statistics for each site + vav_results_grp = vav_results.groupby(['site', 'folder_short']) + vav_results_grp['long_t'].describe() + import pdb; pdb.set_trace() \ No newline at end of file From e6a5298dc5eed88569f39bf61d39e0dc8dd7edff Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 22 Feb 2022 11:17:07 -0800 Subject: [PATCH 63/83] changed vlv model to unscaled sigmoid --- detect_passing_valves/app.py | 47 ++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 72876d1..0d69515 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -651,7 +651,7 @@ def sigmoid(x, k, x0): return sigmoid -def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff', scaled=True): +def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff', scaled=False): """ Build a logistic model with data provided @@ -672,25 +672,46 @@ def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff', scaled=True): try: # fit the curve - scaled_pos = scale_0to1(df[x_col]) - scaled_t = scale_0to1(df[y_col]) - popt, pcov = curve_fit(sigmoid, scaled_pos, scaled_t) - - # calculate fitted temp difference values - est_k, est_x0 = popt - popt[1] = rescale_fit(popt[1], df[x_col]) - y_fitted = rescale_fit(sigmoid(scaled_pos, est_k, est_x0), df[y_col]) - y_fitted.name = 'y_fitted' - - # sort values + if scaled: + scaled_pos = scale_0to1(df[x_col]) + scaled_t = scale_0to1(df[y_col]) + sigmoid_func = sigmoid + popt, pcov = curve_fit(sigmoid_func, scaled_pos, scaled_t) + + # calculate fitted temp difference values + est_k, est_x0 = popt + # popt[1] = rescale_fit(popt[1], df[x_col]) + y_fitted = rescale_fit(sigmoid_func(scaled_pos, est_k, est_x0), df[y_col]) + y_fitted.name = 'y_fitted' + else: + x_vals = df[x_col] + y_vals = df[y_col] + y_max = max(y_vals) + y_min = min(y_vals) + sigmoid_func = make_sigmoid_func(y_max, y_min) + + popt, pcov = curve_fit(sigmoid_func, x_vals, y_vals) + est_k, est_x0 = popt + y_fitted = sigmoid_func(x_vals, est_k, est_x0) + y_fitted.name = 'y_fitted' + + # make sure model goes from 0 to 100 percent + x_ext_vals = range(0,105,5) + y_ext_vals = sigmoid_func(x_ext_vals, est_k, est_x0) + df_ext = pd.DataFrame({'vlv_po': x_ext_vals, 'y_fitted': y_ext_vals}) + + # add extra values and sort values df_fit = pd.concat([df[x_col], y_fitted], axis=1) - df_fit = df_fit.sort_values(by=x_col) + df_fit = pd.concat([df_fit, df_ext]) + df_fit = df_fit.sort_values(by=x_col).reset_index(drop=True) + except RuntimeError: print("Model unabled to be developed\n") return None, None return df_fit, popt + def try_limit_dat_fit_model(vlv_df, df_fraction): # calculate fit model nrows, ncols = vlv_df.shape From 92d986138835be9afefcd15bb9eee394afac5f41 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 22 Feb 2022 11:18:00 -0800 Subject: [PATCH 64/83] check if columns are available --- detect_passing_valves/ext_dat_app_run.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index 46129c3..a2a9ea6 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -259,12 +259,13 @@ def parse_dict_list_file(line): vav_results = pd.DataFrame.from_records(vav_results) numeric_cols = ['minimum_air_flow_cutoff', 'long_t', 'long_tbad', 'bad_ratio', 'long_to'] - vav_results[numeric_cols] = vav_results[numeric_cols].apply(pd.to_numeric, errors='coerce') + avail_cols = list(set(vav_results.columns).intersection(set(numeric_cols))) + vav_results[avail_cols] = vav_results[avail_cols].apply(pd.to_numeric, errors='coerce') + vav_results['folder_short'] = vav_results['folder'].apply(os.path.basename) # summary statistics for each site vav_results_grp = vav_results.groupby(['site', 'folder_short']) vav_results_grp['long_t'].describe() - import pdb; pdb.set_trace() \ No newline at end of file From c13adde7f581f7311ed2bc880c868aca7dd39f8f Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 23 Feb 2022 14:16:07 -0800 Subject: [PATCH 65/83] improved the way it detected fault operation --- detect_passing_valves/app.py | 220 ++++++++++++++++------- detect_passing_valves/ext_dat_app_run.py | 3 +- 2 files changed, 161 insertions(+), 62 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 0d69515..3a7bcfb 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -390,6 +390,20 @@ def calc_add_features(vav_df, drop_na=False, drop_neg_diff=False): return vav_df +def determine_timestamp_interval(df): + """ + Determine time interval for dataframe + """ + + ts = pd.Series(df.index) + ts_int = (ts - ts.shift(1)) + + int_freq = ts_int.value_counts() + + window = int_freq.index[0] + + return window + def drop_unoccupied_dat(df, row=None, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=False, af_accu_factor=0.5): """ @@ -642,6 +656,26 @@ def sigmoid(x, k, x0): y_min=0 return y_min + ((y_max - y_min) / (1 + np.exp(-k * (x - x0)))) +def sigmoid_prime(x, k, x0, y_max=1, y_min=0): + """ + derivative of sigmoid function + + Parameters + ---------- + x: independent variable + k: slope of the sigmoid function + x0: midpoint/inflection point of the sigmoid function + y_max: maximum value of data + y_min: minimum value of data + + Returns + ------- + y: value of the function at point x + """ + num = (y_max-y_min)*np.exp(-k * (x - x0)) + den = (1 + np.exp(-k * (x - x0)))**2 + + return num/den def make_sigmoid_func(y_max=1, y_min=0): @@ -793,11 +827,30 @@ def calc_long_t_diff(vlv_df): return long_t -def analyze_timestamps(vlv_df, th_time, window, row=None, project_folder='./'): + +def check_consecutive_timestamps(df, window=None): """ - Analyze timestamps and valve operation in a pandas dataframe to determine which row values - are th_time minutes after a changed state e.g. determine which data corresponds - to steady-state and transient values. + Check if timestamps are consecutive in dataframe or series + Parameters + ---------- + df: Pandas dataframe with valve timeseries data + + Returns + ------- + List: boolean lsit indicating if timestamps are consecutive + """ + if window is None: + window = determine_timestamp_interval(df) + + ts = pd.Series(df.index) + cons_ts = ((ts - ts.shift(-1)).abs() <= window) | (ts.diff() <= window) + + return list(cons_ts) + + +def analyze_timestamps(vlv_df, th_time, window=None): + """ + Analyze timestamps to make sure they are consecutive Parameters ---------- @@ -809,26 +862,21 @@ def analyze_timestamps(vlv_df, th_time, window, row=None, project_folder='./'): window : aggregation window, in minutes, to average the raw measurement data - row: Pandas series object with metadata for the specific vav valve - - project_folder: name of path for the project and used to save the plot. - Returns ------- vav_df: same input pandas dataframe but with added columns indicating: cons_ts: boolean indicating consecutive timestamps cons_ts_vlv_c: boolean indicating consecutive timestamps when valve is commanded closed same: boolean indicating group number of cons_ts_vlv_c - steady: boolean indicating if the timestamp is in steady state condition """ + if window is None: + window = determine_timestamp_interval(vlv_df) - min_ts = int(th_time/window) + (th_time % window > 0) - min_tst = pd.Timedelta(th_time, unit='min') + window_int = window.seconds/60 + min_ts = int(th_time/window_int) + (th_time % window_int > 0) # only get consecutive timestamps datapoints - ts = pd.Series(vlv_df.index) - ts_int = pd.Timedelta(window, unit='min') - cons_ts = ((ts - ts.shift(-1)).abs() <= ts_int) | (ts.diff() <= ts_int) + cons_ts = check_consecutive_timestamps(vlv_df, window) if (len(cons_ts) < min_ts) | ~(np.any(cons_ts)): return None @@ -837,6 +885,34 @@ def analyze_timestamps(vlv_df, th_time, window, row=None, project_folder='./'): vlv_df.loc[:, 'cons_ts_vlv_c'] = np.logical_and(~vlv_df['vlv_open'], vlv_df['cons_ts']) vlv_df.loc[:, 'same'] = vlv_df['cons_ts_vlv_c'].astype(int).diff().ne(0).cumsum() + return vlv_df + + +def analyze_steady_vs_transient(vlv_df, row=None, project_folder='./'): + """ + Analyze valve operation in a pandas dataframe to determine which row values + are th_time minutes after a changed state e.g. determine which data corresponds + to steady-state and transient values. + + Parameters + ---------- + vav_df: Pandas dataframe with valve timeseries data + + row: Pandas series object with metadata for the specific vav valve + + project_folder: name of path for the project and used to save the plot. + + Returns + ------- + vav_df: same input pandas dataframe but with added columns indicating: + steady: boolean indicating if the timestamp is in steady state condition + """ + + if vlv_df is None: + return None + + min_tst = pd.Timedelta(th_time, unit='min') + # subset by consecutive times that exceed th_time lal = vlv_df.groupby('same') @@ -1135,7 +1211,7 @@ def density_data(dat, rescale_dat=None): return xs, ys -def find_bad_vlv_operation(vlv_df, row, model, popt, window): +def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): """ Determine which timeseries values are data from probable passing valves and return a pandas dataframe of only 'bad' values. @@ -1162,18 +1238,37 @@ def find_bad_vlv_operation(vlv_df, row, model, popt, window): (X minutes defined in global parameter 'shrt_term_fail') due to control errors, mechanical/electrical problems, or other. Default 60 minutes (1 hour). """ + PROBABILITY = 0.16 + window_int = window.seconds/60 pass_type = dict() hi_diff = np.percentile(vlv_df.loc[vlv_df['vlv_open'], 'temp_diff'], 95) if model is not None: - vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() - - # define temperature difference and valve position failure thresholds - vlv_po_th = vlv_po_hi_diff/2.0 - diff_vlv_po_th = max(model[model['vlv_po'] <= vlv_po_th]['y_fitted'].max(), 5) + # # analyze sigmoid function + # y_max = max(vlv_df['temp_diff']) + # y_min = min(vlv_df['temp_diff']) + # sigmoid_func = make_sigmoid_func(y_max, y_min) + # max_model_diff = round(sigmoid_func(100, popt[0], popt[1]), 1) + # if max_model_diff > 2*th_bad_vlv: + # x_prime = range(0,101,1) + # y_prime = sigmoid_prime(x_prime, popt[0], popt[1], y_max, y_min) + # y_prime_norm = y_prime/sum(y_prime) + + # vlv_start_open = list(x_prime)[sum(np.cumsum(y_prime_norm) < PROBABILITY)] + # diff_vlv_po_th = round(sigmoid_func(vlv_start_open, popt[0], popt[1]), 1) + # else: + tdiff_model_max = model[model['vlv_po'] == 100]['y_fitted'].mean() + if tdiff_model_max > 2*th_bad_vlv: + vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() + + # define temperature difference and valve position failure thresholds + vlv_po_th = vlv_po_hi_diff/2.0 + diff_vlv_po_th = max(model[model['vlv_po'] <= vlv_po_th]['y_fitted'].max(), th_bad_vlv) + else: + diff_vlv_po_th = max(hi_diff/4.0, th_bad_vlv) else: - diff_vlv_po_th = max(hi_diff/4.0, 5) + diff_vlv_po_th = max(hi_diff/4.0, th_bad_vlv) # find datapoints that exceed long-term temperature difference exceed_long_t = vlv_df['temp_diff'] >= diff_vlv_po_th @@ -1186,39 +1281,39 @@ def find_bad_vlv_operation(vlv_df, row, model, popt, window): if df_bad.empty: return None, dict() + # check that timestamps are consecutive in df_bad + df_bad_cons_ts = check_consecutive_timestamps(df_bad, window) + df_bad['cons_ts_fault_vlv'] = df_bad_cons_ts + # analyze 'bad' dataframe for possible passing valve - bad_grp = df_bad.groupby('same') + #bad_grp = df_bad.groupby('same') + bad_grp = df_bad.loc[df_bad['cons_ts_fault_vlv'], :].groupby(['same', 'cons_ts_fault_vlv']) + + bad_grp = df_bad.groupby(['same', 'cons_ts_fault_vlv']) bad_grp_count = bad_grp['same'].count() - # max_idx = np.argmax(bad_grp_count) - # max_grp = bad_grp.groups[bad_grp_count.index[max_idx]] + idx_const_vals = bad_grp_count.index.get_level_values("cons_ts_fault_vlv").values + if not any(idx_const_vals): + # not bad values with consecutive timestamps found + return df_bad, pass_type - # if len(max_grp) > 1: - # max_passing_time = max_grp[-1] - max_grp[0] - # else: - # max_passing_time = pd.Timedelta(0, unit='min') - - # # detect long term failures - # if max_passing_time > pd.Timedelta(long_term_fail, unit='min'): - # ts_seconds = max_passing_time.seconds - # ts_days = max_passing_time.days * 3600 * 24 - # pass_type['long_term_fail'] = (ts_days+ts_seconds)/60.0 + bad_grp_count_cons_ts = bad_grp_count[(slice(None), True)] # detect long term failures - long_term_fail_bool = (bad_grp_count*window) > long_term_fail + long_term_fail_bool = (bad_grp_count_cons_ts*window_int) > long_term_fail if any(long_term_fail_bool): - long_term_fail_times = bad_grp_count[long_term_fail_bool]*window + long_term_fail_times = bad_grp_count_cons_ts[long_term_fail_bool]*window_int if long_term_fail_times.count() >= 1 or long_term_fail_times.index[-1] == vlv_df['same'].max(): - dates = [(bad_grp.groups[ky][0], bad_grp.groups[ky][-1]) for ky in long_term_fail_times.index] + dates = [(bad_grp.groups[(ky, True)][0], bad_grp.groups[(ky, True)][-1]) for ky in long_term_fail_times.index.values] pass_type['long_term_fail'] = (long_term_fail_times.mean(), long_term_fail_times.count(), dates) # detect short term failures - bad_grp_left_over = bad_grp_count[~long_term_fail_bool] - short_term_fail_bool = (bad_grp_left_over*window) > shrt_term_fail + bad_grp_left_over = bad_grp_count_cons_ts[~long_term_fail_bool] + short_term_fail_bool = (bad_grp_left_over*window_int) > shrt_term_fail if any(short_term_fail_bool): - shrt_term_fail_times = bad_grp_left_over[short_term_fail_bool]*window + shrt_term_fail_times = bad_grp_left_over[short_term_fail_bool]*window_int if shrt_term_fail_times.count() >= 2 or (shrt_term_fail_times.count() >= 1 and any(long_term_fail_bool)): - dates = [(bad_grp.groups[ky][0], bad_grp.groups[ky][-1]) for ky in shrt_term_fail_times.index] + dates = [(bad_grp.groups[(ky, True)][0], bad_grp.groups[(ky, True)][-1]) for ky in shrt_term_fail_times.index.values] pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count(), dates) return df_bad, pass_type @@ -1274,8 +1369,9 @@ def clean_final_report(final_df, drop_null=True): # drop redundant columns final_df = final_df.drop(columns=['short_term_fail']) + avail_cols = list(set(final_df.columns).intersection(set(['long_term_fail_avg_minutes', 'short_term_fail_avg_minutes']))) # sort by highest value faults - final_df = final_df.sort_values(by=['long_term_fail_avg_minutes', 'short_term_fail_avg_minutes'], ascending=False) + final_df = final_df.sort_values(by=avail_cols, ascending=False) return final_df @@ -1363,7 +1459,7 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): return pass_type, row -def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folder='./', detection_params=None): +def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', detection_params=None): """ Analyze each valve and detect for passing valves @@ -1379,8 +1475,6 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde valve operating point is malfunctioning e.g. allow enough time for residue heat to dissipate from the coil. - window: aggregation window, in minutes, to average the raw measurement data - project_folder: name of path for the project and used to save the plots and csv data. detection_params: dictionary of parameters that control the behavior of the application @@ -1409,6 +1503,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde # calculate additional parameters for analysis vlv_df = calc_add_features(vlv_df, drop_na=False) + # find timestamp interval for dataset + window = determine_timestamp_interval(vlv_df) + # check for empty dataframe if vlv_df.empty: message = "'{}' in site {} has no data! Skipping...".format(row['vlv'], row['site']) @@ -1420,7 +1517,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde # Analyze timestamps and valve operation changes - vlv_df = analyze_timestamps(vlv_df, th_time, window, row=row, project_folder=project_folder) + vlv_df = analyze_timestamps(vlv_df, th_time, window) + vlv_df = analyze_steady_vs_transient(vlv_df, row=row, project_folder=project_folder) if vlv_df is None: print("'{}' in site {} has no data after analyzing \ @@ -1498,24 +1596,26 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde df_fit_nz, popt = build_logistic_model(no_zeros_po) # calculate bad valve instances vs overall dataframe - bad_vlv, pass_type = find_bad_vlv_operation(vlv_df, row, df_fit_nz, popt, window) + fault_vlv, pass_type = find_fault_vlv_operation(vlv_df, row, df_fit_nz, popt, window, th_bad_vlv) passing_type.update(pass_type) - if bad_vlv is None: + if fault_vlv is None: bad_ratio = 0 long_tbad = None# long_tc['50%'] else: - bad_ratio = 100*(bad_vlv.shape[0]/vlv_df.shape[0]) - long_tbad = bad_vlv['temp_diff'].describe()['50%'] + bad_ratio = 100*(fault_vlv.shape[0]/vlv_df.shape[0]) + long_tbad = fault_vlv['temp_diff'].describe()['50%'] + + BAD_RATIO_THRESHOLD = 5 # estimate size of leak in terms of pct that valve is open - if df_fit_nz is not None: + if df_fit_nz is not None and long_tbad is not None: est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() - if est_leak > popt[1]: + if est_leak > popt[1] and bad_ratio > BAD_RATIO_THRESHOLD: passing_type['leak_grtr_xovr_fail'] = est_leak else: - if bad_vlv is not None: - est_leak = bad_vlv['temp_diff'].mean() + if fault_vlv is not None: + est_leak = long_tbad if est_leak > th_bad_vlv: passing_type['leak_grtr_threshold_fail'] = est_leak @@ -1525,24 +1625,24 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, window=15, project_folde elif 'long_term_fail' in passing_type.keys(): print_passing_mgs(row) folder = join(project_folder, bad_folder) - elif any(failure) and bad_ratio > 5: + elif any(failure) and bad_ratio > BAD_RATIO_THRESHOLD: print_passing_mgs(row) folder = join(project_folder, bad_folder) else: folder = join(project_folder, good_folder) - # categorized good and bad points + # categorized good and bad points vlv_df.loc[:, 'good_oper_cat'] = True - if bad_vlv is not None: - vlv_df.loc[bad_vlv.index, 'good_oper_cat'] = False + if fault_vlv is not None: + vlv_df.loc[fault_vlv.index, 'good_oper_cat'] = False _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) row.update({'long_t': round(long_tc['50%'], 2), 'long_tbad': None if long_tbad is None else round(long_tbad, 2), 'bad_ratio': round(bad_ratio, 2), 'folder': folder}) # TODO get a detailed report of the when valve is malfunctioning - # lal = bad_vlv.groupby('same') + # lal = fault_vlv.groupby('same') # grps = list(lal.groups.keys()) - # bad_vlv.loc[lal.groups[grps[0]]] + # fault_vlv.loc[lal.groups[grps[0]]] if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index a2a9ea6..a3f9854 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -176,7 +176,6 @@ def parse_dict_list_file(line): detection_params = { "th_bad_vlv": 10, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning - "window": 15, # aggregation window, in minutes, to average the raw measurement data "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. @@ -208,7 +207,7 @@ def parse_dict_list_file(line): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=5, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) From adec47b2c4d5b1b3f37d26345c86bf7a945c86b5 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 24 Feb 2022 10:01:19 -0800 Subject: [PATCH 66/83] updated/added analysis to gt ext building --- detect_passing_valves/ext_dat_app_run_gt.py | 82 ++++++++++++++++++--- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index be69aa2..19cc066 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -12,15 +12,14 @@ import numpy as np import os import time +import pickle from os.path import join -import matplotlib.pyplot as plt -from scipy.optimize import curve_fit -from scipy.stats import gaussian_kde from itertools import combinations from copy import deepcopy from app import _analyze_vlv, check_folder_exist, clean_final_report +from plot_data import * # from app import _make_tdiff_vs_aflow_plot, \ # analyze_timestamps, rename_existing, drop_unoccupied_dat, calc_long_t_diff, \ @@ -116,7 +115,7 @@ def read_multi_csvs(discharge_temp_file, airflow_rate_file, vlv_pos_file, room_t 'vlv_dat': vlv_dat, 'row': { 'vlv': 'vlv_' + vav_name, - 'site': 'bldg_gt_pr', + 'site': 'lion', 'equip': vav_name, 'upstream_type': None, } @@ -175,9 +174,20 @@ def exclude_time_interval(df, int_str, int_end): return df.loc[~within_interval, :] +def parse_dict_list_file(line): + + dictionary = dict() + pairs = line.strip().strip(",").strip('{}').split(', ') + for pr in pairs: + pair = pr.split(': ') + dictionary[pair[0].strip('\'\'\"\"')] = pair[1].strip('\'\'\"\"') + + return dictionary + + if __name__ == '__main__': dat_folder = join('./', 'external_data', 'bldg_gt_pr', '20211118') - project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_off_period') + project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req') # read files discharge_temp_file = join(dat_folder, 'B44-B45 Discharge Air Temp Sensor Readings - 01MAY2021 to 10NOV2021.csv') @@ -191,12 +201,14 @@ def exclude_time_interval(df, int_str, int_end): # define container folders good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves bad_folder = 'bad_valves' # name of path to the folder to save the plots of the malfunction valves + sensor_fault_folder = 'sensor_fault'# name of path to the folder to save plots of equipment with sensor faults air_flow_folder = 'air_flow_plots' # name of path to the folder to save plots of the air flow values csv_folder = 'csv_data' # name of path to the folder to save detailed valve data # check if holding folders exist check_folder_exist(join(project_folder, bad_folder)) check_folder_exist(join(project_folder, good_folder)) + check_folder_exist(join(project_folder, sensor_fault_folder)) check_folder_exist(join(project_folder, air_flow_folder)) check_folder_exist(join(project_folder, csv_folder)) @@ -204,13 +216,13 @@ def exclude_time_interval(df, int_str, int_end): detection_params = { "th_bad_vlv": 5, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning - "window": 15, # aggregation window, in minutes, to average the raw measurement data "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. - "air_flow_required": True, # boolean indicated is air flow rate data should strictly be used. + "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. "good_folder": good_folder, "bad_folder": bad_folder, + "sensor_fault_folder": sensor_fault_folder, "air_flow_folder": air_flow_folder, "csv_folder": csv_folder, } @@ -219,6 +231,7 @@ def exclude_time_interval(df, int_str, int_end): vavs_df = merge_down_up_stream_dat(vavs_df, ahu_file, matched_ahu_vav_file) results = [] + vav_count_summary = [] for key in vavs_df.keys(): cur_vlv_df = vavs_df[key]['vlv_dat'] required_streams = [stream in cur_vlv_df.columns for stream in ['dnstream_ta', 'upstream_ta', 'vlv_po']] @@ -232,14 +245,65 @@ def exclude_time_interval(df, int_str, int_end): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=15, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) results.append(vlv_dat) + # report and plot + # define fault folders + fault_dat_path = join(project_folder, "passing_valve_results.csv") + fig_folder_faults = join(project_folder, "ts_valve_faults") + fig_folder_good = join(project_folder, "ts_valve_good") + post_process_vlv_dat = join(project_folder, "csv_data") + vav_count_file = join(project_folder, 'vav_count_summary.csv') + raw_analyzed_data = join(project_folder, 'raw_analyzed_data.pkl') + raw_analyzed_results = join(project_folder, 'raw_analyzed_results.pkl') + final_df = pd.DataFrame.from_records(results) final_df = clean_final_report(final_df, drop_null=False) - final_df.to_csv(join(project_folder, "passing_valve_results.csv")) + final_df.to_csv(fault_dat_path) + + vav_count_summary = pd.DataFrame.from_records(vav_count_summary) + vav_count_summary.to_csv(vav_count_file) + + raw_df = open(raw_analyzed_data, "wb") + pickle.dump(vavs_df, raw_df) + raw_df.close() + + raw_result = open(raw_analyzed_results, "wb") + pickle.dump(results, raw_result) + raw_result.close() + + # create timeseries plots of the data + plot_fault_valves(post_process_vlv_dat, fault_dat_path, fig_folder_faults, time_format="Timestamp('%Y-%m-%d %H:%M:%S')") + plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, sensor_fault_folder), sample_size='all', fig_folder=fig_folder_faults) + + # plot good vav operation timeseries + plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, good_folder), sample_size='all', fig_folder=fig_folder_good) + + # Perform additional analysis + f = open(join(project_folder, 'minimum_airflow_values.txt'), 'r') + lines = f.readlines() + f.close() + + vav_results = [] + for line in lines: + vav_results.append(parse_dict_list_file(line)) + + vav_results = pd.DataFrame.from_records(vav_results) + + numeric_cols = ['minimum_air_flow_cutoff', 'long_t', 'long_tbad', 'bad_ratio', 'long_to'] + avail_cols = list(set(vav_results.columns).intersection(set(numeric_cols))) + vav_results[avail_cols] = vav_results[avail_cols].apply(pd.to_numeric, errors='coerce') + import pdb; pdb.set_trace() + + na_folder = vav_results['folder'].isna() + vav_results.loc[~na_folder,'folder_short'] = vav_results.loc[~na_folder, 'folder'].apply(os.path.basename) + + # summary statistics for each site + vav_results_grp = vav_results.groupby(['site', 'folder_short']) + vav_results_grp['long_t'].describe() import pdb; pdb.set_trace() \ No newline at end of file From 81ee6e62bb643aaaab98f58aff6f5e09d2c0e930 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 24 Feb 2022 10:24:27 -0800 Subject: [PATCH 67/83] drop na columns for required streams --- detect_passing_valves/app.py | 63 ++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 3a7bcfb..6a30736 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -382,7 +382,7 @@ def calc_add_features(vav_df, drop_na=False, drop_neg_diff=False): # drop na if drop_na: - vav_df = vav_df.dropna() + vav_df = vav_df.dropna(subset=['dnstream_ta', 'upstream_ta']) # drop values where vav supply air temperature is less than ahu supply air if drop_neg_diff: @@ -431,34 +431,38 @@ def drop_unoccupied_dat(df, row=None, occ_str=6, occ_end=18, wkend_str=5, air_fl df: Pandas dataframe with data values during building occupancy hours """ - if 'air_flow' in df.columns: # drop values where there is no air flow - xs, ys = density_data(df['air_flow'], rescale_dat=df['temp_diff']) - min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) - max_idx = return_extreme_points(ys, type_of_extreme='max', sort=False) - - if min_idx is not None: - low_air_flow = xs[min_idx[0]] - else: - low_air_flow = np.percentile(xs, 5) - - if max_idx is not None: - zero_air_flow = xs[max_idx[0]] - else: - zero_air_flow = 0 - - # take into account the accuracy of the airflow rate measurement if known - if af_accu_factor is not None: - min_air_flow = (1-af_accu_factor)*(low_air_flow-zero_air_flow) + zero_air_flow + density_df = df.loc[:, ['air_flow', 'temp_diff']].dropna() + + if not density_df.empty: + xs, ys = density_data(density_df['air_flow'], rescale_dat=density_df['temp_diff']) + min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) + max_idx = return_extreme_points(ys, type_of_extreme='max', sort=False) + + if min_idx is not None: + low_air_flow = xs[min_idx[0]] + else: + low_air_flow = np.percentile(xs, 5) + + if max_idx is not None: + zero_air_flow = xs[max_idx[0]] + else: + zero_air_flow = 0 + + # take into account the accuracy of the airflow rate measurement if known + if af_accu_factor is not None: + min_air_flow = (1-af_accu_factor)*(low_air_flow-zero_air_flow) + zero_air_flow + else: + min_air_flow = low_air_flow + + # return calculated results + if row is not None: + row.update({"minimum_air_flow_cutoff": round(min_air_flow,1)}) + + df = df.loc[df['air_flow'] > min_air_flow] else: - min_air_flow = low_air_flow - - # return calculated results - if row is not None: - row.update({"minimum_air_flow_cutoff": round(min_air_flow,1)}) - - df = df.loc[df['air_flow'] > min_air_flow] + df = occupied_hours_subset(df, occ_str, occ_end, wkend_str) elif 'air_flow' not in df.columns and air_flow_required: df = pd.DataFrame() else: @@ -1501,10 +1505,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det # calculate additional parameters for analysis - vlv_df = calc_add_features(vlv_df, drop_na=False) - - # find timestamp interval for dataset - window = determine_timestamp_interval(vlv_df) + vlv_df = calc_add_features(vlv_df, drop_na=True) # check for empty dataframe if vlv_df.empty: @@ -1515,8 +1516,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type - # Analyze timestamps and valve operation changes + window = determine_timestamp_interval(vlv_df) vlv_df = analyze_timestamps(vlv_df, th_time, window) vlv_df = analyze_steady_vs_transient(vlv_df, row=row, project_folder=project_folder) From d175878fe5d1f6a436694ef0e5bb60130b618f91 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 24 Feb 2022 10:25:38 -0800 Subject: [PATCH 68/83] use vlv model to categoize valve operation --- detect_passing_valves/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 6a30736..6787e13 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1264,6 +1264,7 @@ def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): # else: tdiff_model_max = model[model['vlv_po'] == 100]['y_fitted'].mean() if tdiff_model_max > 2*th_bad_vlv: + hi_diff = max(hi_diff, tdiff_model_max) vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() # define temperature difference and valve position failure thresholds @@ -1590,9 +1591,13 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det passing_type['tc_to_close_fail'] = round(long_tc_to_diff, 2) # assume a 0 deg difference at 0% open valve + # and at 95 pct temp diff at > 95% percent open valve no_zeros_po = vlv_df.copy() no_zeros_po.loc[~no_zeros_po['vlv_open'], 'temp_diff'] = 0 + hi_diff = np.percentile(vlv_df['temp_diff'], 100) + no_zeros_po.loc[no_zeros_po['vlv_po'] >= 95, 'temp_diff'] = hi_diff + # make a logit regression model assuming that closed valves make a zero temp difference df_fit_nz, popt = build_logistic_model(no_zeros_po) From bb5074656adac7dabc780a31f25215e594edac90 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 2 Mar 2022 09:38:31 -0800 Subject: [PATCH 69/83] visualize short term fault in ts graphs --- detect_passing_valves/plot_data.py | 38 +++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index 3a2f53d..d8ae178 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -153,21 +153,41 @@ def plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder, time_format="T # get file paths of csvs all_csv_files = os.listdir(vlv_dat_folder) + fault_vlv_files = os.listdir(join(vlv_dat_folder, '../', 'bad_valves')) # read csv file with detected passing valves fault_dat = pd.read_csv(fault_dat_path, index_col=False) for idx, df_row in fault_dat.iterrows(): + fault_dates = None + long_fault_dates = [] + short_fault_dates = [] if pd.notnull(df_row['long_term_fail_str_end_dates']): - fault_dates = df_row['long_term_fail_str_end_dates'] - fault_dates = parse_list_timestamps(fault_dates, time_format=time_format) - vlv_name = "{}-{}-{}".format(df_row['site'], df_row['equip'], df_row['vlv']) - csv_names = [f for f in all_csv_files if vlv_name in f] - - for csv in csv_names: - # plot fault data - csv_path = join(vlv_dat_folder, csv) - plot_valve_data(csv_path, fault_dates, fig_folder) + long_fault_dates = df_row['long_term_fail_str_end_dates'] + long_fault_dates = parse_list_timestamps(long_fault_dates, time_format=time_format) + if pd.notnull(df_row['short_term_fail_str_end_dates']): + short_fault_dates = df_row['short_term_fail_str_end_dates'] + short_fault_dates = parse_list_timestamps(short_fault_dates, time_format=time_format) + + fault_idx = [] + for fault_type in [long_fault_dates, short_fault_dates]: + for fault_interval in fault_type: + fault_idx.append(fault_interval) + + if len(fault_idx) > 0: + fault_dates = fault_idx + + vlv_name = "{}-{}-{}".format(df_row['site'], df_row['equip'], df_row['vlv']) + in_fault_folder = [f for f in fault_vlv_files if vlv_name in f] + + if len(in_fault_folder) == 0: + continue + csv_names = [f for f in all_csv_files if vlv_name in f] + + for csv in csv_names: + # plot fault data + csv_path = join(vlv_dat_folder, csv) + plot_valve_data(csv_path, fault_dates, fig_folder) print('-------Finished processing passing valve plots-----') From 9f0bc0956ed70c56f8dd0e325d7ca9b685133061 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 2 Mar 2022 09:39:18 -0800 Subject: [PATCH 70/83] change exceed threshold from 5 to 10 degF --- detect_passing_valves/ext_dat_app_run.py | 2 +- detect_passing_valves/ext_dat_app_run_gt.py | 8 +++++--- detect_passing_valves/ext_data_app_run_mortar.py | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index a3f9854..c971091 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -207,7 +207,7 @@ def parse_dict_list_file(line): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=10, th_time=12, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index 19cc066..50a201e 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -214,7 +214,7 @@ def parse_dict_list_file(line): # define user parameters detection_params = { - "th_bad_vlv": 5, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_bad_vlv": 10, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure @@ -232,10 +232,12 @@ def parse_dict_list_file(line): results = [] vav_count_summary = [] + n_skipped = 0 for key in vavs_df.keys(): cur_vlv_df = vavs_df[key]['vlv_dat'] required_streams = [stream in cur_vlv_df.columns for stream in ['dnstream_ta', 'upstream_ta', 'vlv_po']] if not all(required_streams): + n_skipped += 1 print("Skipping VAV = {} because all required streams are not available".format(key)) continue @@ -245,7 +247,7 @@ def parse_dict_list_file(line): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=10, th_time=12, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) @@ -253,6 +255,7 @@ def parse_dict_list_file(line): # report and plot # define fault folders + print("Skipped a total of {} terminal units".format(n_skipped)) fault_dat_path = join(project_folder, "passing_valve_results.csv") fig_folder_faults = join(project_folder, "ts_valve_faults") fig_folder_good = join(project_folder, "ts_valve_good") @@ -297,7 +300,6 @@ def parse_dict_list_file(line): numeric_cols = ['minimum_air_flow_cutoff', 'long_t', 'long_tbad', 'bad_ratio', 'long_to'] avail_cols = list(set(vav_results.columns).intersection(set(numeric_cols))) vav_results[avail_cols] = vav_results[avail_cols].apply(pd.to_numeric, errors='coerce') - import pdb; pdb.set_trace() na_folder = vav_results['folder'].isna() vav_results.loc[~na_folder,'folder_short'] = vav_results.loc[~na_folder, 'folder'].apply(os.path.basename) diff --git a/detect_passing_valves/ext_data_app_run_mortar.py b/detect_passing_valves/ext_data_app_run_mortar.py index 983abad..a1c3666 100644 --- a/detect_passing_valves/ext_data_app_run_mortar.py +++ b/detect_passing_valves/ext_data_app_run_mortar.py @@ -109,10 +109,12 @@ def parse_dict_list_file(line): results = [] vav_count_summary = [] + n_skipped = 0 for key in vavs_df.keys(): cur_vlv_df = vavs_df[key]['vlv_dat'] required_streams = [stream in cur_vlv_df.columns for stream in ['dnstream_ta', 'upstream_ta', 'vlv_po']] if not all(required_streams): + n_skipped += 1 print("Skipping VAV = {} because all required streams are not available".format(key)) continue @@ -124,7 +126,7 @@ def parse_dict_list_file(line): vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=12, window=15, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=10, th_time=12, window=15, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) @@ -133,6 +135,7 @@ def parse_dict_list_file(line): # report and plot # define fault folders + print("Skipped a total of {} terminal units".format(n_skipped)) fault_dat_path = join(project_folder, "passing_valve_results.csv") fig_folder_faults = join(project_folder, "ts_valve_faults") fig_folder_good = join(project_folder, "ts_valve_good") From 7c1942e27b73adc627041b58ee4739cce1f2c98f Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 2 Mar 2022 09:45:59 -0800 Subject: [PATCH 71/83] heat loss calc and fault manage improvments --- detect_passing_valves/app.py | 116 +++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 19 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 6787e13..ce1231e 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -832,6 +832,37 @@ def calc_long_t_diff(vlv_df): return long_t +def calc_heat_transfer(df, window=None): + """ + Calculate heat transfer from heating coils to air + """ + RHO_AIR = 0.07516 # [lb/ft3] @20C at sea level + Cp_AIR = 0.2402 # [Btu/lb-F] @20C at sea level + + if window is None: + window = determine_timestamp_interval(df) + + window_int = window.seconds/3600 + + if 'air_flow' in df.columns: + temp_diff = df["temp_diff"] + air_flow = df["air_flow"] + + heat_power_xfr = 60*RHO_AIR*Cp_AIR*air_flow*temp_diff # [Btu/hr] + heat_energy_xfr = heat_power_xfr*window_int + else: + return None + + return (round(np.mean(heat_power_xfr), 1), round(sum(heat_energy_xfr), 1)) + + +def calc_long_tc_ratio(vlv_df): + close_vlv_df = vlv_df.loc[np.logical_and(vlv_df['cons_ts_vlv_c'], vlv_df['steady'])] + long_tc_ratio = (close_vlv_df.shape[0]/vlv_df.shape[0])*100 + + return long_tc_ratio + + def check_consecutive_timestamps(df, window=None): """ Check if timestamps are consecutive in dataframe or series @@ -1040,6 +1071,9 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, long_to= # plot parametes y_max = vlv_df['temp_diff'].max() + y_min = vlv_df['temp_diff'].min() + x_max = vlv_df['vlv_po'].max() + x_min = vlv_df['vlv_po'].min() good_oper_color = '#5ab300' bad_oper_color = '#b3005a' @@ -1049,7 +1083,8 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, long_to= ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Valve opened [%]') ax.set_title("Site = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left') - ax.set_ylim((0, np.ceil(y_max*1.05))) + ax.set_ylim((min(-2, y_min)*1.02, np.ceil(y_max*1.05))) + ax.set_xlim((min(-2, x_min)*1.02, max(100, x_max)*1.02)) if any(vlv_df['good_oper_cat']): @@ -1265,7 +1300,10 @@ def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): tdiff_model_max = model[model['vlv_po'] == 100]['y_fitted'].mean() if tdiff_model_max > 2*th_bad_vlv: hi_diff = max(hi_diff, tdiff_model_max) - vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() + vlv_po_hi_diff = model[model['y_fitted'] >= hi_diff]['vlv_po'].mean() + + if vlv_po_hi_diff in [np.nan, None]: + vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() # define temperature difference and valve position failure thresholds vlv_po_th = vlv_po_hi_diff/2.0 @@ -1305,12 +1343,25 @@ def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): bad_grp_count_cons_ts = bad_grp_count[(slice(None), True)] # detect long term failures + # TODO: only calculate heat loss for fault timeperiods and not all of "df_bad" + long_fault = False + long_fault_idx = [] + long_all_dates = [] + + short_fault = False + short_fault_idx = [] + short_all_dates = [] + long_term_fail_bool = (bad_grp_count_cons_ts*window_int) > long_term_fail if any(long_term_fail_bool): long_term_fail_times = bad_grp_count_cons_ts[long_term_fail_bool]*window_int if long_term_fail_times.count() >= 1 or long_term_fail_times.index[-1] == vlv_df['same'].max(): - dates = [(bad_grp.groups[(ky, True)][0], bad_grp.groups[(ky, True)][-1]) for ky in long_term_fail_times.index.values] + long_fault_idx = long_term_fail_times.index.values + long_all_dates = [bad_grp.groups[(ky, True)] for ky in long_fault_idx] + dates = [(ky[0], ky[-1]) for ky in long_all_dates] + pass_type['long_term_fail'] = (long_term_fail_times.mean(), long_term_fail_times.count(), dates) + long_fault = True # detect short term failures bad_grp_left_over = bad_grp_count_cons_ts[~long_term_fail_bool] @@ -1318,8 +1369,27 @@ def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): if any(short_term_fail_bool): shrt_term_fail_times = bad_grp_left_over[short_term_fail_bool]*window_int if shrt_term_fail_times.count() >= 2 or (shrt_term_fail_times.count() >= 1 and any(long_term_fail_bool)): - dates = [(bad_grp.groups[(ky, True)][0], bad_grp.groups[(ky, True)][-1]) for ky in shrt_term_fail_times.index.values] + short_fault_idx = shrt_term_fail_times.index.values + short_all_dates = [bad_grp.groups[(ky, True)] for ky in short_fault_idx] + dates = [(ky[0], ky[-1]) for ky in short_all_dates] + pass_type['short_term_fail'] = (shrt_term_fail_times.mean(), shrt_term_fail_times.count(), dates) + short_fault = True + + if any([long_fault, short_fault]): + # # method 1 of filter fault data: assume all on of one switch is bad + # fault_idx = np.append(long_fault_idx, short_fault_idx) + # fault_points = df_bad['same'].isin(fault_idx) + + # method 2 of filtering for fault data: more specific by using exact dates + fault_idx = [] + for fault_type in [long_all_dates, short_all_dates]: + for fault_dates in fault_type: + for fault_ts in fault_dates: + fault_idx.append(fault_ts) + + fault_points = df_bad.index.isin(fault_idx) + pass_type['heat_loss_pwr-avg_nrgy-sum'] = calc_heat_transfer(df_bad.loc[fault_points], window) return df_bad, pass_type @@ -1407,7 +1477,7 @@ def analyze_only_open(vlv_df, row, th_bad_vlv, project_folder): long_to = vlv_df[vlv_df['vlv_open']]['temp_diff'].describe() if long_to['50%'] < th_bad_vlv: print("'{}' in site {} is open but seems to not cause an increase in air temperature\n".format(row['vlv'], row['site'])) - pass_type['non_responsive_fail'] = round(long_to['50%'] - th_bad_vlv, 2) + pass_type['non_responsive_fail_degF'] = round(long_to['50%'], 2) folder = join(project_folder, bad_folder) vlv_df.loc[:, 'good_oper_cat'] = False long_t = None @@ -1419,6 +1489,7 @@ def analyze_only_open(vlv_df, row, th_bad_vlv, project_folder): long_tbad = None _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_to=long_t, long_tbad=long_tbad, folder=folder) + pass_type['folder'] = folder row.update({'long_to': round(long_to['50%'], 2), 'folder': folder}) return pass_type, row @@ -1445,9 +1516,12 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): """ pass_type = dict() long_tc = calc_long_t_diff(vlv_df) + long_tc_ratio = calc_long_tc_ratio(vlv_df) + if long_tc['50%'] > th_bad_vlv: print_passing_mgs(row) - pass_type['simple_fail'] = round(long_tc['50%'] - th_bad_vlv, 2) + pass_type['simple_fail_dat-ratio_degF'] = (round(long_tc_ratio,1), round(long_tc['50%'], 2)) + folder = join(project_folder, bad_folder) vlv_df.loc[:, 'good_oper_cat'] = False long_t = None @@ -1460,6 +1534,7 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): if log_details: logger.info("[{}] is only closed with good operation data".format(row['vlv'])) _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_t, long_tbad=long_tbad, folder=folder) + pass_type['folder'] = folder row.update({'long_t': round(long_tc['50%'], 2), 'folder': folder}) return pass_type, row @@ -1572,6 +1647,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det long_tc = calc_long_t_diff(vlv_df) long_to = vlv_df[vlv_df['vlv_open']]['temp_diff'].describe() + long_tc_ratio = calc_long_tc_ratio(vlv_df) + if long_tc is None and long_to is not None: pass_type, row = analyze_only_open(vlv_df, row, th_bad_vlv, project_folder) passing_type.update(pass_type) @@ -1582,7 +1659,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det # make simple comparison of long-term closed temp difference and user define threshold if long_tc['50%'] > th_bad_vlv: print_passing_mgs(row) - passing_type['simple_fail'] = round(long_tc['50%'] - th_bad_vlv, 2) + #TODO make sure this gets flag as fault and put in bad folder + passing_type['simple_fail_dat-ratio_degF'] = (round(long_tc_ratio,1), round(long_tc['50%'], 2)) # make comparison between long-term open and long-term closed temp difference long_tc_to_diff = (long_tc['mean'] + long_tc['std']) - (long_to['75%']) @@ -1592,10 +1670,11 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det # assume a 0 deg difference at 0% open valve # and at 95 pct temp diff at > 95% percent open valve + HI_DIFF_PERCENTILE = 95 no_zeros_po = vlv_df.copy() no_zeros_po.loc[~no_zeros_po['vlv_open'], 'temp_diff'] = 0 - hi_diff = np.percentile(vlv_df['temp_diff'], 100) + hi_diff = np.percentile(vlv_df['temp_diff'], HI_DIFF_PERCENTILE) no_zeros_po.loc[no_zeros_po['vlv_po'] >= 95, 'temp_diff'] = hi_diff # make a logit regression model assuming that closed valves make a zero temp difference @@ -1618,20 +1697,23 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if df_fit_nz is not None and long_tbad is not None: est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() if est_leak > popt[1] and bad_ratio > BAD_RATIO_THRESHOLD: - passing_type['leak_grtr_xovr_fail'] = est_leak + passing_type['leak_grtr_xovr_fail_vlv-pos'] = est_leak else: if fault_vlv is not None: est_leak = long_tbad - if est_leak > th_bad_vlv: - passing_type['leak_grtr_threshold_fail'] = est_leak + if est_leak > th_bad_vlv and bad_ratio > BAD_RATIO_THRESHOLD: + passing_type['leak_grtr_threshold_fail_degF'] = est_leak - failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail', 'leak_grtr_threshold_fail'] for x in passing_type.keys()] - if len([x for x in passing_type.keys() if 'sensor_fault' in x]): + failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail_vlv-pos', 'leak_grtr_threshold_fail_degF'] for x in passing_type.keys()] + if len([x for x in passing_type.keys() if 'sensor_fault' in x]) > 0: folder = join(project_folder, sensor_fault_folder) elif 'long_term_fail' in passing_type.keys(): print_passing_mgs(row) folder = join(project_folder, bad_folder) - elif any(failure) and bad_ratio > BAD_RATIO_THRESHOLD: + elif any(failure) and (bad_ratio > BAD_RATIO_THRESHOLD): + print_passing_mgs(row) + folder = join(project_folder, bad_folder) + elif 'simple_fail_dat-ratio_degF' in passing_type.keys() and (long_tc_ratio > BAD_RATIO_THRESHOLD): print_passing_mgs(row) folder = join(project_folder, bad_folder) else: @@ -1643,13 +1725,9 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det vlv_df.loc[fault_vlv.index, 'good_oper_cat'] = False _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + pass_type['folder'] = folder row.update({'long_t': round(long_tc['50%'], 2), 'long_tbad': None if long_tbad is None else round(long_tbad, 2), 'bad_ratio': round(bad_ratio, 2), 'folder': folder}) - # TODO get a detailed report of the when valve is malfunctioning - # lal = fault_vlv.groupby('same') - # grps = list(lal.groups.keys()) - # fault_vlv.loc[lal.groups[grps[0]]] - if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return passing_type From 4d230659d157f42868b7e0835b851b83aa63b079 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 4 Mar 2022 14:06:07 -0800 Subject: [PATCH 72/83] add air flow cutoff in plots --- detect_passing_valves/app.py | 62 +++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index ce1231e..9016afe 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -440,21 +440,7 @@ def drop_unoccupied_dat(df, row=None, occ_str=6, occ_end=18, wkend_str=5, air_fl min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) max_idx = return_extreme_points(ys, type_of_extreme='max', sort=False) - if min_idx is not None: - low_air_flow = xs[min_idx[0]] - else: - low_air_flow = np.percentile(xs, 5) - - if max_idx is not None: - zero_air_flow = xs[max_idx[0]] - else: - zero_air_flow = 0 - - # take into account the accuracy of the airflow rate measurement if known - if af_accu_factor is not None: - min_air_flow = (1-af_accu_factor)*(low_air_flow-zero_air_flow) + zero_air_flow - else: - min_air_flow = low_air_flow + min_air_flow = calc_min_air_flow_cutoff(xs, max_idx, min_idx, af_accu_factor) # return calculated results if row is not None: @@ -476,6 +462,27 @@ def drop_unoccupied_dat(df, row=None, occ_str=6, occ_end=18, wkend_str=5, air_fl return df +def calc_min_air_flow_cutoff(xs, max_idx, min_idx, af_accu_factor=None): + + if min_idx is not None: + low_air_flow = xs[min_idx[0]] + else: + low_air_flow = np.percentile(xs, 5) + + if max_idx is not None: + zero_air_flow = xs[max_idx[0]] + else: + zero_air_flow = 0 + + # take into account the accuracy of the airflow rate measurement if known + if af_accu_factor is not None: + min_air_flow = (1-af_accu_factor)*(low_air_flow-zero_air_flow) + zero_air_flow + else: + min_air_flow = low_air_flow + + return min_air_flow + + def get_vav_flow(fetch_resp_vav, row, fillna=None): """ Return VAV supply air flow @@ -1100,7 +1107,7 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, long_to= if df_fit is not None: # add fit line - ax.plot(df_fit['vlv_po'], df_fit['y_fitted'], '--', label='Fitted valve model', color='#5900b3') + ax.plot(df_fit['vlv_po'], df_fit['y_fitted'], linestyle = '--', label='Fitted valve model', color='#5900b3') if long_t is not None: # add long-term temperature diff @@ -1148,7 +1155,7 @@ def rename_existing(path, idx, row): return path -def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): +def _make_tdiff_vs_aflow_plot(vlv_df, row, folder, af_accu_factor=None): """ Create temperature difference versus air flow plots @@ -1187,19 +1194,25 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder): # create density plot for air flow xs, ys = density_data(vlv_df['air_flow'], rescale_dat=vlv_df['temp_diff']) - ax.plot(xs, ys) + ax.plot(xs, ys, label='KDE') # find modes of the distribution and the trough before/after the modes - max_idx = return_extreme_points(ys, type_of_extreme='max', n_modes=2) + # max_idx = return_extreme_points(ys, type_of_extreme='max', n_modes=2) + max_idx = return_extreme_points(ys, type_of_extreme='max', sort=False) min_idx = return_extreme_points(ys, type_of_extreme='min', sort=False) + if af_accu_factor is not None: + min_air_flow = calc_min_air_flow_cutoff(xs, max_idx, min_idx, af_accu_factor) + # plot vertical line + ax.axvline(x=round(min_air_flow,1), color='gray', linestyle = '--', label='Minimum operation cutoff') + if max_idx is not None: - ax.scatter(x=xs[max_idx], y=ys[max_idx], color = '#ff0000', alpha=1, s=35) + ax.scatter(x=xs[max_idx], y=ys[max_idx], color = '#ff0000', alpha=1, s=35, label='Peaks') if max_idx is not None: - ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35) + ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35, label='Throughs') # Legend - ax.legend(markerscale=2) + ax.legend(fontsize=6, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.50, 1.02), loc='lower left') plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) @@ -1614,11 +1627,10 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if 'air_flow' in vlv_df.columns: # plot temp diff vs air flow - _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots')) - + _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots'), af_accu_factor=af_accu_factor) # drop data that occurs during unoccupied hours - vlv_df, row = drop_unoccupied_dat(vlv_df, row=row, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=air_flow_required) + vlv_df, row = drop_unoccupied_dat(vlv_df, row=row, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=air_flow_required, af_accu_factor=af_accu_factor) if vlv_df.empty: print("'{}' in site {} has no data after hours of \ From 43f8d30a6c62829dcce3cbc3cdda60bca58fe220 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Fri, 4 Mar 2022 14:12:11 -0800 Subject: [PATCH 73/83] final parameters for paper --- detect_passing_valves/ext_dat_app_run.py | 9 ++++++--- detect_passing_valves/ext_dat_app_run_gt.py | 8 +++++--- detect_passing_valves/ext_data_app_run_mortar.py | 10 +++++----- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index c971091..ec1d1ea 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -9,6 +9,7 @@ """ import pandas as pd +from ext_dat_app_run_gt import TH_BAD_VLV import numpy as np import os import time @@ -146,7 +147,7 @@ def parse_dict_list_file(line): if __name__ == '__main__': # NOTE: HW plant off from August 31 to October 7 2021 dat_folder = join('./', 'external_data', 'bldg_trc_rs') - project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_test_no_aflw_req') + project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') csv_list = [ join(dat_folder, 'zone trends, September 2021.csv'), @@ -173,13 +174,15 @@ def parse_dict_list_file(line): check_folder_exist(join(project_folder, csv_folder)) # define user parameters + TH_BAD_VLV = 10 detection_params = { - "th_bad_vlv": 10, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_bad_vlv": TH_BAD_VLV, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. + "af_accu_factor": 0.80, "good_folder": good_folder, "bad_folder": bad_folder, "sensor_fault_folder": sensor_fault_folder, @@ -207,7 +210,7 @@ def parse_dict_list_file(line): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=10, th_time=12, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index 50a201e..53a2d0d 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -187,7 +187,7 @@ def parse_dict_list_file(line): if __name__ == '__main__': dat_folder = join('./', 'external_data', 'bldg_gt_pr', '20211118') - project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req') + project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') # read files discharge_temp_file = join(dat_folder, 'B44-B45 Discharge Air Temp Sensor Readings - 01MAY2021 to 10NOV2021.csv') @@ -213,13 +213,15 @@ def parse_dict_list_file(line): check_folder_exist(join(project_folder, csv_folder)) # define user parameters + TH_BAD_VLV = 10 detection_params = { - "th_bad_vlv": 10, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_bad_vlv": TH_BAD_VLV, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. + "af_accu_factor": 0.80, "good_folder": good_folder, "bad_folder": bad_folder, "sensor_fault_folder": sensor_fault_folder, @@ -247,7 +249,7 @@ def parse_dict_list_file(line): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=10, th_time=12, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) diff --git a/detect_passing_valves/ext_data_app_run_mortar.py b/detect_passing_valves/ext_data_app_run_mortar.py index a1c3666..b6b001c 100644 --- a/detect_passing_valves/ext_data_app_run_mortar.py +++ b/detect_passing_valves/ext_data_app_run_mortar.py @@ -73,7 +73,7 @@ def parse_dict_list_file(line): if __name__ == '__main__': dat_folder = join('with_airflow_checks_year_start', 'csv_data') - project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req') + project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') # define container folders good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves @@ -90,14 +90,15 @@ def parse_dict_list_file(line): check_folder_exist(join(project_folder, csv_folder)) # define user parameters + TH_BAD_VLV = 10 detection_params = { - "th_bad_vlv": 10, # temperature difference from long term temperature difference to consider an operating point as malfunctioning + "th_bad_vlv": TH_BAD_VLV, # temperature difference from long term temperature difference to consider an operating point as malfunctioning "th_time": 12, # length of time, in minutes, after the valve is closed to determine if valve operating point is malfunctioning - "window": 15, # aggregation window, in minutes, to average the raw measurement data "long_term_fail": 4*60, # number of minutes to trigger an long-term passing valve failure "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. + "af_accu_factor": 0.80, "good_folder": good_folder, "bad_folder": bad_folder, "sensor_fault_folder": sensor_fault_folder, @@ -126,7 +127,7 @@ def parse_dict_list_file(line): vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=10, th_time=12, window=15, project_folder=project_folder, detection_params=detection_params) + passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) # save results vlv_dat.update(passing_type) @@ -169,7 +170,6 @@ def parse_dict_list_file(line): # plot good vav operation timeseries plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, good_folder), sample_size='all', fig_folder=fig_folder_good) - import pdb; pdb.set_trace() # Perform additional analysis f = open(join(project_folder, 'minimum_airflow_values.txt'), 'r') From 3c6afbee424154212e8f69d0201e3edbae5572f8 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 9 Mar 2022 09:31:28 -0800 Subject: [PATCH 74/83] updated parameters for paper --- detect_passing_valves/ext_dat_app_run.py | 4 +--- detect_passing_valves/ext_dat_app_run_gt.py | 2 +- detect_passing_valves/ext_data_app_run_mortar.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index ec1d1ea..9e44f37 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -9,7 +9,6 @@ """ import pandas as pd -from ext_dat_app_run_gt import TH_BAD_VLV import numpy as np import os import time @@ -182,7 +181,7 @@ def parse_dict_list_file(line): "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. - "af_accu_factor": 0.80, + "af_accu_factor": 0.60, "good_folder": good_folder, "bad_folder": bad_folder, "sensor_fault_folder": sensor_fault_folder, @@ -248,7 +247,6 @@ def parse_dict_list_file(line): # plot good vav operation timeseries plot_valve_ts_streams(post_process_vlv_dat, join(project_folder, good_folder), sample_size='all', fig_folder=fig_folder_good) - import pdb; pdb.set_trace() # Perform additional analysis f = open(join(project_folder, 'minimum_airflow_values.txt'), 'r') lines = f.readlines() diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index 53a2d0d..da7b8ae 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -221,7 +221,7 @@ def parse_dict_list_file(line): "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. - "af_accu_factor": 0.80, + "af_accu_factor": 0.60, "good_folder": good_folder, "bad_folder": bad_folder, "sensor_fault_folder": sensor_fault_folder, diff --git a/detect_passing_valves/ext_data_app_run_mortar.py b/detect_passing_valves/ext_data_app_run_mortar.py index b6b001c..9242e7a 100644 --- a/detect_passing_valves/ext_data_app_run_mortar.py +++ b/detect_passing_valves/ext_data_app_run_mortar.py @@ -98,7 +98,7 @@ def parse_dict_list_file(line): "shrt_term_fail": 60, # number of minutes to trigger an intermitten passing valve failure "th_vlv_fail": 20, # equivalent percentage of valve open for determining failure. "air_flow_required": False, # boolean indicated is air flow rate data should strictly be used. - "af_accu_factor": 0.80, + "af_accu_factor": 0.60, "good_folder": good_folder, "bad_folder": bad_folder, "sensor_fault_folder": sensor_fault_folder, From 4b99e5ffe6e841e3dcd55be9bf304b0dd0e65512 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 9 Mar 2022 09:42:10 -0800 Subject: [PATCH 75/83] fixed bug for processing fault dates --- detect_passing_valves/plot_data.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/detect_passing_valves/plot_data.py b/detect_passing_valves/plot_data.py index d8ae178..b81846e 100644 --- a/detect_passing_valves/plot_data.py +++ b/detect_passing_valves/plot_data.py @@ -162,12 +162,18 @@ def plot_fault_valves(vlv_dat_folder, fault_dat_path, fig_folder, time_format="T fault_dates = None long_fault_dates = [] short_fault_dates = [] - if pd.notnull(df_row['long_term_fail_str_end_dates']): + if 'long_term_fail_str_end_dates' in df_row.keys(): long_fault_dates = df_row['long_term_fail_str_end_dates'] - long_fault_dates = parse_list_timestamps(long_fault_dates, time_format=time_format) - if pd.notnull(df_row['short_term_fail_str_end_dates']): + if pd.notnull(long_fault_dates): + long_fault_dates = parse_list_timestamps(long_fault_dates, time_format=time_format) + else: + long_fault_dates = [] + if 'short_term_fail_str_end_dates' in df_row.keys(): short_fault_dates = df_row['short_term_fail_str_end_dates'] - short_fault_dates = parse_list_timestamps(short_fault_dates, time_format=time_format) + if pd.notnull(short_fault_dates): + short_fault_dates = parse_list_timestamps(short_fault_dates, time_format=time_format) + else: + short_fault_dates = [] fault_idx = [] for fault_type in [long_fault_dates, short_fault_dates]: From a6483a0d91319ed1b90d21785a93c40239010e7b Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Wed, 9 Mar 2022 09:42:55 -0800 Subject: [PATCH 76/83] various improvements to detection algorithm --- detect_passing_valves/app.py | 288 +++++++++++++++++++++++++---------- 1 file changed, 209 insertions(+), 79 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 9016afe..785a2c2 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -390,6 +390,27 @@ def calc_add_features(vav_df, drop_na=False, drop_neg_diff=False): return vav_df + +def calc_smooth_temp_diff(vlv_df): + """ + Calculate smoothed temperature difference + """ + alpha = 0.98 + dn_stream_temp = vlv_df["dnstream_ta"] + up_stream_temp = vlv_df["upstream_ta"] + + smooth_diff = [] + smooth_t = vlv_df[vlv_df["cons_ts"], "temp_diff"][0] + for ts in range(len(dn_stream_temp)): + if vlv_df["cons_ts"][ts]: + smooth_t = alpha*smooth_t + (1-alpha)*(dn_stream_temp[ts] - up_stream_temp[ts]) + + smooth_diff.append(smooth_t) + + vlv_df["smooth_temp_diff"] = smooth_diff + vlv_df.loc[~vlv_df["cons_ts"], "smooth_temp_diff"] = np.nan + + def determine_timestamp_interval(df): """ Determine time interval for dataframe @@ -715,6 +736,9 @@ def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff', scaled=False): popt: an array of the optimized parameters, slope and inflection point of the sigmoid function """ + # remove nan from data + df = df.dropna(subset=[x_col, y_col]) + try: # fit the curve if scaled: @@ -741,7 +765,8 @@ def build_logistic_model(df, x_col='vlv_po', y_col='temp_diff', scaled=False): y_fitted.name = 'y_fitted' # make sure model goes from 0 to 100 percent - x_ext_vals = range(0,105,5) + model_interval = 1 + x_ext_vals = range(0,100+model_interval,model_interval) y_ext_vals = sigmoid_func(x_ext_vals, est_k, est_x0) df_ext = pd.DataFrame({'vlv_po': x_ext_vals, 'y_fitted': y_ext_vals}) @@ -839,9 +864,21 @@ def calc_long_t_diff(vlv_df): return long_t -def calc_heat_transfer(df, window=None): +def calc_heat_transfer(df, long_term_temp_diff=None, window=None): """ Calculate heat transfer from heating coils to air + + Parameters + ---------- + df: pandas dataframe containing the columns + "air_flow" (airflow rate in cubic feet per second) and + "temp_diff" (temperature difference [Tsuppy_vav - Tsupply_ahu]) + + long_term_temp_diff: average temperature difference when vav reheat + valve is closed + + window: pandas timedelta object indicating timestamp interval + """ RHO_AIR = 0.07516 # [lb/ft3] @20C at sea level Cp_AIR = 0.2402 # [Btu/lb-F] @20C at sea level @@ -851,12 +888,18 @@ def calc_heat_transfer(df, window=None): window_int = window.seconds/3600 + temp_diff = df["temp_diff"] + if long_term_temp_diff is not None: + delta_temp = (temp_diff-long_term_temp_diff) + else: + delta_temp = temp_diff + if 'air_flow' in df.columns: temp_diff = df["temp_diff"] air_flow = df["air_flow"] - heat_power_xfr = 60*RHO_AIR*Cp_AIR*air_flow*temp_diff # [Btu/hr] - heat_energy_xfr = heat_power_xfr*window_int + heat_power_xfr = 60*RHO_AIR*Cp_AIR*air_flow*delta_temp # [Btu/hr] + heat_energy_xfr = heat_power_xfr*window_int # [Btu] else: return None @@ -1183,7 +1226,7 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder, af_accu_factor=None): fig, ax = plt.subplots(figsize=(8,4.5)) ax.set_ylabel('Temperature difference [°F]') ax.set_xlabel('Air flow [cfm]') - ax.set_title("Valve = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left') + ax.set_title("Site = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left') if any(~vlv_df['vlv_open']): ax.scatter(x=vlv_df.loc[~vlv_df['vlv_open'], 'air_flow'], y=vlv_df.loc[~vlv_df['vlv_open'], 'temp_diff'], color = closed_vlv_color, alpha=1/3, s=10, label='Closed valve') @@ -1209,7 +1252,7 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder, af_accu_factor=None): if max_idx is not None: ax.scatter(x=xs[max_idx], y=ys[max_idx], color = '#ff0000', alpha=1, s=35, label='Peaks') if max_idx is not None: - ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35, label='Throughs') + ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35, label='Troughs') # Legend ax.legend(fontsize=6, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.50, 1.02), loc='lower left') @@ -1263,7 +1306,18 @@ def density_data(dat, rescale_dat=None): return xs, ys -def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): +def smooth_ts(val, box_pts): + """ + Apply smoothing function to timeseries values + """ + + box = np.ones(box_pts)/box_pts + val_smooth = np.convolve(val, box, mode='same') + + return val_smooth + + +def find_fault_vlv_operation(vlv_df, row, long_tc, window, th_bad_vlv): """ Determine which timeseries values are data from probable passing valves and return a pandas dataframe of only 'bad' values. @@ -1290,68 +1344,135 @@ def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): (X minutes defined in global parameter 'shrt_term_fail') due to control errors, mechanical/electrical problems, or other. Default 60 minutes (1 hour). """ - PROBABILITY = 0.16 - window_int = window.seconds/60 + window_int = window.seconds/60 pass_type = dict() - hi_diff = np.percentile(vlv_df.loc[vlv_df['vlv_open'], 'temp_diff'], 95) - - if model is not None: - # # analyze sigmoid function - # y_max = max(vlv_df['temp_diff']) - # y_min = min(vlv_df['temp_diff']) - # sigmoid_func = make_sigmoid_func(y_max, y_min) - # max_model_diff = round(sigmoid_func(100, popt[0], popt[1]), 1) - # if max_model_diff > 2*th_bad_vlv: - # x_prime = range(0,101,1) - # y_prime = sigmoid_prime(x_prime, popt[0], popt[1], y_max, y_min) - # y_prime_norm = y_prime/sum(y_prime) - - # vlv_start_open = list(x_prime)[sum(np.cumsum(y_prime_norm) < PROBABILITY)] - # diff_vlv_po_th = round(sigmoid_func(vlv_start_open, popt[0], popt[1]), 1) - # else: - tdiff_model_max = model[model['vlv_po'] == 100]['y_fitted'].mean() - if tdiff_model_max > 2*th_bad_vlv: - hi_diff = max(hi_diff, tdiff_model_max) - vlv_po_hi_diff = model[model['y_fitted'] >= hi_diff]['vlv_po'].mean() - - if vlv_po_hi_diff in [np.nan, None]: - vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() - - # define temperature difference and valve position failure thresholds - vlv_po_th = vlv_po_hi_diff/2.0 - diff_vlv_po_th = max(model[model['vlv_po'] <= vlv_po_th]['y_fitted'].max(), th_bad_vlv) + HI_DIFF_PERCENTILE = 95 + # MAX_ITER = 5 + + def categorize_fault_operation(vlv_df, min_temp_diff_cutoff, iter=1, row=row): + # assume median of anything below long_tc deg difference at 0% open valve + # and at 95 pct temp diff at > 95% percent open valve + ideal_vlv_df = vlv_df.copy(deep=True) + closed_valve = ~ideal_vlv_df['vlv_open'] + below_long_tc = ideal_vlv_df['temp_diff'] < min_temp_diff_cutoff + + adj_delT_at_closed_valve = ideal_vlv_df.loc[np.logical_and(closed_valve, below_long_tc), 'temp_diff'].describe() + ideal_vlv_df.loc[closed_valve, 'temp_diff'] = adj_delT_at_closed_valve["50%"] + + hi_diff = np.percentile(ideal_vlv_df.loc[ideal_vlv_df['vlv_open'], 'temp_diff'], HI_DIFF_PERCENTILE) + ideal_vlv_df.loc[ideal_vlv_df['vlv_po'] >= 95, 'temp_diff'] = hi_diff + + try: + # make a logit regression model assuming that closed valves make a zero temp difference + model, popt = build_logistic_model(ideal_vlv_df) + except ValueError: + import pdb; pdb.set_trace() + + if model is not None: + # # analyze sigmoid function + # y_max = max(vlv_df['temp_diff']) + # y_min = min(vlv_df['temp_diff']) + # sigmoid_func = make_sigmoid_func(y_max, y_min) + # max_model_diff = round(sigmoid_func(100, popt[0], popt[1]), 1) + # if max_model_diff > 2*th_bad_vlv: + # x_prime = range(0,101,1) + # y_prime = sigmoid_prime(x_prime, popt[0], popt[1], y_max, y_min) + # y_prime_norm = y_prime/sum(y_prime) + + # vlv_start_open = list(x_prime)[sum(np.cumsum(y_prime_norm) < PROBABILITY)] + # diff_vlv_po_th = round(sigmoid_func(vlv_start_open, popt[0], popt[1]), 1) + # else: + lower_bend = popt[1] - 1.317/popt[0] + # if row["equip"] == "VAVRM1013": + # import pdb; pdb.set_trace() + + vlv_position_range = np.logical_and(model['vlv_po'] >= (lower_bend - 2), model['vlv_po'] <= (lower_bend + 2)) + diff_vlv_po_th = min(model[vlv_position_range]['y_fitted'].mean(), 10) + + # tdiff_model_max = model[model['vlv_po'] == 100]['y_fitted'].mean() + # if tdiff_model_max > 2*th_bad_vlv: + # hi_diff = max(hi_diff, tdiff_model_max) + # vlv_po_hi_diff = model[model['y_fitted'] >= hi_diff]['vlv_po'].mean() + + # if vlv_po_hi_diff in [np.nan, None]: + # vlv_po_hi_diff = model[model['y_fitted'] <= hi_diff]['vlv_po'].max() + + # # define temperature difference and valve position failure thresholds + # vlv_po_th = vlv_po_hi_diff/2.0 + # diff_vlv_po_th = max(model[model['vlv_po'] <= vlv_po_th]['y_fitted'].max(), th_bad_vlv) + # else: + # diff_vlv_po_th = max(hi_diff/4.0, th_bad_vlv) else: - diff_vlv_po_th = max(hi_diff/4.0, th_bad_vlv) + diff_vlv_po_th = max(hi_diff/4.0, th_bad_vlv/2) + + smooth_box = 3 + vlv_df["temp_diff_smooth"] = np.nan + df_grps = vlv_df.loc[~vlv_df["vlv_open"], :].groupby(["same"]) + df_grp_count = df_grps["same"].count() + for grp in df_grp_count.index.values: + if df_grp_count[grp] > smooth_box: + df_grp = df_grps.get_group(grp) + temp_values = df_grp.loc[df_grp["steady"], "temp_diff"] + if len(temp_values) < smooth_box: + continue + temp_diff_smooth = smooth_ts(temp_values, smooth_box) + vlv_df.loc[temp_values.index, "temp_diff_smooth"] = temp_diff_smooth + + # find datapoints that exceed long-term temperature difference + exceed_long_t = vlv_df['temp_diff'] >= diff_vlv_po_th + smoothed_threshold = min(adj_delT_at_closed_valve["mean"] + adj_delT_at_closed_valve["std"]*2, diff_vlv_po_th) + exceed_long_t_smooth = vlv_df['temp_diff_smooth'] >= smoothed_threshold + + # subset data by consecutive steady state values when valve is commanded closed and + # exceeds long-term temperature difference + exceed_long_t_params = np.logical_and(exceed_long_t, exceed_long_t_smooth) + th_exceed = np.logical_and(np.logical_and(vlv_df['cons_ts_vlv_c'], vlv_df['steady']), exceed_long_t_params) + vlv_df["fault_operation"] = th_exceed + df_bad = vlv_df.copy(deep=True) + df_bad = df_bad[th_exceed] + + # check that stats of fault and normal valve operation + # when it is closed do not overlap + long_tbad = calc_long_t_diff(df_bad) + + return vlv_df, df_bad, model, popt, long_tbad, adj_delT_at_closed_valve + + # Add boolean indicate if data is above temperature threshold + # and above minimum airflow cutoff + if "minimum_air_flow_cutoff" in row.keys(): + is_above_air_flow = vlv_df["air_flow"] > row["minimum_air_flow_cutoff"] + is_above_temp = vlv_df["temp_diff"] > th_bad_vlv + is_above_thresholds = np.logical_and(is_above_air_flow, is_above_temp) else: - diff_vlv_po_th = max(hi_diff/4.0, th_bad_vlv) + is_above_thresholds = vlv_df["temp_diff"] > th_bad_vlv - # find datapoints that exceed long-term temperature difference - exceed_long_t = vlv_df['temp_diff'] >= diff_vlv_po_th + vlv_df["abv_thr_temp_airflow"] = is_above_thresholds - # subset data by consecutive steady state values when valve is commanded closed and - # exceeds long-term temperature difference - th_exceed = np.logical_and(np.logical_and(vlv_df['cons_ts_vlv_c'], vlv_df['steady']), exceed_long_t) - df_bad = vlv_df[th_exceed] + + try: + min_temp_diff_cutoff = min(long_tc["50%"]*2, th_bad_vlv) + vlv_df, df_bad, model, popt, long_tbad, adj_delT_at_closed_valve = categorize_fault_operation(vlv_df, min_temp_diff_cutoff) + except ValueError: + import pdb; pdb.set_trace() if df_bad.empty: - return None, dict() + df_bad = None + # vlv_df, df_bad, + return vlv_df, df_bad, pass_type, model, popt # check that timestamps are consecutive in df_bad df_bad_cons_ts = check_consecutive_timestamps(df_bad, window) df_bad['cons_ts_fault_vlv'] = df_bad_cons_ts # analyze 'bad' dataframe for possible passing valve - #bad_grp = df_bad.groupby('same') - bad_grp = df_bad.loc[df_bad['cons_ts_fault_vlv'], :].groupby(['same', 'cons_ts_fault_vlv']) - bad_grp = df_bad.groupby(['same', 'cons_ts_fault_vlv']) bad_grp_count = bad_grp['same'].count() idx_const_vals = bad_grp_count.index.get_level_values("cons_ts_fault_vlv").values if not any(idx_const_vals): # not bad values with consecutive timestamps found - return df_bad, pass_type + return vlv_df, df_bad, pass_type, model, popt bad_grp_count_cons_ts = bad_grp_count[(slice(None), True)] @@ -1402,9 +1523,9 @@ def find_fault_vlv_operation(vlv_df, row, model, popt, window, th_bad_vlv): fault_idx.append(fault_ts) fault_points = df_bad.index.isin(fault_idx) - pass_type['heat_loss_pwr-avg_nrgy-sum'] = calc_heat_transfer(df_bad.loc[fault_points], window) + pass_type['heat_loss_pwr-avg_nrgy-sum'] = calc_heat_transfer(df_bad.loc[fault_points], adj_delT_at_closed_valve["50%"], window) - return df_bad, pass_type + return vlv_df, df_bad, pass_type, model, popt def print_passing_mgs(row): @@ -1531,19 +1652,40 @@ def analyze_only_close(vlv_df, row, th_bad_vlv, project_folder): long_tc = calc_long_t_diff(vlv_df) long_tc_ratio = calc_long_tc_ratio(vlv_df) - if long_tc['50%'] > th_bad_vlv: - print_passing_mgs(row) - pass_type['simple_fail_dat-ratio_degF'] = (round(long_tc_ratio,1), round(long_tc['50%'], 2)) + min_temp_diff_cutoff = min(long_tc["50%"]*2, th_bad_vlv) + + ideal_vlv_df = vlv_df.copy(deep=True) + closed_valve = ~ideal_vlv_df['vlv_open'] + below_long_tc = ideal_vlv_df['temp_diff'] < min_temp_diff_cutoff + + adj_delT_at_closed_valve = ideal_vlv_df.loc[np.logical_and(closed_valve, below_long_tc), 'temp_diff'].describe() + diff_vlv_po_th = adj_delT_at_closed_valve["mean"] + adj_delT_at_closed_valve["std"] + + exceed_long_t = vlv_df['temp_diff'] >= diff_vlv_po_th + + th_exceed = np.logical_and(np.logical_and(vlv_df['cons_ts_vlv_c'], vlv_df['steady']), exceed_long_t) + vlv_df["fault_operation"] = th_exceed + df_bad = vlv_df.copy(deep=True) + df_bad = df_bad[th_exceed] + + vlv_df.loc[:, 'good_oper_cat'] = True + if not df_bad.empty: + long_tbad = calc_long_t_diff(df_bad) + long_tbad = long_tbad['50%'] + bad_ratio = 100*(df_bad.shape[0]/vlv_df.shape[0]) + vlv_df.loc[df_bad.index, 'good_oper_cat'] = False + else: + bad_ratio = 0 + long_tbad = None + if bad_ratio > 10 and pd.notnull(long_tbad) and long_tbad > th_bad_vlv/2.0: + print_passing_mgs(row) + pass_type['simple_fail_dat-ratio_faultdegf_gooddegF'] = (round(bad_ratio,1), round(long_tbad,1), round(adj_delT_at_closed_valve['50%'], 2)) folder = join(project_folder, bad_folder) - vlv_df.loc[:, 'good_oper_cat'] = False - long_t = None - long_tbad = long_tc['50%'] + long_t = adj_delT_at_closed_valve['50%'] else: - vlv_df.loc[:, 'good_oper_cat'] = True folder = join(project_folder, good_folder) - long_t = long_tc['50%'] - long_tbad = None + long_t = adj_delT_at_closed_valve['50%'] if log_details: logger.info("[{}] is only closed with good operation data".format(row['vlv'])) _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_t, long_tbad=long_tbad, folder=folder) @@ -1680,20 +1822,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det print_passing_mgs(row) passing_type['tc_to_close_fail'] = round(long_tc_to_diff, 2) - # assume a 0 deg difference at 0% open valve - # and at 95 pct temp diff at > 95% percent open valve - HI_DIFF_PERCENTILE = 95 - no_zeros_po = vlv_df.copy() - no_zeros_po.loc[~no_zeros_po['vlv_open'], 'temp_diff'] = 0 - - hi_diff = np.percentile(vlv_df['temp_diff'], HI_DIFF_PERCENTILE) - no_zeros_po.loc[no_zeros_po['vlv_po'] >= 95, 'temp_diff'] = hi_diff - - # make a logit regression model assuming that closed valves make a zero temp difference - df_fit_nz, popt = build_logistic_model(no_zeros_po) - # calculate bad valve instances vs overall dataframe - fault_vlv, pass_type = find_fault_vlv_operation(vlv_df, row, df_fit_nz, popt, window, th_bad_vlv) + vlv_df, fault_vlv, pass_type, ideal_model, popt = find_fault_vlv_operation(vlv_df, row, long_tc, window, th_bad_vlv) passing_type.update(pass_type) if fault_vlv is None: @@ -1706,8 +1836,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det BAD_RATIO_THRESHOLD = 5 # estimate size of leak in terms of pct that valve is open - if df_fit_nz is not None and long_tbad is not None: - est_leak = df_fit_nz[df_fit_nz['y_fitted'] <= long_tbad]['vlv_po'].max() + if ideal_model is not None and long_tbad is not None: + est_leak = ideal_model[ideal_model['y_fitted'] <= long_tbad]['vlv_po'].max() if est_leak > popt[1] and bad_ratio > BAD_RATIO_THRESHOLD: passing_type['leak_grtr_xovr_fail_vlv-pos'] = est_leak else: @@ -1719,13 +1849,13 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det failure = [x in ['long_term_fail', 'leak_grtr_xovr_fail_vlv-pos', 'leak_grtr_threshold_fail_degF'] for x in passing_type.keys()] if len([x for x in passing_type.keys() if 'sensor_fault' in x]) > 0: folder = join(project_folder, sensor_fault_folder) - elif 'long_term_fail' in passing_type.keys(): + elif 'long_term_fail' in passing_type.keys() and passing_type['long_term_fail'][1] >= 2 and pd.notnull(long_tbad) and long_tbad > th_bad_vlv/2.0: print_passing_mgs(row) folder = join(project_folder, bad_folder) - elif any(failure) and (bad_ratio > BAD_RATIO_THRESHOLD): + elif any(failure) and (bad_ratio > BAD_RATIO_THRESHOLD) and pd.notnull(long_tbad) and long_tbad > th_bad_vlv/2.0: print_passing_mgs(row) folder = join(project_folder, bad_folder) - elif 'simple_fail_dat-ratio_degF' in passing_type.keys() and (long_tc_ratio > BAD_RATIO_THRESHOLD): + elif 'simple_fail_dat-ratio_degF' in passing_type.keys() and (long_tc_ratio > BAD_RATIO_THRESHOLD) and pd.notnull(long_tbad) and long_tbad > th_bad_vlv/2.0: print_passing_mgs(row) folder = join(project_folder, bad_folder) else: @@ -1736,7 +1866,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if fault_vlv is not None: vlv_df.loc[fault_vlv.index, 'good_oper_cat'] = False - _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], long_tbad=long_tbad, df_fit=df_fit_nz, bad_ratio=bad_ratio, folder=folder) + _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=long_tc['50%'], long_tbad=long_tbad, df_fit=ideal_model, bad_ratio=bad_ratio, folder=folder) pass_type['folder'] = folder row.update({'long_t': round(long_tc['50%'], 2), 'long_tbad': None if long_tbad is None else round(long_tbad, 2), 'bad_ratio': round(bad_ratio, 2), 'folder': folder}) From 000a5a7e7594d8308822438ae354c85ba1f76182 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Thu, 10 Mar 2022 13:37:20 -0800 Subject: [PATCH 77/83] script to analyze results in aggregate --- detect_passing_valves/aggregate_results.py | 102 +++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 detect_passing_valves/aggregate_results.py diff --git a/detect_passing_valves/aggregate_results.py b/detect_passing_valves/aggregate_results.py new file mode 100644 index 0000000..c8e6a0c --- /dev/null +++ b/detect_passing_valves/aggregate_results.py @@ -0,0 +1,102 @@ +from os.path import join +import os +from unicodedata import numeric +import pandas as pd +import numpy as np + +def parse_dict_list_file(line): + + dictionary = dict() + pairs = line.strip().strip(",").strip('{}').split(', ') + for pr in pairs: + pair = pr.split(': ') + dictionary[pair[0].strip('\'\'\"\"')] = pair[1].strip('\'\'\"\"') + + return dictionary + +if __name__ == '__main__': + dat_folder = join('with_airflow_checks_year_start', 'csv_data') + project_folder = join('./', 'external_analysis') + + mortar_dat_file = join(project_folder, 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') + bear_dat_file = join(project_folder, 'bldg_trc_rs', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') + lion_dat_file = join(project_folder, 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') + + all_datasets = [] + for dataset in [mortar_dat_file, bear_dat_file, lion_dat_file]: + # Perform additional analysis + f = open(join(dataset, 'minimum_airflow_values.txt'), 'r') + lines = f.readlines() + f.close() + + vav_results = [] + for line in lines: + vav_results.append(parse_dict_list_file(line)) + + vav_results = pd.DataFrame.from_records(vav_results) + all_datasets.append(vav_results) + + all_datasets = pd.concat(all_datasets).reset_index(drop=True) + + # clean dataframe + numeric_cols = ['minimum_air_flow_cutoff', 'long_t', 'long_tbad', 'bad_ratio', 'long_to'] + avail_cols = list(set(all_datasets.columns).intersection(set(numeric_cols))) + all_datasets[avail_cols] = all_datasets[avail_cols].apply(pd.to_numeric, errors='coerce') + + all_datasets = all_datasets.dropna(subset=['folder']) + + all_datasets['folder_short'] = all_datasets['folder'].apply(os.path.basename) + + + # add rest of info including heat loss due to passing valves + all_passing_valve_results = [] + for dataset in [mortar_dat_file, bear_dat_file, lion_dat_file]: + final_df = pd.read_csv(join(dataset, 'passing_valve_results.csv'), index_col=0) + all_passing_valve_results.append(final_df) + + all_passing_valve_results = pd.concat(all_passing_valve_results).reset_index(drop=True) + + all_datasets = pd.merge(all_datasets, all_passing_valve_results, how='left', on=['vlv', 'site', 'equip']) + + # separate heat rate loss from energy loss + heat_loss_pwer = [] + heat_loss_enrg = [] + for hl in all_datasets['heat_loss_pwr-avg_nrgy-sum']: + if pd.notnull(hl): + hl = hl.strip('()').split(", ") + heat_loss_pwer.append(float(hl[0])) + heat_loss_enrg.append(float(hl[1])) + else: + heat_loss_pwer.append(np.nan) + heat_loss_enrg.append(np.nan) + + all_datasets['heat_loss_pwr'] = heat_loss_pwer + all_datasets['heat_loss_enrg'] = heat_loss_enrg + + + # summary statistics on long term difference for each site + site_grp = all_datasets.groupby(['site']) + vav_results_grp = all_datasets.groupby(['site', 'folder_short']) + grp_dat_stats = vav_results_grp['long_t'].describe() + + # summary statistics on long term difference for all data + agg_dat_grp = all_datasets.groupby(['folder_short']) + agg_dat_stats = agg_dat_grp['long_t'].describe() + + + # summary statistics on heat loss due to passing valves + + # on aggregate + agg_dat_grp["long_term_fail_num_times_detected"].describe() + agg_dat_grp["long_term_fail_avg_minutes"].describe() + + agg_dat_grp["short_term_fail_num_times_detected"].describe() + agg_dat_grp["short_term_fail_avg_minutes"].describe() + + # by site + from_btuhr_to_watts = 0.293071 + site_grp["heat_loss_pwr"].describe()*from_btuhr_to_watts + all_datasets["heat_loss_pwr"].describe()*from_btuhr_to_watts + + + From 16e1006c56bc9246010164bc6dcef4647ecb3fbe Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 14 Mar 2022 09:20:18 -0700 Subject: [PATCH 78/83] output vlv_df and fixed consecutive timestamp bug --- detect_passing_valves/app.py | 55 ++++++++++++++----- detect_passing_valves/ext_dat_app_run.py | 3 +- detect_passing_valves/ext_dat_app_run_gt.py | 3 +- .../ext_data_app_run_mortar.py | 3 +- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index 785a2c2..b419eb6 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -901,6 +901,7 @@ def calc_heat_transfer(df, long_term_temp_diff=None, window=None): heat_power_xfr = 60*RHO_AIR*Cp_AIR*air_flow*delta_temp # [Btu/hr] heat_energy_xfr = heat_power_xfr*window_int # [Btu] else: + print("No air flow!") return None return (round(np.mean(heat_power_xfr), 1), round(sum(heat_energy_xfr), 1)) @@ -928,7 +929,7 @@ def check_consecutive_timestamps(df, window=None): window = determine_timestamp_interval(df) ts = pd.Series(df.index) - cons_ts = ((ts - ts.shift(-1)).abs() <= window) | (ts.diff() <= window) + cons_ts = ((ts.shift(-1) - ts) <= window) return list(cons_ts) @@ -1461,13 +1462,21 @@ def categorize_fault_operation(vlv_df, min_temp_diff_cutoff, iter=1, row=row): # vlv_df, df_bad, return vlv_df, df_bad, pass_type, model, popt + # verify that temp diff is greater than zero + # check that timestamps are consecutive in df_bad df_bad_cons_ts = check_consecutive_timestamps(df_bad, window) - df_bad['cons_ts_fault_vlv'] = df_bad_cons_ts + + if "minimum_air_flow_cutoff" in row.keys(): + above_airflow = df_bad["air_flow"] > row["minimum_air_flow_cutoff"] + df_bad['cons_ts_fault_vlv'] = np.logical_and(df_bad_cons_ts, above_airflow) + else: + df_bad['cons_ts_fault_vlv'] = df_bad_cons_ts # analyze 'bad' dataframe for possible passing valve - bad_grp = df_bad.groupby(['same', 'cons_ts_fault_vlv']) - bad_grp_count = bad_grp['same'].count() + df_bad.loc[:, 'same_fault'] = df_bad['cons_ts_fault_vlv'].astype(int).diff().ne(0).cumsum() + bad_grp = df_bad.groupby(['same_fault', 'cons_ts_fault_vlv']) + bad_grp_count = bad_grp['same_fault'].count() idx_const_vals = bad_grp_count.index.get_level_values("cons_ts_fault_vlv").values if not any(idx_const_vals): @@ -1489,7 +1498,7 @@ def categorize_fault_operation(vlv_df, min_temp_diff_cutoff, iter=1, row=row): long_term_fail_bool = (bad_grp_count_cons_ts*window_int) > long_term_fail if any(long_term_fail_bool): long_term_fail_times = bad_grp_count_cons_ts[long_term_fail_bool]*window_int - if long_term_fail_times.count() >= 1 or long_term_fail_times.index[-1] == vlv_df['same'].max(): + if long_term_fail_times.count() >= 1 or long_term_fail_times.index[-1] == vlv_df['same_fault'].max(): long_fault_idx = long_term_fail_times.index.values long_all_dates = [bad_grp.groups[(ky, True)] for ky in long_fault_idx] dates = [(ky[0], ky[-1]) for ky in long_all_dates] @@ -1523,7 +1532,21 @@ def categorize_fault_operation(vlv_df, min_temp_diff_cutoff, iter=1, row=row): fault_idx.append(fault_ts) fault_points = df_bad.index.isin(fault_idx) - pass_type['heat_loss_pwr-avg_nrgy-sum'] = calc_heat_transfer(df_bad.loc[fault_points], adj_delT_at_closed_valve["50%"], window) + + df_heat_loss = df_bad.loc[fault_points] + df_heat_intended = vlv_df.loc[np.logical_and(~vlv_df.index.isin(df_heat_loss.index), vlv_df["vlv_open"])] + pass_type['heat_loss_pwr-avg_nrgy-sum'] = calc_heat_transfer(df_heat_loss, adj_delT_at_closed_valve["50%"], window) + pass_type['heat_intentional_pwr-avg_nrgy-sum'] = calc_heat_transfer(df_heat_intended, adj_delT_at_closed_valve["50%"], window) + + # update fault operation + df_bad["fault_operation"] = False + df_bad.loc[fault_idx, "fault_operation"] = True + + vlv_df["fault_operation"] = False + vlv_df["df_bad"] = False + + vlv_df.loc[fault_idx, "fault_operation"] = True + vlv_df.loc[df_bad.index, "df_bad"] = True return vlv_df, df_bad, pass_type, model, popt @@ -1745,7 +1768,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if log_details: logger.info(message) row.update({'output_details': 'no data after calc features'}) if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) - return passing_type + return vlv_df, passing_type # Analyze timestamps and valve operation changes window = determine_timestamp_interval(vlv_df) @@ -1757,7 +1780,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det consecutive timestamps! Skipping...".format(row['vlv'], row['site'])) row.update({'output_details': 'no data after timestamp analysis'}) if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) - return passing_type + return vlv_df, passing_type # check that sensors are not reporting constant numbers @@ -1770,6 +1793,8 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if 'air_flow' in vlv_df.columns: # plot temp diff vs air flow _make_tdiff_vs_aflow_plot(vlv_df, row, folder=join(project_folder, 'air_flow_plots'), af_accu_factor=af_accu_factor) + else: + row.update({'airflow': 'False'}) # drop data that occurs during unoccupied hours vlv_df, row = drop_unoccupied_dat(vlv_df, row=row, occ_str=6, occ_end=18, wkend_str=5, air_flow_required=air_flow_required, af_accu_factor=af_accu_factor) @@ -1779,7 +1804,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det occupancy check! Skipping...".format(row['vlv'], row['site'])) row.update({'output_details': 'no data after occupancy check'}) if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) - return passing_type + return vlv_df, passing_type # determine if valve datastream has open and closed data bool_type = vlv_df['vlv_open'].value_counts().index @@ -1794,7 +1819,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det passing_type, row = analyze_only_close(vlv_df, row, th_bad_vlv, project_folder) row.update({'output_details': 'only closed valve data'}) if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) - return passing_type + return vlv_df, passing_type # TODO: Figure out what to do if long_tc is None! # calculate long-term temp diff when valve is closed @@ -1808,7 +1833,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det passing_type.update(pass_type) row.update({'output_details': 'no consecutive timestamps and steady state conditions when valve is closed'}) if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) - return passing_type + return vlv_df, passing_type # make simple comparison of long-term closed temp difference and user define threshold if long_tc['50%'] > th_bad_vlv: @@ -1830,7 +1855,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det bad_ratio = 0 long_tbad = None# long_tc['50%'] else: - bad_ratio = 100*(fault_vlv.shape[0]/vlv_df.shape[0]) + bad_ratio = 100*(fault_vlv[fault_vlv["fault_operation"]].shape[0]/vlv_df.shape[0]) long_tbad = fault_vlv['temp_diff'].describe()['50%'] BAD_RATIO_THRESHOLD = 5 @@ -1872,7 +1897,7 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) - return passing_type + return vlv_df, passing_type def log_row_details(row, outfolder): @@ -1911,7 +1936,7 @@ def _analyze_ahu(vlv_df, row, th_bad_vlv, th_time, project_folder): else: passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv, th_time, project_folder) - return passing_type + return vlv_df, passing_type def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time, project_folder): @@ -1949,7 +1974,7 @@ def _analyze(metadata, fetch_resp, clean_func, analyze_func, th_bad_vlv, th_time vlv_df = clean_func(fetch_resp, row) # analyze for passing valves - passing_type = analyze_func(vlv_df, row, th_bad_vlv, th_time, project_folder) + vlv_df, passing_type = analyze_func(vlv_df, row, th_bad_vlv, th_time, project_folder) except: import traceback diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index 9e44f37..46b9aab 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -209,7 +209,8 @@ def parse_dict_list_file(line): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) + vlv_df, passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) + vavs_df[key]['vlv_dat'] = vlv_df # save results vlv_dat.update(passing_type) diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index da7b8ae..4f8a7c4 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -249,7 +249,8 @@ def parse_dict_list_file(line): # define variables vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) + vlv_df, passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) + vavs_df[key]['vlv_dat'] = vlv_df # save results vlv_dat.update(passing_type) diff --git a/detect_passing_valves/ext_data_app_run_mortar.py b/detect_passing_valves/ext_data_app_run_mortar.py index 9242e7a..9c6dee7 100644 --- a/detect_passing_valves/ext_data_app_run_mortar.py +++ b/detect_passing_valves/ext_data_app_run_mortar.py @@ -127,7 +127,8 @@ def parse_dict_list_file(line): vlv_dat = dict(row) # run passing valve detection algorithm - passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) + vlv_df, passing_type = _analyze_vlv(vlv_df, row, th_bad_vlv=TH_BAD_VLV, th_time=12, project_folder=project_folder, detection_params=detection_params) + vavs_df[key]['vlv_dat'] = vlv_df # save results vlv_dat.update(passing_type) From f594326b00611226b741dd1f5c3195c6efa1dab6 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 15 Mar 2022 18:17:53 -0700 Subject: [PATCH 79/83] udpated analysis script --- detect_passing_valves/aggregate_results.py | 75 ++++++++++++++++------ detect_passing_valves/app.py | 1 - 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/detect_passing_valves/aggregate_results.py b/detect_passing_valves/aggregate_results.py index c8e6a0c..12485a5 100644 --- a/detect_passing_valves/aggregate_results.py +++ b/detect_passing_valves/aggregate_results.py @@ -1,6 +1,5 @@ from os.path import join import os -from unicodedata import numeric import pandas as pd import numpy as np @@ -14,6 +13,20 @@ def parse_dict_list_file(line): return dictionary +def separate_heat_transfer_col(df, col): + heat_tfr_pwer = [] + heat_tfr_enrg = [] + for hl in df[col]: + if pd.notnull(hl): + hl = hl.strip('()').split(", ") + heat_tfr_pwer.append(float(hl[0])) + heat_tfr_enrg.append(float(hl[1])) + else: + heat_tfr_pwer.append(np.nan) + heat_tfr_enrg.append(np.nan) + + return heat_tfr_pwer, heat_tfr_enrg + if __name__ == '__main__': dat_folder = join('with_airflow_checks_year_start', 'csv_data') project_folder = join('./', 'external_analysis') @@ -59,25 +72,24 @@ def parse_dict_list_file(line): all_datasets = pd.merge(all_datasets, all_passing_valve_results, how='left', on=['vlv', 'site', 'equip']) # separate heat rate loss from energy loss - heat_loss_pwer = [] - heat_loss_enrg = [] - for hl in all_datasets['heat_loss_pwr-avg_nrgy-sum']: - if pd.notnull(hl): - hl = hl.strip('()').split(", ") - heat_loss_pwer.append(float(hl[0])) - heat_loss_enrg.append(float(hl[1])) - else: - heat_loss_pwer.append(np.nan) - heat_loss_enrg.append(np.nan) - + heat_loss_pwer, heat_loss_enrg = separate_heat_transfer_col(df=all_datasets, col='heat_loss_pwr-avg_nrgy-sum') + all_datasets['heat_loss_pwr'] = heat_loss_pwer all_datasets['heat_loss_enrg'] = heat_loss_enrg + # separate intentional heat rate + heat_intend_pwer, heat_intend_enrg = separate_heat_transfer_col(df=all_datasets, col='heat_intentional_pwr-avg_nrgy-sum') + + all_datasets['heat_intend_pwr'] = heat_intend_pwer + all_datasets['heat_intend_enrg'] = heat_intend_enrg + # summary statistics on long term difference for each site site_grp = all_datasets.groupby(['site']) + folder_grp = all_datasets.groupby(['folder_short']) vav_results_grp = all_datasets.groupby(['site', 'folder_short']) grp_dat_stats = vav_results_grp['long_t'].describe() + folder_grp['long_t'].describe() # summary statistics on long term difference for all data agg_dat_grp = all_datasets.groupby(['folder_short']) @@ -85,18 +97,45 @@ def parse_dict_list_file(line): # summary statistics on heat loss due to passing valves - + no_sensor_faults = all_datasets.loc[all_datasets["folder_short"] != "sensor_fault"] + folder_grp_nsf = no_sensor_faults.groupby(['folder_short']) + heat_loss = folder_grp_nsf["heat_loss_enrg"].sum().sum() + heat_intent = folder_grp_nsf["heat_intend_enrg"].sum().sum() + + heat_loss_ratio = heat_loss/heat_intent + # on aggregate - agg_dat_grp["long_term_fail_num_times_detected"].describe() - agg_dat_grp["long_term_fail_avg_minutes"].describe() + no_sensor_faults["long_term_fail_num_times_detected"].describe() + no_sensor_faults["long_term_fail_avg_minutes"].describe() - agg_dat_grp["short_term_fail_num_times_detected"].describe() - agg_dat_grp["short_term_fail_avg_minutes"].describe() + no_sensor_faults["short_term_fail_num_times_detected"].describe() + no_sensor_faults["short_term_fail_avg_minutes"].describe() + + no_sensor_faults["heat_loss_pwr"].describe() + no_sensor_faults["heat_loss_enrg"].sum()/1000 # by site from_btuhr_to_watts = 0.293071 - site_grp["heat_loss_pwr"].describe()*from_btuhr_to_watts + site_grp_nsf = no_sensor_faults.groupby(['site']) + + site_heat_loss = site_grp_nsf["heat_loss_enrg"].sum() + site_heat_intend = site_grp_nsf["heat_intend_enrg"].sum() + + site_heat_loss/site_heat_intend + + # by fault category + site_grp_nsf["heat_loss_pwr"].describe()*from_btuhr_to_watts + + # all + all_datasets.loc[all_datasets["folder_short"] == "bad_valves", "heat_loss_enrg"] + all_datasets.loc[:,"heat_intend_enrg"] + all_datasets["heat_loss_pwr"].describe()*from_btuhr_to_watts + subset_lion = np.logical_and(all_datasets["site"] == "lion", all_datasets["folder_short"] == "bad_valves") + df_bad_lion = all_datasets.loc[subset_lion] + + df_bad_lion["loss_pct"] = df_bad_lion["heat_loss_enrg"]/df_bad_lion["heat_intend_enrg"] + df_bad_lion.sort_values("loss_pct", ascending=False) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index b419eb6..b75c6fb 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1782,7 +1782,6 @@ def _analyze_vlv(vlv_df, row, th_bad_vlv=5, th_time=45, project_folder='./', det if log_rows_info: log_row_details(row, join(project_folder, 'minimum_airflow_values.txt')) return vlv_df, passing_type - # check that sensors are not reporting constant numbers vlv_df, passing_type = fault_sensor_inactivity(vlv_df, passing_type) From c3f1f8b5e250dbc8ba66b70e109aa0f324bd612f Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 15 Mar 2022 18:18:30 -0700 Subject: [PATCH 80/83] lowered threshold to avoid more false positive in sensor faults --- detect_passing_valves/fault_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/fault_tests.py b/detect_passing_valves/fault_tests.py index 28b751e..3eb55ab 100644 --- a/detect_passing_valves/fault_tests.py +++ b/detect_passing_valves/fault_tests.py @@ -10,7 +10,7 @@ def fault_sensor_inactivity(vlv_df, passing_type): Check that sensors are taking measurements and not just reporting a constant number """ - pct_threshold = 1.5 + pct_threshold = 0.75 analysis_cols = ['upstream_ta', 'dnstream_ta', 'air_flow'] df_cols = vlv_df.columns @@ -26,8 +26,8 @@ def fault_sensor_inactivity(vlv_df, passing_type): passing_type["sensor_fault_CONSTANT_{}".format(col)] = (col_min, col_max) return vlv_df, passing_type - col_dat = (col_dat-col_min)/(col_max-col_min) - col_stats = abs(col_dat.diff(periods=-1).loc[vlv_df['cons_ts']]).describe() + col_dat_norm = (col_dat-col_min)/(col_max-col_min) + col_stats = abs(col_dat_norm.diff(periods=-1).loc[vlv_df['cons_ts']]).describe() try: if round(col_stats["std"]*100,1) < pct_threshold: From aa826cffb853e78777e16eb7433afae72b05d46d Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 13 Jun 2022 18:40:28 -0700 Subject: [PATCH 81/83] modify plot size and increase font size --- detect_passing_valves/app.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index b75c6fb..e6eafe9 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1130,10 +1130,12 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, long_to= bad_oper_color = '#b3005a' # plot temperature difference vs valve position - fig, ax = plt.subplots(figsize=(8,4.5)) - ax.set_ylabel('Temperature difference [°F]') - ax.set_xlabel('Valve opened [%]') - ax.set_title("Site = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left') + fig, ax = plt.subplots(figsize=(8,4.25)) + plt.xticks(fontsize=12) + plt.yticks(fontsize=12) + ax.set_ylabel('Temperature difference [°F]', fontsize=12) + ax.set_xlabel('Valve opened [%]', fontsize=12) + ax.set_title("Site = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left', fontsize=12) ax.set_ylim((min(-2, y_min)*1.02, np.ceil(y_max*1.05))) ax.set_xlim((min(-2, x_min)*1.02, max(100, x_max)*1.02)) @@ -1169,8 +1171,9 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, long_to= ax.text(.25, 0.98*y_max, "Fault operation proportion={:.1f}%".format(bad_ratio)) # legend - # ax.legend(fontsize=8, markerscale=1, borderaxespad=0., ncol=2, loc='upper right', bbox_to_anchor=(0.15, 1.05, 1., .102)) - ax.legend(fontsize=6, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.48, 1.02), loc='lower left') + #ax.legend(bbox_to_anchor=(-0.15, -0.44), fontsize=10, markerscale=1, ncol=3, loc='lower left', frameon=False, handletextpad=0.15) + ax.legend(bbox_to_anchor=(0.48, -0.48), fontsize=10, markerscale=1, ncol=2, loc='lower center', frameon=False, handletextpad=0.15) + fig.subplots_adjust(bottom=0.27) plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) @@ -1224,10 +1227,12 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder, af_accu_factor=None): vlv_df.loc[vlv_df['vlv_open'], 'color_open'] = open_vlv_color # plot temperature difference vs valve position - fig, ax = plt.subplots(figsize=(8,4.5)) - ax.set_ylabel('Temperature difference [°F]') - ax.set_xlabel('Air flow [cfm]') - ax.set_title("Site = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left') + fig, ax = plt.subplots(figsize=(8,4.25)) + plt.xticks(fontsize=12) + plt.yticks(fontsize=12) + ax.set_ylabel('Temperature difference [°F]', fontsize=12) + ax.set_xlabel('Air flow [cfm]', fontsize=12) + ax.set_title("Site = {}\nEquip. = {}".format(row['site'], row['equip']), loc='left', fontsize=12) if any(~vlv_df['vlv_open']): ax.scatter(x=vlv_df.loc[~vlv_df['vlv_open'], 'air_flow'], y=vlv_df.loc[~vlv_df['vlv_open'], 'temp_diff'], color = closed_vlv_color, alpha=1/3, s=10, label='Closed valve') @@ -1256,7 +1261,9 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder, af_accu_factor=None): ax.scatter(x=xs[min_idx], y=ys[min_idx], color = '#ff8000', alpha=1, s=35, label='Troughs') # Legend - ax.legend(fontsize=6, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.50, 1.02), loc='lower left') + # ax.legend(fontsize=10, markerscale=1, borderaxespad=0., ncol=2, bbox_to_anchor=(.50, 1.02), loc='lower left') + ax.legend(bbox_to_anchor=(0.48, -0.40), fontsize=10, markerscale=1, ncol=3, loc='lower center', frameon=False, handletextpad=0.15) + fig.subplots_adjust(bottom=0.25) plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) From caab670cba34b318642e2c773b8d618bcf77315f Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Mon, 13 Jun 2022 18:41:18 -0700 Subject: [PATCH 82/83] 2nd draft review changes --- detect_passing_valves/ext_dat_app_run.py | 2 +- detect_passing_valves/ext_dat_app_run_gt.py | 2 +- detect_passing_valves/ext_data_app_run_mortar.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/ext_dat_app_run.py b/detect_passing_valves/ext_dat_app_run.py index 46b9aab..ad01589 100644 --- a/detect_passing_valves/ext_dat_app_run.py +++ b/detect_passing_valves/ext_dat_app_run.py @@ -146,7 +146,7 @@ def parse_dict_list_file(line): if __name__ == '__main__': # NOTE: HW plant off from August 31 to October 7 2021 dat_folder = join('./', 'external_data', 'bldg_trc_rs') - project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') + project_folder = join('./', 'external_analysis', 'bldg_trc_rs', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold_2nd_draft_revisit') csv_list = [ join(dat_folder, 'zone trends, September 2021.csv'), diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index 4f8a7c4..ffe3a07 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -187,7 +187,7 @@ def parse_dict_list_file(line): if __name__ == '__main__': dat_folder = join('./', 'external_data', 'bldg_gt_pr', '20211118') - project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') + project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold_2nd_draft_revisit') # read files discharge_temp_file = join(dat_folder, 'B44-B45 Discharge Air Temp Sensor Readings - 01MAY2021 to 10NOV2021.csv') diff --git a/detect_passing_valves/ext_data_app_run_mortar.py b/detect_passing_valves/ext_data_app_run_mortar.py index 9c6dee7..4214387 100644 --- a/detect_passing_valves/ext_data_app_run_mortar.py +++ b/detect_passing_valves/ext_data_app_run_mortar.py @@ -73,7 +73,7 @@ def parse_dict_list_file(line): if __name__ == '__main__': dat_folder = join('with_airflow_checks_year_start', 'csv_data') - project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold') + project_folder = join('./', 'external_analysis', 'MORTAR', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold_2nd_draft_revisit') # define container folders good_folder = 'good_valves' # name of path to the folder to save the plots of the correct operating valves From f307c2c750e21316a2d93704edfd25c71dc27057 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Date: Tue, 30 Aug 2022 11:31:50 -0700 Subject: [PATCH 83/83] improved plot quality to 600dpi --- detect_passing_valves/app.py | 4 ++-- detect_passing_valves/ext_dat_app_run_gt.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/detect_passing_valves/app.py b/detect_passing_valves/app.py index e6eafe9..50543d8 100644 --- a/detect_passing_valves/app.py +++ b/detect_passing_valves/app.py @@ -1177,7 +1177,7 @@ def _make_tdiff_vs_vlvpo_plot(vlv_df, row, long_t=None, long_tbad=None, long_to= plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) - plt.savefig(full_path) + plt.savefig(full_path, dpi=600) plt.close() @@ -1267,7 +1267,7 @@ def _make_tdiff_vs_aflow_plot(vlv_df, row, folder, af_accu_factor=None): plt_name = "{}-{}-{}".format(row['site'], row['equip'], row['vlv']) full_path = rename_existing(join(folder, plt_name + '.png'), idx=0, row=row) - plt.savefig(full_path) + plt.savefig(full_path, dpi=600) plt.close() diff --git a/detect_passing_valves/ext_dat_app_run_gt.py b/detect_passing_valves/ext_dat_app_run_gt.py index ffe3a07..6a925db 100644 --- a/detect_passing_valves/ext_dat_app_run_gt.py +++ b/detect_passing_valves/ext_dat_app_run_gt.py @@ -187,7 +187,7 @@ def parse_dict_list_file(line): if __name__ == '__main__': dat_folder = join('./', 'external_data', 'bldg_gt_pr', '20211118') - project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold_2nd_draft_revisit') + project_folder = join('./', 'external_analysis', 'bldg_gt_pr', 'lg_4hr_shrt_1hr_test_no_aflw_req_10C_threshold_2nd_draft_revisit_hiquality_plots') # read files discharge_temp_file = join(dat_folder, 'B44-B45 Discharge Air Temp Sensor Readings - 01MAY2021 to 10NOV2021.csv')