ecopipeline 1.0.5__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecopipeline/event_tracking/Alarm.py +317 -0
- ecopipeline/event_tracking/__init__.py +18 -1
- ecopipeline/event_tracking/alarms/AbnormalCOP.py +76 -0
- ecopipeline/event_tracking/alarms/BackupUse.py +94 -0
- ecopipeline/event_tracking/alarms/BalancingValve.py +78 -0
- ecopipeline/event_tracking/alarms/BlownFuse.py +72 -0
- ecopipeline/event_tracking/alarms/Boundary.py +90 -0
- ecopipeline/event_tracking/alarms/HPWHInlet.py +73 -0
- ecopipeline/event_tracking/alarms/HPWHOutage.py +96 -0
- ecopipeline/event_tracking/alarms/HPWHOutlet.py +85 -0
- ecopipeline/event_tracking/alarms/LSInconsist.py +114 -0
- ecopipeline/event_tracking/alarms/PowerRatio.py +111 -0
- ecopipeline/event_tracking/alarms/SOOChange.py +127 -0
- ecopipeline/event_tracking/alarms/ShortCycle.py +59 -0
- ecopipeline/event_tracking/alarms/TMSetpoint.py +127 -0
- ecopipeline/event_tracking/alarms/TempRange.py +84 -0
- ecopipeline/event_tracking/alarms/__init__.py +0 -0
- ecopipeline/event_tracking/event_tracking.py +119 -1177
- ecopipeline/load/__init__.py +2 -2
- ecopipeline/load/load.py +304 -3
- {ecopipeline-1.0.5.dist-info → ecopipeline-1.1.0.dist-info}/METADATA +1 -1
- ecopipeline-1.1.0.dist-info/RECORD +41 -0
- {ecopipeline-1.0.5.dist-info → ecopipeline-1.1.0.dist-info}/WHEEL +1 -1
- ecopipeline-1.0.5.dist-info/RECORD +0 -25
- {ecopipeline-1.0.5.dist-info → ecopipeline-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {ecopipeline-1.0.5.dist-info → ecopipeline-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import datetime as datetime
|
|
4
|
+
from ecopipeline import ConfigManager
|
|
5
|
+
import re
|
|
6
|
+
import mysql.connector.errors as mysqlerrors
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from ecopipeline.event_tracking.Alarm import Alarm
|
|
9
|
+
|
|
10
|
+
class SOOChange(Alarm):
|
|
11
|
+
"""
|
|
12
|
+
Detects unexpected state of operation (SOO) changes by checking if the heat pump turns on or off
|
|
13
|
+
when the temperature is not near the expected aquastat setpoint thresholds. An alarm is triggered
|
|
14
|
+
if the HP turns on/off and the corresponding temperature is more than 5.0 degrees away from the
|
|
15
|
+
expected threshold.
|
|
16
|
+
|
|
17
|
+
VarNames syntax:
|
|
18
|
+
SOOCHNG_POW:### - Indicates a power variable for the heat pump system (should be total power across all primary heat pumps). ### is the power threshold (default 1.0) above which
|
|
19
|
+
the heat pump system is considered 'on'.
|
|
20
|
+
SOOCHNG_ON_[Mode ID]:### - Indicates the temperature variable at the ON aquastat fraction. ### is the temperature (default 115.0)
|
|
21
|
+
that should trigger the heat pump to turn ON. Mode ID should be the load up mode from ['loadUp','shed','criticalPeak','gridEmergency','advLoadUp','normal'] or left blank for normal mode
|
|
22
|
+
SOOCHNG_OFF_[Mode ID]:### - Indicates the temperature variable at the OFF aquastat fraction (can be same as ON aquastat). ### is the temperature (default 140.0)
|
|
23
|
+
that should trigger the heat pump to turn OFF. Mode ID should be the load up mode from ['loadUp','shed','criticalPeak','gridEmergency','advLoadUp','normal'] or left blank for normal mode
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
default_power_threshold : float
|
|
28
|
+
Default power threshold for POW alarm codes when no custom bound is specified (default 1.0). Heat pump is considered 'on'
|
|
29
|
+
when power exceeds this value.
|
|
30
|
+
default_on_temp : float
|
|
31
|
+
Default ON temperature threshold (default 115.0). When the HP turns on, an alarm triggers if the temperature
|
|
32
|
+
is more than 5.0 degrees away from this value.
|
|
33
|
+
default_off_temp : float
|
|
34
|
+
Default OFF temperature threshold (default 140.0). When the HP turns off, an alarm triggers if the temperature
|
|
35
|
+
is more than 5.0 degrees away from this value.
|
|
36
|
+
"""
|
|
37
|
+
def __init__(self, bounds_df : pd.DataFrame, default_power_threshold : float = 1.0, default_on_temp : float = 115.0, default_off_temp : float = 140.0):
|
|
38
|
+
alarm_tag = 'SOO'
|
|
39
|
+
self.default_power_threshold = default_power_threshold
|
|
40
|
+
self.default_on_temp = default_on_temp
|
|
41
|
+
self.default_off_temp = default_off_temp
|
|
42
|
+
self.soo_dict = {
|
|
43
|
+
'loadUp' : 'LOAD UP',
|
|
44
|
+
'shed' : 'SHED',
|
|
45
|
+
'criticalPeak': 'CRITICAL PEAK',
|
|
46
|
+
'gridEmergency' : 'GRID EMERGENCY',
|
|
47
|
+
'advLoadUp' : 'ADVANCED LOAD UP'
|
|
48
|
+
}
|
|
49
|
+
type_default_dict = {
|
|
50
|
+
'POW' : default_power_threshold,
|
|
51
|
+
'ON' : default_on_temp,
|
|
52
|
+
'OFF' : default_off_temp
|
|
53
|
+
}
|
|
54
|
+
super().__init__(bounds_df, alarm_tag, type_default_dict, alarm_db_type='SOO_CHANGE')
|
|
55
|
+
|
|
56
|
+
def specific_alarm_function(self, df: pd.DataFrame, daily_df : pd.DataFrame, config : ConfigManager):
|
|
57
|
+
ls_df = config.get_ls_df()
|
|
58
|
+
pow_codes = self.bounds_df[self.bounds_df['alarm_code_type'] == 'POW']
|
|
59
|
+
if len(pow_codes) != 1:
|
|
60
|
+
raise Exception(f"Improper alarm codes for SOO changes; must have 1 POW variable to indicate power to HPWH(s).")
|
|
61
|
+
pow_var_name = pow_codes.iloc[0]['variable_name']
|
|
62
|
+
pow_thresh = pow_codes.iloc[0]['bound']
|
|
63
|
+
non_pow_bounds_df = self.bounds_df[self.bounds_df['alarm_code_type'] != 'POW']
|
|
64
|
+
|
|
65
|
+
for alarm_id in non_pow_bounds_df['alarm_code_id'].unique():
|
|
66
|
+
ls_filtered_df = df.copy()
|
|
67
|
+
soo_mode_name = 'NORMAL'
|
|
68
|
+
if alarm_id in self.soo_dict.keys():
|
|
69
|
+
if not ls_df.empty:
|
|
70
|
+
# Filter ls_filtered_df for only date ranges in the right mode of ls_df
|
|
71
|
+
mode_rows = ls_df[ls_df['event'] == alarm_id]
|
|
72
|
+
mask = pd.Series(False, index=ls_filtered_df.index)
|
|
73
|
+
for _, row in mode_rows.iterrows():
|
|
74
|
+
mask |= (ls_filtered_df.index >= row['startDateTime']) & (ls_filtered_df.index < row['endDateTime'])
|
|
75
|
+
ls_filtered_df = ls_filtered_df[mask]
|
|
76
|
+
soo_mode_name = self.soo_dict[alarm_id]
|
|
77
|
+
else:
|
|
78
|
+
print(f"Cannot check for {alarm_id} because there are no {alarm_id} periods in time frame.")
|
|
79
|
+
continue
|
|
80
|
+
elif not ls_df.empty:
|
|
81
|
+
# Filter out all date range rows from ls_filtered_df's indexes
|
|
82
|
+
mask = pd.Series(True, index=ls_filtered_df.index)
|
|
83
|
+
for _, row in ls_df.iterrows():
|
|
84
|
+
mask &= ~((ls_filtered_df.index >= row['startDateTime']) & (ls_filtered_df.index < row['endDateTime']))
|
|
85
|
+
ls_filtered_df = ls_filtered_df[mask]
|
|
86
|
+
|
|
87
|
+
for day in daily_df.index:
|
|
88
|
+
next_day = day + pd.Timedelta(days=1)
|
|
89
|
+
filtered_df = ls_filtered_df.loc[(ls_filtered_df.index >= day) & (ls_filtered_df.index < next_day)]
|
|
90
|
+
id_group = non_pow_bounds_df[non_pow_bounds_df['alarm_code_id'] == alarm_id]
|
|
91
|
+
on_t_codes = id_group[id_group['alarm_code_type'] == 'ON']
|
|
92
|
+
off_t_codes = id_group[id_group['alarm_code_type'] == 'OFF']
|
|
93
|
+
if len(on_t_codes) != 1 or len(off_t_codes) != 1:
|
|
94
|
+
raise Exception(f"Improper alarm codes for SOO changes with id {alarm_id}. Must have 1 ON and 1 OFF variable")
|
|
95
|
+
on_t_var_name = on_t_codes.iloc[0]['variable_name']
|
|
96
|
+
on_t_pretty_name = on_t_codes.iloc[0]['pretty_name']
|
|
97
|
+
on_t_thresh = on_t_codes.iloc[0]['bound']
|
|
98
|
+
off_t_var_name = off_t_codes.iloc[0]['variable_name']
|
|
99
|
+
off_t_pretty_name = off_t_codes.iloc[0]['pretty_name']
|
|
100
|
+
off_t_thresh = off_t_codes.iloc[0]['bound']
|
|
101
|
+
if pow_var_name in filtered_df.columns:
|
|
102
|
+
power_below = filtered_df[pow_var_name] <= pow_thresh
|
|
103
|
+
power_above = filtered_df[pow_var_name] > pow_thresh
|
|
104
|
+
|
|
105
|
+
# Check all turn-on events
|
|
106
|
+
if on_t_var_name in filtered_df.columns:
|
|
107
|
+
power_turn_on = power_below.shift(1) & power_above
|
|
108
|
+
power_on_times = filtered_df.index[power_turn_on.fillna(False)]
|
|
109
|
+
# Check if temperature is within 5.0 of on_t_thresh at each turn-on moment
|
|
110
|
+
for power_time in power_on_times:
|
|
111
|
+
temp_at_turn_on = filtered_df.loc[power_time, on_t_var_name]
|
|
112
|
+
if abs(temp_at_turn_on - on_t_thresh) > 5.0:
|
|
113
|
+
self._add_an_alarm(power_time, power_time, on_t_var_name,
|
|
114
|
+
f"Unexpected SOO change: during {soo_mode_name}, HP turned on at {power_time} but {on_t_pretty_name} was {temp_at_turn_on:.1f} F (setpoint at {on_t_thresh} F).",
|
|
115
|
+
certainty="med")
|
|
116
|
+
|
|
117
|
+
# Check all turn-off events
|
|
118
|
+
if off_t_var_name in filtered_df.columns:
|
|
119
|
+
power_turn_off = power_above.shift(1) & power_below
|
|
120
|
+
power_off_times = filtered_df.index[power_turn_off.fillna(False)]
|
|
121
|
+
# Check if temperature is within 5.0 of off_t_thresh at each turn-off moment
|
|
122
|
+
for power_time in power_off_times:
|
|
123
|
+
temp_at_turn_off = filtered_df.loc[power_time, off_t_var_name]
|
|
124
|
+
if abs(temp_at_turn_off - off_t_thresh) > 5.0:
|
|
125
|
+
self._add_an_alarm(power_time, power_time, off_t_var_name,
|
|
126
|
+
f"Unexpected SOO change: during {soo_mode_name}, HP turned off at {power_time} but {off_t_pretty_name} was {temp_at_turn_off:.1f} F (setpoint at {off_t_thresh} F).",
|
|
127
|
+
certainty="med")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import datetime as datetime
|
|
4
|
+
from ecopipeline import ConfigManager
|
|
5
|
+
import re
|
|
6
|
+
import mysql.connector.errors as mysqlerrors
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from ecopipeline.event_tracking.Alarm import Alarm
|
|
9
|
+
|
|
10
|
+
class ShortCycle(Alarm):
|
|
11
|
+
"""
|
|
12
|
+
Detects short cycling by identifying when the heat pump turns on for less than `short_cycle_time`
|
|
13
|
+
consecutive minutes before turning off again. Short cycling can indicate equipment issues or
|
|
14
|
+
improper system sizing.
|
|
15
|
+
|
|
16
|
+
VarNames syntax:
|
|
17
|
+
SHRTCYC_[OPTIONAL ID]:### - Indicates a power variable for the heat pump. ### is the power threshold (default 1.0) above which
|
|
18
|
+
the heat pump is considered 'on'.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
default_power_threshold : float
|
|
23
|
+
Default power threshold when no custom bound is specified in the alarm code (default 1.0). Heat pump is considered 'on'
|
|
24
|
+
when power exceeds this value.
|
|
25
|
+
short_cycle_time : int
|
|
26
|
+
Minimum expected run time in minutes (default 15). An alarm triggers if the heat pump runs for fewer than this many
|
|
27
|
+
consecutive minutes before turning off.
|
|
28
|
+
"""
|
|
29
|
+
def __init__(self, bounds_df : pd.DataFrame, default_power_threshold : float = 1.0, short_cycle_time : int = 15):
|
|
30
|
+
alarm_tag = 'SHRTCYC'
|
|
31
|
+
type_default_dict = {'default' : default_power_threshold}
|
|
32
|
+
self.short_cycle_time = short_cycle_time
|
|
33
|
+
super().__init__(bounds_df, alarm_tag,type_default_dict, two_part_tag = False, alarm_db_type='SHORT_CYCLE')
|
|
34
|
+
|
|
35
|
+
def specific_alarm_function(self, df: pd.DataFrame, daily_df : pd.DataFrame, config : ConfigManager):
|
|
36
|
+
for var_name in self.bounds_df['variable_name'].unique():
|
|
37
|
+
for day in daily_df.index:
|
|
38
|
+
next_day = day + pd.Timedelta(days=1)
|
|
39
|
+
filtered_df = df.loc[(df.index >= day) & (df.index < next_day)]
|
|
40
|
+
rows = self.bounds_df[self.bounds_df['variable_name'] == var_name]
|
|
41
|
+
pwr_thresh = rows.iloc[0]['bound']
|
|
42
|
+
var_pretty = rows.iloc[0]['pretty_name']
|
|
43
|
+
if len(rows) != 1:
|
|
44
|
+
raise Exception(f"Multiple short cycle alarm codes set for {var_name}")
|
|
45
|
+
if var_name in filtered_df.columns:
|
|
46
|
+
power_on_mask = filtered_df[var_name] > pwr_thresh
|
|
47
|
+
|
|
48
|
+
# Find runs of consecutive True values by detecting changes in the mask
|
|
49
|
+
mask_changes = power_on_mask != power_on_mask.shift(1)
|
|
50
|
+
run_groups = mask_changes.cumsum()
|
|
51
|
+
|
|
52
|
+
# For each run where power is on, check if it's shorter than short_cycle_time
|
|
53
|
+
for group_id in run_groups[power_on_mask].unique():
|
|
54
|
+
run_indices = filtered_df.index[(run_groups == group_id) & power_on_mask]
|
|
55
|
+
run_length = len(run_indices)
|
|
56
|
+
if run_length > 0 and run_length < self.short_cycle_time:
|
|
57
|
+
start_time = run_indices[0]
|
|
58
|
+
end_time = run_indices[-1]
|
|
59
|
+
self._add_an_alarm(start_time, end_time, var_name, f"Short cycle: {var_pretty} was on for only {run_length} minutes starting at {start_time}.")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import datetime as datetime
|
|
4
|
+
from ecopipeline import ConfigManager
|
|
5
|
+
import re
|
|
6
|
+
import mysql.connector.errors as mysqlerrors
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from ecopipeline.event_tracking.Alarm import Alarm
|
|
9
|
+
|
|
10
|
+
class TMSetpoint(Alarm):
|
|
11
|
+
"""
|
|
12
|
+
Function will take a pandas dataframe and location of alarm information in a csv,
|
|
13
|
+
and create an dataframe with applicable alarm events
|
|
14
|
+
|
|
15
|
+
VarNames syntax:
|
|
16
|
+
TMNSTPT_T_ID:### - Swing Tank Outlet Temperature. Alarm triggered if over number ### (or 130) for 3 minutes with power on
|
|
17
|
+
TMNSTPT_SP_ID:### - Swing Tank Power. ### is lowest recorded power for Swing Tank to be considered 'on'. Defaults to 1.0
|
|
18
|
+
TMNSTPT_TP_ID:### - Total System Power for ratio alarming for alarming if swing tank power is more than ### (40% default) of usage
|
|
19
|
+
TMNSTPT_ST_ID:### - Swing Tank Setpoint that should not change at all from ### (default 130)
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
default_fault_time : int
|
|
24
|
+
Number of consecutive minutes for T+SP alarms (default 3). T+SP alarms trigger when tank is powered and temperature exceeds
|
|
25
|
+
setpoint for this many consecutive minutes.
|
|
26
|
+
default_setpoint : float
|
|
27
|
+
Default temperature setpoint in degrees for T and ST alarm codes when no custom bound is specified (default 130.0)
|
|
28
|
+
default_power_indication : float
|
|
29
|
+
Default power threshold in kW for SP alarm codes when no custom bound is specified (default 1.0)
|
|
30
|
+
default_power_ratio : float
|
|
31
|
+
Default power ratio threshold (as decimal, e.g., 0.4 for 40%) for TP alarm codes when no custom bound is specified (default 0.4)
|
|
32
|
+
"""
|
|
33
|
+
def __init__(self, bounds_df : pd.DataFrame, default_fault_time : int = 3, default_setpoint : float = 130.0, default_power_indication : float = 1.0,
|
|
34
|
+
default_power_ratio : float = 0.4):
|
|
35
|
+
alarm_tag = 'TMNSTPT'
|
|
36
|
+
self.default_fault_time = default_fault_time
|
|
37
|
+
type_default_dict = {'T' : default_setpoint,
|
|
38
|
+
'SP': default_power_indication,
|
|
39
|
+
'TP': default_power_ratio,
|
|
40
|
+
'ST': default_setpoint}
|
|
41
|
+
super().__init__(bounds_df, alarm_tag,type_default_dict, alarm_db_type='TM_SETPOINT')
|
|
42
|
+
|
|
43
|
+
def specific_alarm_function(self, df: pd.DataFrame, daily_df : pd.DataFrame, config : ConfigManager):
|
|
44
|
+
for day in daily_df.index:
|
|
45
|
+
next_day = day + pd.Timedelta(days=1)
|
|
46
|
+
filtered_df = df.loc[(df.index >= day) & (df.index < next_day)]
|
|
47
|
+
for alarm_id in self.bounds_df['alarm_code_id'].unique():
|
|
48
|
+
id_group = self.bounds_df[self.bounds_df['alarm_code_id'] == alarm_id]
|
|
49
|
+
|
|
50
|
+
# Get T and SP alarm codes for this ID
|
|
51
|
+
t_codes = id_group[id_group['alarm_code_type'] == 'T']
|
|
52
|
+
sp_codes = id_group[id_group['alarm_code_type'] == 'SP']
|
|
53
|
+
tp_codes = id_group[id_group['alarm_code_type'] == 'TP']
|
|
54
|
+
st_codes = id_group[id_group['alarm_code_type'] == 'ST']
|
|
55
|
+
|
|
56
|
+
# Check for multiple T or SP codes with same ID
|
|
57
|
+
if len(t_codes) > 1 or len(sp_codes) > 1 or len(tp_codes) > 1 or len(st_codes) > 1:
|
|
58
|
+
raise Exception(f"Improper alarm codes for swing tank setpoint with id {alarm_id}")
|
|
59
|
+
trigger_columns_condition_met = False
|
|
60
|
+
if len(st_codes) == 1:
|
|
61
|
+
st_var_name = st_codes.iloc[0]['variable_name']
|
|
62
|
+
st_setpoint = st_codes.iloc[0]['bound']
|
|
63
|
+
st_pretty_name = st_codes.iloc[0]['pretty_name']
|
|
64
|
+
# Check if st_var_name exists in filtered_df
|
|
65
|
+
if st_var_name in filtered_df.columns:
|
|
66
|
+
trigger_columns_condition_met = True
|
|
67
|
+
# Check if setpoint was altered for over 10 minutes
|
|
68
|
+
altered_mask = filtered_df[st_var_name] != st_setpoint
|
|
69
|
+
consecutive_condition = altered_mask.rolling(window=10).min() == 1
|
|
70
|
+
if consecutive_condition.any():
|
|
71
|
+
# Find all consecutive groups where condition is true
|
|
72
|
+
group = (consecutive_condition != consecutive_condition.shift()).cumsum()
|
|
73
|
+
for group_id in consecutive_condition.groupby(group).first()[lambda x: x].index:
|
|
74
|
+
streak_indices = consecutive_condition[group == group_id].index
|
|
75
|
+
start_time = streak_indices[0] - pd.Timedelta(minutes=9)
|
|
76
|
+
end_time = streak_indices[-1]
|
|
77
|
+
streak_length = len(streak_indices) + 9
|
|
78
|
+
actual_value = filtered_df.loc[streak_indices[0], st_var_name]
|
|
79
|
+
self._add_an_alarm(start_time, end_time, st_var_name,
|
|
80
|
+
f"Setpoint altered: {st_pretty_name} was {actual_value} for {streak_length} minutes starting at {start_time} (expected {st_setpoint}).")
|
|
81
|
+
# Check if we have both T and SP
|
|
82
|
+
if len(t_codes) == 1 and len(sp_codes) == 1:
|
|
83
|
+
t_var_name = t_codes.iloc[0]['variable_name']
|
|
84
|
+
t_pretty_name = t_codes.iloc[0]['pretty_name']
|
|
85
|
+
sp_var_name = sp_codes.iloc[0]['variable_name']
|
|
86
|
+
sp_pretty_name = sp_codes.iloc[0]['pretty_name']
|
|
87
|
+
sp_power_indication = sp_codes.iloc[0]['bound']
|
|
88
|
+
t_setpoint = t_codes.iloc[0]['bound']
|
|
89
|
+
# Check if both variables exist in df
|
|
90
|
+
if t_var_name in filtered_df.columns and sp_var_name in filtered_df.columns:
|
|
91
|
+
trigger_columns_condition_met = True
|
|
92
|
+
# Check for consecutive minutes where SP > default_power_indication
|
|
93
|
+
# AND T >= default_setpoint
|
|
94
|
+
power_mask = filtered_df[sp_var_name] >= sp_power_indication
|
|
95
|
+
temp_mask = filtered_df[t_var_name] >= t_setpoint
|
|
96
|
+
combined_mask = power_mask & temp_mask
|
|
97
|
+
|
|
98
|
+
# Check for fault_time consecutive minutes
|
|
99
|
+
consecutive_condition = combined_mask.rolling(window=self.default_fault_time).min() == 1
|
|
100
|
+
if consecutive_condition.any():
|
|
101
|
+
# Find all consecutive groups where condition is true
|
|
102
|
+
group = (consecutive_condition != consecutive_condition.shift()).cumsum()
|
|
103
|
+
for group_id in consecutive_condition.groupby(group).first()[lambda x: x].index:
|
|
104
|
+
streak_indices = consecutive_condition[group == group_id].index
|
|
105
|
+
start_time = streak_indices[0] - pd.Timedelta(minutes=self.default_fault_time - 1)
|
|
106
|
+
end_time = streak_indices[-1]
|
|
107
|
+
streak_length = len(streak_indices) + self.default_fault_time - 1
|
|
108
|
+
actual_temp = filtered_df.loc[streak_indices[0], t_var_name]
|
|
109
|
+
self._add_an_alarm(start_time, end_time, sp_var_name,
|
|
110
|
+
f"High TM Setpoint: {sp_pretty_name} showed draw for {streak_length} minutes starting at {start_time} while {t_pretty_name} was {actual_temp:.1f} F (above {t_setpoint} F).",
|
|
111
|
+
certainty="med")
|
|
112
|
+
|
|
113
|
+
if len(tp_codes) == 1 and len(sp_codes) == 1:
|
|
114
|
+
tp_var_name = tp_codes.iloc[0]['variable_name']
|
|
115
|
+
sp_var_name = sp_codes.iloc[0]['variable_name']
|
|
116
|
+
sp_pretty_name = sp_codes.iloc[0]['pretty_name']
|
|
117
|
+
tp_ratio = tp_codes.iloc[0]['bound']
|
|
118
|
+
# Check if both variables exist in df
|
|
119
|
+
if tp_var_name in daily_df.columns and sp_var_name in daily_df.columns:
|
|
120
|
+
trigger_columns_condition_met = True
|
|
121
|
+
# Check if swing tank power ratio exceeds threshold
|
|
122
|
+
if day in daily_df.index and daily_df.loc[day, tp_var_name] != 0:
|
|
123
|
+
power_ratio = daily_df.loc[day, sp_var_name] / daily_df.loc[day, tp_var_name]
|
|
124
|
+
if power_ratio > tp_ratio:
|
|
125
|
+
self._add_an_alarm(day, day + timedelta(1), sp_var_name,
|
|
126
|
+
f"High temperature maintenance power ratio: {sp_pretty_name} accounted for {power_ratio * 100:.1f}% of daily power (threshold {tp_ratio * 100}%).",
|
|
127
|
+
certainty="low")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import datetime as datetime
|
|
4
|
+
from ecopipeline import ConfigManager
|
|
5
|
+
import re
|
|
6
|
+
import mysql.connector.errors as mysqlerrors
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from ecopipeline.event_tracking.Alarm import Alarm
|
|
9
|
+
|
|
10
|
+
class TempRange(Alarm):
|
|
11
|
+
"""
|
|
12
|
+
Detects when a temperature value falls outside an acceptable range for
|
|
13
|
+
too long. An alarm is triggered if the temperature is above the high bound or below the low bound
|
|
14
|
+
for `fault_time` consecutive minutes.
|
|
15
|
+
|
|
16
|
+
VarNames syntax:
|
|
17
|
+
TMPRANG_[OPTIONAL ID]:###-### - Indicates a temperature variable. ###-### is the acceptable temperature range
|
|
18
|
+
(e.g., TMPRANG:110-130 means temperature should stay between 110 and 130 degrees).
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
df: pd.DataFrame
|
|
23
|
+
Post-transformed dataframe for minute data. It should be noted that this function expects consecutive, in order minutes. If minutes
|
|
24
|
+
are out of order or have gaps, the function may return erroneous alarms.
|
|
25
|
+
daily_df: pd.DataFrame
|
|
26
|
+
Post-transformed dataframe for daily data. Used for determining which days to process.
|
|
27
|
+
config : ecopipeline.ConfigManager
|
|
28
|
+
The ConfigManager object that holds configuration data for the pipeline. Among other things, this object will point to a file
|
|
29
|
+
called Variable_Names.csv in the input folder of the pipeline (e.g. "full/path/to/pipeline/input/Variable_Names.csv").
|
|
30
|
+
The file must have at least two columns which must be titled "variable_name" and "alarm_codes" which should contain the
|
|
31
|
+
name of each variable in the dataframe that requires alarming and the DHW alarm codes (e.g., DHW:110-130, DHW_1:115-125).
|
|
32
|
+
system: str
|
|
33
|
+
String of system name if processing a particular system in a Variable_Names.csv file with multiple systems. Leave as an empty string if not applicable.
|
|
34
|
+
default_high_temp : float
|
|
35
|
+
Default high temperature bound when no custom range is specified in the alarm code (default 130). Temperature above this triggers alarm.
|
|
36
|
+
default_low_temp : float
|
|
37
|
+
Default low temperature bound when no custom range is specified in the alarm code (default 130). Temperature below this triggers alarm.
|
|
38
|
+
fault_time : int
|
|
39
|
+
Number of consecutive minutes that temperature must be outside the acceptable range before triggering an alarm (default 10).
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
pd.DataFrame:
|
|
44
|
+
Pandas dataframe with alarm events
|
|
45
|
+
"""
|
|
46
|
+
def __init__(self, bounds_df : pd.DataFrame, default_high_temp : float = 130, default_low_temp : float = 115, fault_time : int = 10):
|
|
47
|
+
alarm_tag = 'TMPRANG'
|
|
48
|
+
type_default_dict = {'default': [default_low_temp, default_high_temp]}
|
|
49
|
+
self.fault_time = fault_time
|
|
50
|
+
super().__init__(bounds_df, alarm_tag,type_default_dict, two_part_tag = False, range_bounds=True, alarm_db_type='TEMP_RANGE')
|
|
51
|
+
|
|
52
|
+
def specific_alarm_function(self, df: pd.DataFrame, daily_df : pd.DataFrame, config : ConfigManager):
|
|
53
|
+
# Process each unique alarm_code_id
|
|
54
|
+
for dhw_var in self.bounds_df['variable_name'].unique():
|
|
55
|
+
for day in daily_df.index:
|
|
56
|
+
next_day = day + pd.Timedelta(days=1)
|
|
57
|
+
filtered_df = df.loc[(df.index >= day) & (df.index < next_day)]
|
|
58
|
+
rows = self.bounds_df[self.bounds_df['variable_name'] == dhw_var]
|
|
59
|
+
low_bound = rows.iloc[0]['bound']
|
|
60
|
+
high_bound = rows.iloc[0]['bound2']
|
|
61
|
+
pretty_name = rows.iloc[0]['pretty_name']
|
|
62
|
+
|
|
63
|
+
if dhw_var in filtered_df.columns:
|
|
64
|
+
# Check if temp is above high bound or below low bound
|
|
65
|
+
out_of_range_mask = (filtered_df[dhw_var] > high_bound) | (filtered_df[dhw_var] < low_bound)
|
|
66
|
+
|
|
67
|
+
# Check for fault_time consecutive minutes
|
|
68
|
+
consecutive_condition = out_of_range_mask.rolling(window=self.fault_time).min() == 1
|
|
69
|
+
if consecutive_condition.any():
|
|
70
|
+
# Find all streaks of consecutive True values
|
|
71
|
+
group = (consecutive_condition != consecutive_condition.shift()).cumsum()
|
|
72
|
+
|
|
73
|
+
# Iterate through each streak and add an alarm for each
|
|
74
|
+
for group_id in consecutive_condition.groupby(group).first()[lambda x: x].index:
|
|
75
|
+
streak_indices = consecutive_condition[group == group_id].index
|
|
76
|
+
streak_length = len(streak_indices)
|
|
77
|
+
|
|
78
|
+
# Adjust start time because first (fault_time-1) minutes don't count in window
|
|
79
|
+
start_time = streak_indices[0] - pd.Timedelta(minutes=self.fault_time-1)
|
|
80
|
+
end_time = streak_indices[-1]
|
|
81
|
+
adjusted_streak_length = streak_length + self.fault_time - 1
|
|
82
|
+
|
|
83
|
+
self._add_an_alarm(start_time, end_time, dhw_var,
|
|
84
|
+
f"Temperature out of range: {pretty_name} was outside {low_bound}-{high_bound} F for {adjusted_streak_length} consecutive minutes starting at {start_time}.")
|
|
File without changes
|