ecopipeline 1.0.4__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.
Files changed (30) hide show
  1. ecopipeline/event_tracking/Alarm.py +317 -0
  2. ecopipeline/event_tracking/__init__.py +20 -2
  3. ecopipeline/event_tracking/alarms/AbnormalCOP.py +76 -0
  4. ecopipeline/event_tracking/alarms/BackupUse.py +94 -0
  5. ecopipeline/event_tracking/alarms/BalancingValve.py +78 -0
  6. ecopipeline/event_tracking/alarms/BlownFuse.py +72 -0
  7. ecopipeline/event_tracking/alarms/Boundary.py +90 -0
  8. ecopipeline/event_tracking/alarms/HPWHInlet.py +73 -0
  9. ecopipeline/event_tracking/alarms/HPWHOutage.py +96 -0
  10. ecopipeline/event_tracking/alarms/HPWHOutlet.py +85 -0
  11. ecopipeline/event_tracking/alarms/LSInconsist.py +114 -0
  12. ecopipeline/event_tracking/alarms/PowerRatio.py +111 -0
  13. ecopipeline/event_tracking/alarms/SOOChange.py +127 -0
  14. ecopipeline/event_tracking/alarms/ShortCycle.py +59 -0
  15. ecopipeline/event_tracking/alarms/TMSetpoint.py +127 -0
  16. ecopipeline/event_tracking/alarms/TempRange.py +84 -0
  17. ecopipeline/event_tracking/alarms/__init__.py +0 -0
  18. ecopipeline/event_tracking/event_tracking.py +517 -704
  19. ecopipeline/extract/__init__.py +2 -2
  20. ecopipeline/extract/extract.py +84 -0
  21. ecopipeline/load/__init__.py +2 -2
  22. ecopipeline/load/load.py +304 -3
  23. ecopipeline/transform/transform.py +1 -1
  24. ecopipeline/utils/ConfigManager.py +15 -2
  25. {ecopipeline-1.0.4.dist-info → ecopipeline-1.1.0.dist-info}/METADATA +1 -1
  26. ecopipeline-1.1.0.dist-info/RECORD +41 -0
  27. {ecopipeline-1.0.4.dist-info → ecopipeline-1.1.0.dist-info}/WHEEL +1 -1
  28. ecopipeline-1.0.4.dist-info/RECORD +0 -25
  29. {ecopipeline-1.0.4.dist-info → ecopipeline-1.1.0.dist-info}/licenses/LICENSE +0 -0
  30. {ecopipeline-1.0.4.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