open-fdd 0.1.6__py3-none-any.whl → 0.1.8__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.
@@ -1,185 +1,89 @@
1
1
  import pandas as pd
2
2
  import numpy as np
3
- from open_fdd.air_handling_unit.faults.fault_condition import (
4
- FaultCondition,
5
- MissingColumnError,
6
- InvalidParameterError,
7
- )
3
+ from open_fdd.core.base_fault import BaseFaultCondition
4
+ from open_fdd.core.mixins import FaultConditionMixin
5
+ from open_fdd.air_handling_unit.faults.helper_utils import SharedUtils
8
6
  import operator
9
7
  import sys
8
+ from open_fdd.core.exceptions import MissingColumnError, InvalidParameterError
10
9
 
11
10
 
12
- class FaultConditionOne(FaultCondition):
11
+ class FaultConditionOne(BaseFaultCondition, FaultConditionMixin):
13
12
  """Class provides the definitions for Fault Condition 1.
14
13
  AHU low duct static pressure fan fault.
15
14
 
16
15
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc1.py -rP -s
17
16
  """
18
17
 
19
- def __init__(self, dict_):
20
- super().__init__()
21
-
22
- # Threshold parameters
23
- self.vfd_speed_percent_err_thres = dict_.get(
24
- "VFD_SPEED_PERCENT_ERR_THRES", None
25
- )
26
- self.vfd_speed_percent_max = dict_.get("VFD_SPEED_PERCENT_MAX", None)
27
- self.duct_static_inches_err_thres = dict_.get(
28
- "DUCT_STATIC_INCHES_ERR_THRES", None
29
- )
30
-
31
- # Validate that threshold parameters are floats
32
- for param, value in [
33
- ("vfd_speed_percent_err_thres", self.vfd_speed_percent_err_thres),
34
- ("vfd_speed_percent_max", self.vfd_speed_percent_max),
35
- ("duct_static_inches_err_thres", self.duct_static_inches_err_thres),
36
- ]:
37
- if not isinstance(value, float):
38
- raise InvalidParameterError(
39
- f"The parameter '{param}' should be a float, but got {type(value).__name__}."
40
- )
41
-
42
- # Other attributes
18
+ def _init_specific_attributes(self, dict_):
19
+ # Initialize specific attributes
43
20
  self.duct_static_col = dict_.get("DUCT_STATIC_COL", None)
44
21
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
45
22
  self.duct_static_setpoint_col = dict_.get("DUCT_STATIC_SETPOINT_COL", None)
46
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
47
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
48
-
49
- self.equation_string = (
50
- "fc1_flag = 1 if (DSP < DPSP - εDSP) and (VFDSPD >= VFDSPD_max - εVFDSPD) "
51
- "for N consecutive values else 0 \n"
23
+ self.duct_static_inches_err_thres = dict_.get(
24
+ "DUCT_STATIC_INCHES_ERR_THRES", None
52
25
  )
53
- self.description_string = (
54
- "Fault Condition 1: Duct static too low at fan at full speed \n"
26
+ self.vfd_speed_percent_max = dict_.get("VFD_SPEED_PERCENT_MAX", None)
27
+ self.vfd_speed_percent_err_thres = dict_.get(
28
+ "VFD_SPEED_PERCENT_ERR_THRES", None
55
29
  )
56
- self.required_column_description = "Required inputs are the duct static pressure, setpoint, and supply fan VFD speed \n"
57
- self.error_string = f"One or more required columns are missing or None \n"
58
-
59
- self.set_attributes(dict_)
60
30
 
61
- # Set required columns specific to this fault condition
31
+ # Set required columns
62
32
  self.required_columns = [
63
33
  self.duct_static_col,
64
34
  self.supply_vfd_speed_col,
65
35
  self.duct_static_setpoint_col,
66
36
  ]
67
37
 
68
- # Check if any of the required columns are None
69
- if any(col is None for col in self.required_columns):
70
- raise MissingColumnError(
71
- f"{self.error_string}"
72
- f"{self.equation_string}"
73
- f"{self.description_string}"
74
- f"{self.required_column_description}"
75
- f"{self.required_columns}"
76
- )
77
-
78
- # Ensure all required columns are strings
79
- self.required_columns = [str(col) for col in self.required_columns]
80
-
81
- self.mapped_columns = (
82
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
83
- )
84
-
85
- def get_required_columns(self) -> str:
86
- """Called from IPython to print out."""
87
- return (
88
- f"{self.equation_string}"
89
- f"{self.description_string}"
90
- f"{self.required_column_description}"
91
- f"{self.mapped_columns}"
38
+ # Set documentation strings
39
+ self.equation_string = "fc1_flag = 1 if (DP < DPSP - εDP) and (VFDSPD >= VFDSPD_max - εVFDSPD) for N consecutive values else 0 \n"
40
+ self.description_string = (
41
+ "Fault Condition 1: Duct static too low at fan at full speed \n"
92
42
  )
43
+ self.required_column_description = "Required inputs are the duct static pressure, setpoint, and supply fan VFD speed \n"
44
+ self.error_string = "One or more required columns are missing or None \n"
93
45
 
46
+ @FaultConditionMixin._handle_errors
94
47
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
95
- try:
96
- # Ensure all required columns are present
97
- self.check_required_columns(df)
98
-
99
- if self.troubleshoot_mode:
100
- self.troubleshoot_cols(df)
101
-
102
- # Check analog outputs [data with units of %] are floats only
103
- columns_to_check = [self.supply_vfd_speed_col]
104
- self.check_analog_pct(df, columns_to_check)
105
-
106
- # Perform checks
107
- static_check = (
108
- df[self.duct_static_col]
109
- < df[self.duct_static_setpoint_col] - self.duct_static_inches_err_thres
110
- )
111
- fan_check = (
112
- df[self.supply_vfd_speed_col]
113
- >= self.vfd_speed_percent_max - self.vfd_speed_percent_err_thres
114
- )
115
-
116
- # Combined condition check
117
- combined_check = static_check & fan_check
48
+ self._apply_common_checks(df)
49
+ self._apply_analog_checks(df, [self.supply_vfd_speed_col])
118
50
 
119
- # Rolling sum to count consecutive trues
120
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
51
+ # Convert VFD speed from percentage to fraction if needed
52
+ if (df[self.supply_vfd_speed_col] > 1.0).any():
53
+ df[self.supply_vfd_speed_col] = df[self.supply_vfd_speed_col] / 100.0
121
54
 
122
- # Set flag to 1 if rolling sum equals the window size
123
- df["fc1_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
55
+ # Convert thresholds from percentage to fraction
56
+ vfd_speed_max = self.vfd_speed_percent_max / 100.0
57
+ vfd_speed_err_thres = self.vfd_speed_percent_err_thres / 100.0
124
58
 
125
- return df
59
+ # Specific checks
60
+ static_check = (
61
+ df[self.duct_static_col]
62
+ < df[self.duct_static_setpoint_col] - self.duct_static_inches_err_thres
63
+ )
64
+ fan_check = df[self.supply_vfd_speed_col] >= vfd_speed_max - vfd_speed_err_thres
65
+ combined_check = static_check & fan_check
126
66
 
127
- except MissingColumnError as e:
128
- print(f"Error: {e.message}")
129
- sys.stdout.flush()
130
- raise e
131
- except InvalidParameterError as e:
132
- print(f"Error: {e.message}")
133
- sys.stdout.flush()
134
- raise e
67
+ self._set_fault_flag(df, combined_check, "fc1_flag")
68
+ return df
135
69
 
136
70
 
137
- class FaultConditionTwo(FaultCondition):
71
+ class FaultConditionTwo(BaseFaultCondition, FaultConditionMixin):
138
72
  """Class provides the definitions for Fault Condition 2.
139
73
  Mix temperature too low; should be between outside and return air.
140
74
  """
141
75
 
142
- def __init__(self, dict_):
143
- super().__init__()
144
-
145
- # Threshold parameters
146
- self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
147
- self.return_degf_err_thres = dict_.get("RETURN_DEGF_ERR_THRES", None)
148
- self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
149
-
150
- # Validate that threshold parameters are floats
151
- for param, value in [
152
- ("mix_degf_err_thres", self.mix_degf_err_thres),
153
- ("return_degf_err_thres", self.return_degf_err_thres),
154
- ("outdoor_degf_err_thres", self.outdoor_degf_err_thres),
155
- ]:
156
- if not isinstance(value, float):
157
- raise InvalidParameterError(
158
- f"The parameter '{param}' should be a float, but got {type(value).__name__}."
159
- )
160
-
161
- # Other attributes
76
+ def _init_specific_attributes(self, dict_):
77
+ # Initialize specific attributes
162
78
  self.mat_col = dict_.get("MAT_COL", None)
163
79
  self.rat_col = dict_.get("RAT_COL", None)
164
80
  self.oat_col = dict_.get("OAT_COL", None)
165
81
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
166
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
167
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
168
-
169
- self.equation_string = (
170
- "fc2_flag = 1 if (MAT + εMAT < min(RAT - εRAT, OAT - εOAT)) and (VFDSPD > 0) "
171
- "for N consecutive values else 0 \n"
172
- )
173
- self.description_string = "Fault Condition 2: Mix temperature too low; should be between outside and return air \n"
174
- self.required_column_description = (
175
- "Required inputs are the mix air temperature, return air temperature, outside air temperature, "
176
- "and supply fan VFD speed \n"
177
- )
178
- self.error_string = "One or more required columns are missing or None \n"
179
-
180
- self.set_attributes(dict_)
82
+ self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
83
+ self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
84
+ self.return_degf_err_thres = dict_.get("RETURN_DEGF_ERR_THRES", None)
181
85
 
182
- # Set required columns specific to this fault condition
86
+ # Set required columns
183
87
  self.required_columns = [
184
88
  self.mat_col,
185
89
  self.rat_col,
@@ -187,119 +91,47 @@ class FaultConditionTwo(FaultCondition):
187
91
  self.supply_vfd_speed_col,
188
92
  ]
189
93
 
190
- # Check if any of the required columns are None
191
- if any(col is None for col in self.required_columns):
192
- raise MissingColumnError(
193
- f"{self.error_string}"
194
- f"{self.equation_string}"
195
- f"{self.description_string}"
196
- f"{self.required_column_description}"
197
- f"{self.required_columns}"
198
- )
94
+ # Set documentation strings
95
+ self.equation_string = "fc2_flag = 1 if (MAT - εMAT < min(RAT - εRAT, OAT - εOAT)) and (VFDSPD > 0) for N consecutive values else 0 \n"
96
+ self.description_string = "Fault Condition 2: Mix temperature too low; should be between outside and return air \n"
97
+ self.required_column_description = "Required inputs are the mixed air temperature, return air temperature, outside air temperature, and supply fan VFD speed \n"
98
+ self.error_string = "One or more required columns are missing or None \n"
199
99
 
200
- # Ensure all required columns are strings
201
- self.required_columns = [str(col) for col in self.required_columns]
100
+ @FaultConditionMixin._handle_errors
101
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
102
+ self._apply_common_checks(df)
103
+ self._apply_analog_checks(df, [self.supply_vfd_speed_col])
202
104
 
203
- self.mapped_columns = (
204
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
105
+ # Specific checks
106
+ mat_check = df[self.mat_col] - self.mix_degf_err_thres
107
+ temp_min_check = np.minimum(
108
+ df[self.rat_col] - self.return_degf_err_thres,
109
+ df[self.oat_col] - self.outdoor_degf_err_thres,
205
110
  )
206
-
207
- def get_required_columns(self) -> str:
208
- """Returns a string representation of the required columns."""
209
- return (
210
- f"{self.equation_string}"
211
- f"{self.description_string}"
212
- f"{self.required_column_description}"
213
- f"{self.mapped_columns}"
111
+ combined_check = (mat_check < temp_min_check) & (
112
+ df[self.supply_vfd_speed_col] > 0.01
214
113
  )
215
114
 
216
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
217
- try:
218
- # Ensure all required columns are present
219
- self.check_required_columns(df)
220
-
221
- if self.troubleshoot_mode:
222
- self.troubleshoot_cols(df)
223
-
224
- # Check analog outputs [data with units of %] are floats only
225
- columns_to_check = [self.supply_vfd_speed_col]
226
- self.check_analog_pct(df, columns_to_check)
227
-
228
- # Perform checks
229
- mat_check = df[self.mat_col] + self.mix_degf_err_thres
230
- temp_min_check = np.minimum(
231
- df[self.rat_col] - self.return_degf_err_thres,
232
- df[self.oat_col] - self.outdoor_degf_err_thres,
233
- )
234
-
235
- combined_check = (mat_check < temp_min_check) & (
236
- df[self.supply_vfd_speed_col] > 0.01
237
- )
238
-
239
- # Rolling sum to count consecutive trues
240
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
241
-
242
- # Set flag to 1 if rolling sum equals the window size
243
- df["fc2_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
244
-
245
- return df
246
-
247
- except MissingColumnError as e:
248
- print(f"Error: {e.message}")
249
- sys.stdout.flush()
250
- raise e
251
- except InvalidParameterError as e:
252
- print(f"Error: {e.message}")
253
- sys.stdout.flush()
254
- raise e
115
+ self._set_fault_flag(df, combined_check, "fc2_flag")
116
+ return df
255
117
 
256
118
 
257
- class FaultConditionThree(FaultCondition):
119
+ class FaultConditionThree(BaseFaultCondition, FaultConditionMixin):
258
120
  """Class provides the definitions for Fault Condition 3.
259
121
  Mix temperature too high; should be between outside and return air.
260
122
  """
261
123
 
262
- def __init__(self, dict_):
263
- super().__init__()
264
-
265
- # Threshold parameters
266
- self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
267
- self.return_degf_err_thres = dict_.get("RETURN_DEGF_ERR_THRES", None)
268
- self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
269
-
270
- # Validate that threshold parameters are floats
271
- for param, value in [
272
- ("mix_degf_err_thres", self.mix_degf_err_thres),
273
- ("return_degf_err_thres", self.return_degf_err_thres),
274
- ("outdoor_degf_err_thres", self.outdoor_degf_err_thres),
275
- ]:
276
- if not isinstance(value, float):
277
- raise InvalidParameterError(
278
- f"The parameter '{param}' should be a float, but got {type(value).__name__}."
279
- )
280
-
281
- # Other attributes
124
+ def _init_specific_attributes(self, dict_):
125
+ # Initialize specific attributes
282
126
  self.mat_col = dict_.get("MAT_COL", None)
283
127
  self.rat_col = dict_.get("RAT_COL", None)
284
128
  self.oat_col = dict_.get("OAT_COL", None)
285
129
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
286
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
287
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
288
-
289
- self.equation_string = (
290
- "fc3_flag = 1 if (MAT - εMAT > max(RAT + εRAT, OAT + εOAT)) and (VFDSPD > 0) "
291
- "for N consecutive values else 0 \n"
292
- )
293
- self.description_string = "Fault Condition 3: Mix temperature too high; should be between outside and return air \n"
294
- self.required_column_description = (
295
- "Required inputs are the mix air temperature, return air temperature, outside air temperature, "
296
- "and supply fan VFD speed \n"
297
- )
298
- self.error_string = "One or more required columns are missing or None \n"
299
-
300
- self.set_attributes(dict_)
130
+ self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
131
+ self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
132
+ self.return_degf_err_thres = dict_.get("RETURN_DEGF_ERR_THRES", None)
301
133
 
302
- # Set required columns specific to this fault condition
134
+ # Set required columns
303
135
  self.required_columns = [
304
136
  self.mat_col,
305
137
  self.rat_col,
@@ -307,74 +139,32 @@ class FaultConditionThree(FaultCondition):
307
139
  self.supply_vfd_speed_col,
308
140
  ]
309
141
 
310
- # Check if any of the required columns are None
311
- if any(col is None for col in self.required_columns):
312
- raise MissingColumnError(
313
- f"{self.error_string}"
314
- f"{self.equation_string}"
315
- f"{self.description_string}"
316
- f"{self.required_column_description}"
317
- f"{self.required_columns}"
318
- )
142
+ # Set documentation strings
143
+ self.equation_string = "fc3_flag = 1 if (MAT - εMAT > max(RAT + εRAT, OAT + εOAT)) and (VFDSPD > 0) for N consecutive values else 0 \n"
144
+ self.description_string = "Fault Condition 3: Mix temperature too high; should be between outside and return air \n"
145
+ self.required_column_description = "Required inputs are the mixed air temperature, return air temperature, outside air temperature, and supply fan VFD speed \n"
146
+ self.error_string = "One or more required columns are missing or None \n"
319
147
 
320
- # Ensure all required columns are strings
321
- self.required_columns = [str(col) for col in self.required_columns]
148
+ @FaultConditionMixin._handle_errors
149
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
150
+ self._apply_common_checks(df)
151
+ self._apply_analog_checks(df, [self.supply_vfd_speed_col])
322
152
 
323
- self.mapped_columns = (
324
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
153
+ # Specific checks
154
+ mat_check = df[self.mat_col] - self.mix_degf_err_thres
155
+ temp_max_check = np.maximum(
156
+ df[self.rat_col] + self.return_degf_err_thres,
157
+ df[self.oat_col] + self.outdoor_degf_err_thres,
325
158
  )
326
-
327
- def get_required_columns(self) -> str:
328
- """Returns a string representation of the required columns."""
329
- return (
330
- f"{self.equation_string}"
331
- f"{self.description_string}"
332
- f"{self.required_column_description}"
333
- f"{self.mapped_columns}"
159
+ combined_check = (mat_check > temp_max_check) & (
160
+ df[self.supply_vfd_speed_col] > 0.01
334
161
  )
335
162
 
336
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
337
- try:
338
- # Ensure all required columns are present
339
- self.check_required_columns(df)
340
-
341
- if self.troubleshoot_mode:
342
- self.troubleshoot_cols(df)
343
-
344
- # Check analog outputs [data with units of %] are floats only
345
- columns_to_check = [self.supply_vfd_speed_col]
346
- self.check_analog_pct(df, columns_to_check)
347
-
348
- # Perform checks
349
- mat_check = df[self.mat_col] - self.mix_degf_err_thres
350
- temp_max_check = np.maximum(
351
- df[self.rat_col] + self.return_degf_err_thres,
352
- df[self.oat_col] + self.outdoor_degf_err_thres,
353
- )
354
-
355
- combined_check = (mat_check > temp_max_check) & (
356
- df[self.supply_vfd_speed_col] > 0.01
357
- )
358
-
359
- # Rolling sum to count consecutive trues
360
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
361
-
362
- # Set flag to 1 if rolling sum equals the window size
363
- df["fc3_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
364
-
365
- return df
366
-
367
- except MissingColumnError as e:
368
- print(f"Error: {e.message}")
369
- sys.stdout.flush()
370
- raise e
371
- except InvalidParameterError as e:
372
- print(f"Error: {e.message}")
373
- sys.stdout.flush()
374
- raise e
163
+ self._set_fault_flag(df, combined_check, "fc3_flag")
164
+ return df
375
165
 
376
166
 
377
- class FaultConditionFour(FaultCondition):
167
+ class FaultConditionFour(BaseFaultCondition, FaultConditionMixin):
378
168
  """Class provides the definitions for Fault Condition 4.
379
169
 
380
170
  This fault flags excessive operating states on the AHU
@@ -386,15 +176,12 @@ class FaultConditionFour(FaultCondition):
386
176
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc4.py -rP -s
387
177
  """
388
178
 
389
- def __init__(self, dict_):
390
- super().__init__()
391
-
179
+ def _init_specific_attributes(self, dict_):
392
180
  # Threshold parameters
393
181
  self.delta_os_max = dict_.get("DELTA_OS_MAX", None)
394
182
  self.ahu_min_oa_dpr = dict_.get("AHU_MIN_OA_DPR", None)
395
183
 
396
184
  # Validate that delta_os_max can be either a float or an integer
397
- # if not isinstance(self.delta_os_max, (float, int)):
398
185
  if not isinstance(self.delta_os_max, (int)):
399
186
  raise InvalidParameterError(
400
187
  f"The parameter 'delta_os_max' should be an integer data type, but got {type(self.delta_os_max).__name__}."
@@ -411,8 +198,8 @@ class FaultConditionFour(FaultCondition):
411
198
  self.heating_sig_col = dict_.get("HEATING_SIG_COL", None)
412
199
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
413
200
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
414
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
415
201
 
202
+ # Set documentation strings
416
203
  self.equation_string = (
417
204
  "fc4_flag = 1 if excessive mode changes (> δOS_max) occur "
418
205
  "within an hour across heating, econ, econ+mech, mech clg, and min OA modes \n"
@@ -424,8 +211,6 @@ class FaultConditionFour(FaultCondition):
424
211
  )
425
212
  self.error_string = "One or more required columns are missing or None \n"
426
213
 
427
- self.set_attributes(dict_)
428
-
429
214
  # Set required columns, making heating and cooling optional
430
215
  self.required_columns = [
431
216
  self.economizer_sig_col,
@@ -438,137 +223,48 @@ class FaultConditionFour(FaultCondition):
438
223
  if self.cooling_sig_col:
439
224
  self.required_columns.append(self.cooling_sig_col)
440
225
 
441
- # Check if any of the required columns are None
442
- if any(col is None for col in self.required_columns):
443
- raise MissingColumnError(
444
- f"{self.error_string}"
445
- f"{self.equation_string}"
446
- f"{self.description_string}"
447
- f"{self.required_column_description}"
448
- f"{self.required_columns}"
449
- )
450
- # Ensure all required columns are strings
451
- self.required_columns = [str(col) for col in self.required_columns]
452
-
453
- self.mapped_columns = (
454
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
455
- )
456
-
457
- def get_required_columns(self) -> str:
458
- """Returns a string representation of the required columns."""
459
- return (
460
- f"{self.equation_string}"
461
- f"{self.description_string}"
462
- f"{self.required_column_description}"
463
- f"{self.mapped_columns}"
464
- )
465
-
226
+ @FaultConditionMixin._handle_errors
466
227
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
467
- try:
468
- # Ensure all required columns are present
469
- self.check_required_columns(df)
470
-
471
- # If the optional columns are not present, create them with all values set to 0.0
472
- if self.heating_sig_col not in df.columns:
473
- df[self.heating_sig_col] = 0.0
474
- if self.cooling_sig_col not in df.columns:
475
- df[self.cooling_sig_col] = 0.0
476
-
477
- if self.troubleshoot_mode:
478
- self.troubleshoot_cols(df)
479
-
480
- # Check analog outputs [data with units of %] are floats only
481
- columns_to_check = [
482
- self.economizer_sig_col,
483
- self.heating_sig_col,
484
- self.cooling_sig_col,
485
- self.supply_vfd_speed_col,
486
- ]
487
-
488
- for col in columns_to_check:
489
- self.check_analog_pct(df, [col])
490
-
491
- print("=" * 50)
492
- print("Warning: The program is in FC4 and resampling the data")
493
- print("to compute AHU OS state changes per hour")
494
- print("to flag any hunting issue")
495
- print("and this usually takes a while to run...")
496
- print("=" * 50)
497
-
498
- sys.stdout.flush()
499
-
500
- # AHU htg only mode based on OA damper @ min oa and only htg pid/vlv modulating
501
- df["heating_mode"] = (
502
- (df[self.heating_sig_col] > 0)
503
- & (df[self.cooling_sig_col] == 0)
504
- & (df[self.supply_vfd_speed_col] > 0)
505
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
506
- )
507
-
508
- # AHU econ only mode based on OA damper modulating and clg htg = zero
509
- df["econ_only_cooling_mode"] = (
510
- (df[self.heating_sig_col] == 0)
511
- & (df[self.cooling_sig_col] == 0)
512
- & (df[self.supply_vfd_speed_col] > 0)
513
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
514
- )
515
-
516
- # AHU econ+mech clg mode based on OA damper modulating for cooling and clg pid/vlv modulating
517
- df["econ_plus_mech_cooling_mode"] = (
518
- (df[self.heating_sig_col] == 0)
519
- & (df[self.cooling_sig_col] > 0)
520
- & (df[self.supply_vfd_speed_col] > 0)
521
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
522
- )
523
-
524
- # AHU mech mode based on OA damper @ min OA and clg pid/vlv modulating
525
- df["mech_cooling_only_mode"] = (
526
- (df[self.heating_sig_col] == 0)
527
- & (df[self.cooling_sig_col] > 0)
528
- & (df[self.supply_vfd_speed_col] > 0)
529
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
530
- )
531
-
532
- # AHU minimum OA mode without heating or cooling (ventilation mode)
533
- df["min_oa_mode_only"] = (
534
- (df[self.heating_sig_col] == 0)
535
- & (df[self.cooling_sig_col] == 0)
536
- & (df[self.supply_vfd_speed_col] > 0)
537
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
538
- )
539
-
540
- # Fill non-finite values with zero or drop them
541
- df = df.fillna(0)
228
+ """Apply the fault condition to the DataFrame."""
229
+ # Apply common checks
230
+ self._apply_common_checks(df)
231
+ # Add analog checks for supply_vfd_speed_col
232
+ self._apply_analog_checks(df, [self.supply_vfd_speed_col])
233
+
234
+ # Convert VFD speed from percentage to fraction if needed
235
+ if (df[self.supply_vfd_speed_col] > 1.0).any():
236
+ df[self.supply_vfd_speed_col] = df[self.supply_vfd_speed_col] / 100.0
237
+
238
+ # Calculate operating state changes
239
+ df["os_change"] = 0
240
+ df.loc[df[self.economizer_sig_col] > 0, "os_change"] += 1
241
+ df.loc[df[self.supply_vfd_speed_col] > self.ahu_min_oa_dpr, "os_change"] += 1
242
+ if self.heating_sig_col:
243
+ df.loc[df[self.heating_sig_col] > 0, "os_change"] += 1
244
+ if self.cooling_sig_col:
245
+ df.loc[df[self.cooling_sig_col] > 0, "os_change"] += 1
542
246
 
543
- df = df.astype(int)
544
- df = df.resample("60min").apply(lambda x: (x.eq(1) & x.shift().ne(1)).sum())
247
+ # Calculate changes in operating state
248
+ df["os_change_diff"] = df["os_change"].diff().abs()
249
+ df["os_change_diff"] = df["os_change_diff"].fillna(0)
545
250
 
546
- df["fc4_flag"] = (
547
- df[df.columns].gt(self.delta_os_max).any(axis=1).astype(int)
548
- )
251
+ # Calculate rolling sum of changes
252
+ df["os_change_sum"] = df["os_change_diff"].rolling(window=60).sum()
549
253
 
550
- return df
254
+ # Set fault flag
255
+ df["fc4_flag"] = (df["os_change_sum"] > self.delta_os_max).astype(int)
551
256
 
552
- except MissingColumnError as e:
553
- print(f"Error: {e.message}")
554
- sys.stdout.flush()
555
- raise e
556
- except InvalidParameterError as e:
557
- print(f"Error: {e.message}")
558
- sys.stdout.flush()
559
- raise e
257
+ return df
560
258
 
561
259
 
562
- class FaultConditionFive(FaultCondition):
260
+ class FaultConditionFive(BaseFaultCondition, FaultConditionMixin):
563
261
  """Class provides the definitions for Fault Condition 5.
564
262
  SAT too low; should be higher than MAT in HTG MODE
565
263
  --Broken heating valve or other mechanical issue
566
264
  related to heat valve not working as designed
567
265
  """
568
266
 
569
- def __init__(self, dict_):
570
- super().__init__()
571
-
267
+ def _init_specific_attributes(self, dict_):
572
268
  # Threshold parameters
573
269
  self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
574
270
  self.supply_degf_err_thres = dict_.get("SUPPLY_DEGF_ERR_THRES", None)
@@ -590,9 +286,8 @@ class FaultConditionFive(FaultCondition):
590
286
  self.sat_col = dict_.get("SAT_COL", None)
591
287
  self.heating_sig_col = dict_.get("HEATING_SIG_COL", None)
592
288
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
593
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
594
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
595
289
 
290
+ # Set documentation strings
596
291
  self.equation_string = (
597
292
  "fc5_flag = 1 if (SAT + εSAT <= MAT - εMAT + ΔT_supply_fan) and "
598
293
  "(heating signal > 0) and (VFDSPD > 0) for N consecutive values else 0 \n"
@@ -607,8 +302,6 @@ class FaultConditionFive(FaultCondition):
607
302
  )
608
303
  self.error_string = "One or more required columns are missing or None \n"
609
304
 
610
- self.set_attributes(dict_)
611
-
612
305
  # Set required columns specific to this fault condition
613
306
  self.required_columns = [
614
307
  self.mat_col,
@@ -617,72 +310,33 @@ class FaultConditionFive(FaultCondition):
617
310
  self.supply_vfd_speed_col,
618
311
  ]
619
312
 
620
- # Check if any of the required columns are None
621
- if any(col is None for col in self.required_columns):
622
- raise MissingColumnError(
623
- f"{self.error_string}"
624
- f"{self.equation_string}"
625
- f"{self.description_string}"
626
- f"{self.required_column_description}"
627
- f"{self.required_columns}"
628
- )
629
-
630
- # Ensure all required columns are strings
631
- self.required_columns = [str(col) for col in self.required_columns]
632
-
633
- self.mapped_columns = (
634
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
635
- )
636
-
637
- def get_required_columns(self) -> str:
638
- """Returns a string representation of the required columns."""
639
- return (
640
- f"{self.equation_string}"
641
- f"{self.description_string}"
642
- f"{self.required_column_description}"
643
- f"{self.mapped_columns}"
644
- )
645
-
313
+ @FaultConditionMixin._handle_errors
646
314
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
647
- try:
648
- # Ensure all required columns are present
649
- self.check_required_columns(df)
650
-
651
- # Check analog outputs [data with units of %] are floats only
652
- columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
653
- self.check_analog_pct(df, columns_to_check)
654
-
655
- # Perform checks
656
- sat_check = df[self.sat_col] + self.supply_degf_err_thres
657
- mat_check = (
658
- df[self.mat_col] - self.mix_degf_err_thres + self.delta_t_supply_fan
659
- )
315
+ """Apply the fault condition to the DataFrame."""
316
+ # Apply common checks
317
+ self._apply_common_checks(df)
660
318
 
661
- combined_check = (
662
- (sat_check <= mat_check)
663
- & (df[self.heating_sig_col] > 0.01)
664
- & (df[self.supply_vfd_speed_col] > 0.01)
665
- )
319
+ # Check analog outputs [data with units of %] are floats only
320
+ columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
321
+ self._apply_analog_checks(df, columns_to_check)
666
322
 
667
- # Rolling sum to count consecutive trues
668
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
323
+ # Perform checks
324
+ sat_check = df[self.sat_col] + self.supply_degf_err_thres
325
+ mat_check = df[self.mat_col] - self.mix_degf_err_thres + self.delta_t_supply_fan
669
326
 
670
- # Set flag to 1 if rolling sum equals the window size
671
- df["fc5_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
327
+ combined_check = (
328
+ (sat_check <= mat_check)
329
+ & (df[self.heating_sig_col] > 0.01)
330
+ & (df[self.supply_vfd_speed_col] > 0.01)
331
+ )
672
332
 
673
- return df
333
+ # Set fault flag
334
+ self._set_fault_flag(df, combined_check, "fc5_flag")
674
335
 
675
- except MissingColumnError as e:
676
- print(f"Error: {e.message}")
677
- sys.stdout.flush()
678
- raise e
679
- except InvalidParameterError as e:
680
- print(f"Error: {e.message}")
681
- sys.stdout.flush()
682
- raise e
336
+ return df
683
337
 
684
338
 
685
- class FaultConditionSix(FaultCondition):
339
+ class FaultConditionSix(BaseFaultCondition, FaultConditionMixin):
686
340
  """Class provides the definitions for Fault Condition 6.
687
341
 
688
342
  This fault related to knowing the design air flow for
@@ -699,9 +353,7 @@ class FaultConditionSix(FaultCondition):
699
353
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc6.py -rP -s
700
354
  """
701
355
 
702
- def __init__(self, dict_):
703
- super().__init__()
704
-
356
+ def _init_specific_attributes(self, dict_):
705
357
  # Threshold parameters
706
358
  self.airflow_err_thres = dict_.get("AIRFLOW_ERR_THRES", None)
707
359
  self.ahu_min_oa_cfm_design = dict_.get("AHU_MIN_OA_CFM_DESIGN", None)
@@ -737,9 +389,8 @@ class FaultConditionSix(FaultCondition):
737
389
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
738
390
  self.heating_sig_col = dict_.get("HEATING_SIG_COL", None)
739
391
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
740
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
741
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
742
392
 
393
+ # Set documentation strings
743
394
  self.equation_string = (
744
395
  "fc6_flag = 1 if |OA_frac_calc - OA_min| > airflow_err_thres "
745
396
  "in non-economizer modes, considering htg and mech clg OS \n"
@@ -755,8 +406,6 @@ class FaultConditionSix(FaultCondition):
755
406
  )
756
407
  self.error_string = "One or more required columns are missing or None \n"
757
408
 
758
- self.set_attributes(dict_)
759
-
760
409
  # Set required columns specific to this fault condition
761
410
  self.required_columns = [
762
411
  self.supply_fan_air_volume_col,
@@ -769,96 +418,66 @@ class FaultConditionSix(FaultCondition):
769
418
  self.cooling_sig_col,
770
419
  ]
771
420
 
772
- # Check if any of the required columns are None
773
- if any(col is None for col in self.required_columns):
774
- raise MissingColumnError(
775
- f"{self.error_string}"
776
- f"{self.equation_string}"
777
- f"{self.description_string}"
778
- f"{self.required_column_description}"
779
- f"{self.required_columns}"
780
- )
781
-
782
- # Ensure all required columns are strings
783
- self.required_columns = [str(col) for col in self.required_columns]
784
-
785
- self.mapped_columns = (
786
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
787
- )
788
-
789
- def get_required_columns(self) -> str:
790
- """Returns a string representation of the required columns."""
791
- return (
792
- f"{self.equation_string}"
793
- f"{self.description_string}"
794
- f"{self.required_column_description}"
795
- f"{self.mapped_columns}"
796
- )
797
-
421
+ @FaultConditionMixin._handle_errors
798
422
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
799
- try:
800
- # Ensure all required columns are present
801
- self.check_required_columns(df)
802
-
803
- # Check analog outputs [data with units of %] are floats only
804
- columns_to_check = [
805
- self.supply_vfd_speed_col,
806
- self.economizer_sig_col,
807
- self.heating_sig_col,
808
- self.cooling_sig_col,
809
- ]
810
- self.check_analog_pct(df, columns_to_check)
811
-
812
- # Calculate intermediate values
813
- rat_minus_oat = abs(df[self.rat_col] - df[self.oat_col])
814
- percent_oa_calc = (df[self.mat_col] - df[self.rat_col]) / (
815
- df[self.oat_col] - df[self.rat_col]
816
- )
423
+ """Apply the fault condition to the DataFrame."""
424
+ # Apply common checks
425
+ self._apply_common_checks(df)
426
+
427
+ # Check for zeros in the columns that could lead to division by zero errors
428
+ cols_to_check = [self.rat_col, self.oat_col, self.supply_fan_air_volume_col]
429
+ if df[cols_to_check].eq(0).any().any():
430
+ print(f"Warning: Zero values found in columns: {cols_to_check}")
431
+ print("This may cause division by zero errors.")
432
+ sys.stdout.flush()
817
433
 
818
- # Replace negative values in percent_oa_calc with zero using vectorized operation
819
- percent_oa_calc = percent_oa_calc.clip(lower=0)
434
+ # Check analog outputs [data with units of %] are floats only
435
+ columns_to_check = [
436
+ self.supply_vfd_speed_col,
437
+ self.economizer_sig_col,
438
+ self.heating_sig_col,
439
+ self.cooling_sig_col,
440
+ ]
441
+ self._apply_analog_checks(df, columns_to_check)
820
442
 
821
- perc_OAmin = self.ahu_min_oa_cfm_design / df[self.supply_fan_air_volume_col]
822
- percent_oa_calc_minus_perc_OAmin = abs(percent_oa_calc - perc_OAmin)
443
+ # Calculate intermediate values
444
+ rat_minus_oat = abs(df[self.rat_col] - df[self.oat_col])
445
+ percent_oa_calc = (df[self.mat_col] - df[self.rat_col]) / (
446
+ df[self.oat_col] - df[self.rat_col]
447
+ )
823
448
 
824
- # Combined checks for OS 1 and OS 4 modes
825
- os1_htg_mode_check = (
826
- (rat_minus_oat >= self.oat_rat_delta_min)
827
- & (percent_oa_calc_minus_perc_OAmin > self.airflow_err_thres)
828
- & (df[self.heating_sig_col] > 0.0)
829
- & (df[self.supply_vfd_speed_col] > 0.0)
830
- )
449
+ # Replace negative values in percent_oa_calc with zero using vectorized operation
450
+ percent_oa_calc = percent_oa_calc.clip(lower=0)
831
451
 
832
- os4_clg_mode_check = (
833
- (rat_minus_oat >= self.oat_rat_delta_min)
834
- & (percent_oa_calc_minus_perc_OAmin > self.airflow_err_thres)
835
- & (df[self.heating_sig_col] == 0.0)
836
- & (df[self.cooling_sig_col] > 0.0)
837
- & (df[self.supply_vfd_speed_col] > 0.0)
838
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
839
- )
452
+ perc_OAmin = self.ahu_min_oa_cfm_design / df[self.supply_fan_air_volume_col]
453
+ percent_oa_calc_minus_perc_OAmin = abs(percent_oa_calc - perc_OAmin)
840
454
 
841
- combined_check = os1_htg_mode_check | os4_clg_mode_check
455
+ # Combined checks for OS 1 and OS 4 modes
456
+ os1_htg_mode_check = (
457
+ (rat_minus_oat >= self.oat_rat_delta_min)
458
+ & (percent_oa_calc_minus_perc_OAmin > self.airflow_err_thres)
459
+ & (df[self.heating_sig_col] > 0.0)
460
+ & (df[self.supply_vfd_speed_col] > 0.0)
461
+ )
842
462
 
843
- # Rolling sum to count consecutive trues
844
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
463
+ os4_clg_mode_check = (
464
+ (rat_minus_oat >= self.oat_rat_delta_min)
465
+ & (percent_oa_calc_minus_perc_OAmin > self.airflow_err_thres)
466
+ & (df[self.heating_sig_col] == 0.0)
467
+ & (df[self.cooling_sig_col] > 0.0)
468
+ & (df[self.supply_vfd_speed_col] > 0.0)
469
+ & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
470
+ )
845
471
 
846
- # Set flag to 1 if rolling sum equals the window size
847
- df["fc6_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
472
+ combined_check = os1_htg_mode_check | os4_clg_mode_check
848
473
 
849
- return df
474
+ # Set fault flag
475
+ self._set_fault_flag(df, combined_check, "fc6_flag")
850
476
 
851
- except MissingColumnError as e:
852
- print(f"Error: {e.message}")
853
- sys.stdout.flush()
854
- raise e
855
- except InvalidParameterError as e:
856
- print(f"Error: {e.message}")
857
- sys.stdout.flush()
858
- raise e
477
+ return df
859
478
 
860
479
 
861
- class FaultConditionSeven(FaultCondition):
480
+ class FaultConditionSeven(BaseFaultCondition, FaultConditionMixin):
862
481
  """Class provides the definitions for Fault Condition 7.
863
482
  Very similar to FC 13 but uses heating valve.
864
483
  Supply air temperature too low in full heating.
@@ -866,9 +485,7 @@ class FaultConditionSeven(FaultCondition):
866
485
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc7.py -rP -s
867
486
  """
868
487
 
869
- def __init__(self, dict_):
870
- super().__init__()
871
-
488
+ def _init_specific_attributes(self, dict_):
872
489
  # Threshold parameters
873
490
  self.supply_degf_err_thres = dict_.get("SUPPLY_DEGF_ERR_THRES", None)
874
491
 
@@ -883,9 +500,8 @@ class FaultConditionSeven(FaultCondition):
883
500
  self.sat_setpoint_col = dict_.get("SAT_SETPOINT_COL", None)
884
501
  self.heating_sig_col = dict_.get("HEATING_SIG_COL", None)
885
502
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
886
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
887
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
888
503
 
504
+ # Set documentation strings
889
505
  self.equation_string = (
890
506
  "fc7_flag = 1 if SAT < (SATSP - εSAT) in full heating mode "
891
507
  "and VFD speed > 0 for N consecutive values else 0 \n"
@@ -900,8 +516,6 @@ class FaultConditionSeven(FaultCondition):
900
516
  )
901
517
  self.error_string = "One or more required columns are missing or None \n"
902
518
 
903
- self.set_attributes(dict_)
904
-
905
519
  # Set required columns specific to this fault condition
906
520
  self.required_columns = [
907
521
  self.sat_col,
@@ -910,69 +524,32 @@ class FaultConditionSeven(FaultCondition):
910
524
  self.supply_vfd_speed_col,
911
525
  ]
912
526
 
913
- # Check if any of the required columns are None
914
- if any(col is None for col in self.required_columns):
915
- raise MissingColumnError(
916
- f"{self.error_string}"
917
- f"{self.equation_string}"
918
- f"{self.description_string}"
919
- f"{self.required_column_description}"
920
- f"{self.required_columns}"
921
- )
922
-
923
- # Ensure all required columns are strings
924
- self.required_columns = [str(col) for col in self.required_columns]
925
-
926
- self.mapped_columns = (
927
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
928
- )
929
-
930
- def get_required_columns(self) -> str:
931
- """Returns a string representation of the required columns."""
932
- return (
933
- f"{self.equation_string}"
934
- f"{self.description_string}"
935
- f"{self.required_column_description}"
936
- f"{self.mapped_columns}"
937
- )
938
-
527
+ @FaultConditionMixin._handle_errors
939
528
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
940
- try:
941
- # Ensure all required columns are present
942
- self.check_required_columns(df)
529
+ """Apply the fault condition to the DataFrame."""
530
+ # Apply common checks
531
+ self._apply_common_checks(df)
943
532
 
944
- # Check analog outputs [data with units of %] are floats only
945
- columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
946
- self.check_analog_pct(df, columns_to_check)
533
+ # Check analog outputs [data with units of %] are floats only
534
+ columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
535
+ self._apply_analog_checks(df, columns_to_check)
947
536
 
948
- # Perform checks
949
- sat_check = df[self.sat_setpoint_col] - self.supply_degf_err_thres
537
+ # Perform checks
538
+ sat_check = df[self.sat_setpoint_col] - self.supply_degf_err_thres
950
539
 
951
- combined_check = (
952
- (df[self.sat_col] < sat_check)
953
- & (df[self.heating_sig_col] > 0.9)
954
- & (df[self.supply_vfd_speed_col] > 0)
955
- )
956
-
957
- # Rolling sum to count consecutive trues
958
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
959
-
960
- # Set flag to 1 if rolling sum equals the window size
961
- df["fc7_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
540
+ combined_check = (
541
+ (df[self.sat_col] < sat_check)
542
+ & (df[self.heating_sig_col] > 0.9)
543
+ & (df[self.supply_vfd_speed_col] > 0)
544
+ )
962
545
 
963
- return df
546
+ # Set fault flag
547
+ self._set_fault_flag(df, combined_check, "fc7_flag")
964
548
 
965
- except MissingColumnError as e:
966
- print(f"Error: {e.message}")
967
- sys.stdout.flush()
968
- raise e
969
- except InvalidParameterError as e:
970
- print(f"Error: {e.message}")
971
- sys.stdout.flush()
972
- raise e
549
+ return df
973
550
 
974
551
 
975
- class FaultConditionEight(FaultCondition):
552
+ class FaultConditionEight(BaseFaultCondition, FaultConditionMixin):
976
553
  """Class provides the definitions for Fault Condition 8.
977
554
  Supply air temperature and mix air temperature should
978
555
  be approx equal in economizer mode.
@@ -980,9 +557,7 @@ class FaultConditionEight(FaultCondition):
980
557
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc8.py -rP -s
981
558
  """
982
559
 
983
- def __init__(self, dict_):
984
- super().__init__()
985
-
560
+ def _init_specific_attributes(self, dict_):
986
561
  # Threshold parameters
987
562
  self.delta_t_supply_fan = dict_.get("DELTA_T_SUPPLY_FAN", None)
988
563
  self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
@@ -1006,9 +581,8 @@ class FaultConditionEight(FaultCondition):
1006
581
  self.sat_col = dict_.get("SAT_COL", None)
1007
582
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1008
583
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
1009
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1010
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1011
584
 
585
+ # Set documentation strings
1012
586
  self.equation_string = (
1013
587
  "fc8_flag = 1 if |SAT - MAT - ΔT_fan| > √(εSAT² + εMAT²) "
1014
588
  "in economizer mode for N consecutive values else 0 \n"
@@ -1059,51 +633,42 @@ class FaultConditionEight(FaultCondition):
1059
633
  f"{self.mapped_columns}"
1060
634
  )
1061
635
 
636
+ @FaultConditionMixin._handle_errors
1062
637
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1063
- try:
1064
- # Ensure all required columns are present
1065
- self.check_required_columns(df)
1066
-
1067
- # Check analog outputs [data with units of %] are floats only
1068
- columns_to_check = [
1069
- self.economizer_sig_col,
1070
- self.cooling_sig_col,
1071
- ]
1072
- self.check_analog_pct(df, columns_to_check)
1073
-
1074
- # Perform checks
1075
- sat_fan_mat = abs(
1076
- df[self.sat_col] - self.delta_t_supply_fan - df[self.mat_col]
1077
- )
1078
- sat_mat_sqrted = np.sqrt(
1079
- self.supply_degf_err_thres**2 + self.mix_degf_err_thres**2
1080
- )
638
+ """Apply the fault condition to the DataFrame."""
639
+ # Apply common checks
640
+ self._apply_common_checks(df)
1081
641
 
1082
- combined_check = (
1083
- (sat_fan_mat > sat_mat_sqrted)
1084
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
1085
- & (df[self.cooling_sig_col] < 0.1)
1086
- )
642
+ # Check analog outputs [data with units of %] are floats only
643
+ columns_to_check = [
644
+ self.economizer_sig_col,
645
+ self.cooling_sig_col,
646
+ ]
647
+ self._apply_analog_checks(df, columns_to_check)
1087
648
 
1088
- # Rolling sum to count consecutive trues
1089
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
649
+ # Perform checks
650
+ sat_fan_mat = abs(df[self.sat_col] - self.delta_t_supply_fan - df[self.mat_col])
651
+ sat_mat_sqrted = np.sqrt(
652
+ self.supply_degf_err_thres**2 + self.mix_degf_err_thres**2
653
+ )
1090
654
 
1091
- # Set flag to 1 if rolling sum equals the window size
1092
- df["fc8_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
655
+ combined_check = (
656
+ (sat_fan_mat > sat_mat_sqrted)
657
+ # Verify AHU is running in OS 3 cooling mode with minimum OA
658
+ & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
659
+ & (df[self.cooling_sig_col] < 0.1)
660
+ )
1093
661
 
1094
- return df
662
+ # Rolling sum to count consecutive trues
663
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1095
664
 
1096
- except MissingColumnError as e:
1097
- print(f"Error: {e.message}")
1098
- sys.stdout.flush()
1099
- raise e
1100
- except InvalidParameterError as e:
1101
- print(f"Error: {e.message}")
1102
- sys.stdout.flush()
1103
- raise e
665
+ # Set flag to 1 if rolling sum equals the window size
666
+ df["fc8_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
667
+
668
+ return df
1104
669
 
1105
670
 
1106
- class FaultConditionNine(FaultCondition):
671
+ class FaultConditionNine(BaseFaultCondition, FaultConditionMixin):
1107
672
  """Class provides the definitions for Fault Condition 9.
1108
673
  Outside air temperature too high in free cooling without
1109
674
  additional mechanical cooling in economizer mode.
@@ -1111,9 +676,7 @@ class FaultConditionNine(FaultCondition):
1111
676
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc9.py -rP -s
1112
677
  """
1113
678
 
1114
- def __init__(self, dict_):
1115
- super().__init__()
1116
-
679
+ def _init_specific_attributes(self, dict_):
1117
680
  # Threshold parameters
1118
681
  self.delta_t_supply_fan = dict_.get("DELTA_T_SUPPLY_FAN", None)
1119
682
  self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
@@ -1137,9 +700,8 @@ class FaultConditionNine(FaultCondition):
1137
700
  self.oat_col = dict_.get("OAT_COL", None)
1138
701
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
1139
702
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1140
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1141
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1142
703
 
704
+ # Set documentation strings
1143
705
  self.equation_string = (
1144
706
  "fc9_flag = 1 if OAT > (SATSP - ΔT_fan + εSAT) "
1145
707
  "in free cooling mode for N consecutive values else 0 \n"
@@ -1154,8 +716,6 @@ class FaultConditionNine(FaultCondition):
1154
716
  )
1155
717
  self.error_string = "One or more required columns are missing or None \n"
1156
718
 
1157
- self.set_attributes(dict_)
1158
-
1159
719
  # Set required columns specific to this fault condition
1160
720
  self.required_columns = [
1161
721
  self.sat_setpoint_col,
@@ -1164,78 +724,41 @@ class FaultConditionNine(FaultCondition):
1164
724
  self.economizer_sig_col,
1165
725
  ]
1166
726
 
1167
- # Check if any of the required columns are None
1168
- if any(col is None for col in self.required_columns):
1169
- raise MissingColumnError(
1170
- f"{self.error_string}"
1171
- f"{self.equation_string}"
1172
- f"{self.description_string}"
1173
- f"{self.required_column_description}"
1174
- f"{self.required_columns}"
1175
- )
727
+ @FaultConditionMixin._handle_errors
728
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
729
+ """Apply the fault condition to the DataFrame."""
730
+ # Apply common checks
731
+ self._apply_common_checks(df)
1176
732
 
1177
- # Ensure all required columns are strings
1178
- self.required_columns = [str(col) for col in self.required_columns]
733
+ # Check analog outputs [data with units of %] are floats only
734
+ columns_to_check = [
735
+ self.economizer_sig_col,
736
+ self.cooling_sig_col,
737
+ ]
738
+ self._apply_analog_checks(df, columns_to_check)
1179
739
 
1180
- self.mapped_columns = (
1181
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
740
+ # Perform calculations
741
+ oat_minus_oaterror = df[self.oat_col] - self.outdoor_degf_err_thres
742
+ satsp_delta_saterr = (
743
+ df[self.sat_setpoint_col]
744
+ - self.delta_t_supply_fan
745
+ + self.supply_degf_err_thres
1182
746
  )
1183
747
 
1184
- def get_required_columns(self) -> str:
1185
- """Returns a string representation of the required columns."""
1186
- return (
1187
- f"{self.equation_string}"
1188
- f"{self.description_string}"
1189
- f"{self.required_column_description}"
1190
- f"{self.mapped_columns}"
748
+ combined_check = (
749
+ (oat_minus_oaterror > satsp_delta_saterr)
750
+ # verify AHU is in OS2 only free cooling mode
751
+ & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
752
+ & (df[self.cooling_sig_col] < 0.1)
1191
753
  )
1192
754
 
1193
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1194
- try:
1195
- # Ensure all required columns are present
1196
- self.check_required_columns(df)
1197
-
1198
- # Check analog outputs [data with units of %] are floats only
1199
- columns_to_check = [
1200
- self.economizer_sig_col,
1201
- self.cooling_sig_col,
1202
- ]
1203
- self.check_analog_pct(df, columns_to_check)
1204
-
1205
- # Perform calculations
1206
- oat_minus_oaterror = df[self.oat_col] - self.outdoor_degf_err_thres
1207
- satsp_delta_saterr = (
1208
- df[self.sat_setpoint_col]
1209
- - self.delta_t_supply_fan
1210
- + self.supply_degf_err_thres
1211
- )
1212
-
1213
- combined_check = (
1214
- (oat_minus_oaterror > satsp_delta_saterr)
1215
- # verify AHU is in OS2 only free cooling mode
1216
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
1217
- & (df[self.cooling_sig_col] < 0.1)
1218
- )
755
+ # Set fault flag
756
+ self._set_fault_flag(df, combined_check, "fc9_flag")
1219
757
 
1220
- # Rolling sum to count consecutive trues
1221
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1222
-
1223
- # Set flag to 1 if rolling sum equals the window size
1224
- df["fc9_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1225
-
1226
- return df
1227
-
1228
- except MissingColumnError as e:
1229
- print(f"Error: {e.message}")
1230
- sys.stdout.flush()
1231
- raise e
1232
- except InvalidParameterError as e:
1233
- print(f"Error: {e.message}")
1234
- sys.stdout.flush()
1235
- raise e
758
+ return df
1236
759
 
1237
760
 
1238
- class FaultConditionTen(FaultCondition):
761
+ class FaultConditionTen(BaseFaultCondition, FaultConditionMixin):
1239
762
  """Class provides the definitions for Fault Condition 10.
1240
763
  Outdoor air temperature and mix air temperature should
1241
764
  be approx equal in economizer plus mech cooling mode.
@@ -1243,9 +766,7 @@ class FaultConditionTen(FaultCondition):
1243
766
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc10.py -rP -s
1244
767
  """
1245
768
 
1246
- def __init__(self, dict_):
1247
- super().__init__()
1248
-
769
+ def _init_specific_attributes(self, dict_):
1249
770
  # Threshold parameters
1250
771
  self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
1251
772
  self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
@@ -1265,9 +786,8 @@ class FaultConditionTen(FaultCondition):
1265
786
  self.mat_col = dict_.get("MAT_COL", None)
1266
787
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
1267
788
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1268
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1269
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1270
789
 
790
+ # Set documentation strings
1271
791
  self.equation_string = (
1272
792
  "fc10_flag = 1 if |OAT - MAT| > √(εOAT² + εMAT²) in "
1273
793
  "economizer + mech cooling mode for N consecutive values else 0 \n"
@@ -1282,8 +802,6 @@ class FaultConditionTen(FaultCondition):
1282
802
  )
1283
803
  self.error_string = "One or more required columns are missing or None \n"
1284
804
 
1285
- self.set_attributes(dict_)
1286
-
1287
805
  # Set required columns specific to this fault condition
1288
806
  self.required_columns = [
1289
807
  self.oat_col,
@@ -1292,97 +810,55 @@ class FaultConditionTen(FaultCondition):
1292
810
  self.economizer_sig_col,
1293
811
  ]
1294
812
 
1295
- # Check if any of the required columns are None
1296
- if any(col is None for col in self.required_columns):
1297
- raise MissingColumnError(
1298
- f"{self.error_string}"
1299
- f"{self.equation_string}"
1300
- f"{self.description_string}"
1301
- f"{self.required_column_description}"
1302
- f"{self.required_columns}"
1303
- )
813
+ @FaultConditionMixin._handle_errors
814
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
815
+ """Apply the fault condition to the DataFrame."""
816
+ # Apply common checks
817
+ self._apply_common_checks(df)
1304
818
 
1305
- # Ensure all required columns are strings
1306
- self.required_columns = [str(col) for col in self.required_columns]
819
+ # Check analog outputs [data with units of %] are floats only
820
+ columns_to_check = [
821
+ self.economizer_sig_col,
822
+ self.cooling_sig_col,
823
+ ]
824
+ self._apply_analog_checks(df, columns_to_check)
1307
825
 
1308
- self.mapped_columns = (
1309
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
826
+ # Perform calculations
827
+ abs_mat_minus_oat = abs(df[self.mat_col] - df[self.oat_col])
828
+ mat_oat_sqrted = np.sqrt(
829
+ self.mix_degf_err_thres**2 + self.outdoor_degf_err_thres**2
1310
830
  )
1311
831
 
1312
- def get_required_columns(self) -> str:
1313
- """Returns a string representation of the required columns."""
1314
- return (
1315
- f"{self.equation_string}"
1316
- f"{self.description_string}"
1317
- f"{self.required_column_description}"
1318
- f"{self.mapped_columns}"
832
+ combined_check = (
833
+ (abs_mat_minus_oat > mat_oat_sqrted)
834
+ # Verify AHU is running in OS 3 cooling mode with minimum OA
835
+ & (df[self.cooling_sig_col] > 0.01)
836
+ & (df[self.economizer_sig_col] > 0.9)
1319
837
  )
1320
838
 
1321
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1322
- try:
1323
- # Ensure all required columns are present
1324
- self.check_required_columns(df)
1325
-
1326
- # Check analog outputs [data with units of %] are floats only
1327
- columns_to_check = [
1328
- self.economizer_sig_col,
1329
- self.cooling_sig_col,
1330
- ]
1331
- self.check_analog_pct(df, columns_to_check)
1332
-
1333
- # Perform calculations
1334
- abs_mat_minus_oat = abs(df[self.mat_col] - df[self.oat_col])
1335
- mat_oat_sqrted = np.sqrt(
1336
- self.mix_degf_err_thres**2 + self.outdoor_degf_err_thres**2
1337
- )
839
+ # Set fault flag
840
+ self._set_fault_flag(df, combined_check, "fc10_flag")
1338
841
 
1339
- combined_check = (
1340
- (abs_mat_minus_oat > mat_oat_sqrted)
1341
- # Verify AHU is running in OS 3 cooling mode with minimum OA
1342
- & (df[self.cooling_sig_col] > 0.01)
1343
- & (df[self.economizer_sig_col] > 0.9)
1344
- )
1345
-
1346
- # Rolling sum to count consecutive trues
1347
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1348
-
1349
- # Set flag to 1 if rolling sum equals the window size
1350
- df["fc10_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1351
-
1352
- return df
1353
-
1354
- except MissingColumnError as e:
1355
- print(f"Error: {e.message}")
1356
- sys.stdout.flush()
1357
- raise e
1358
- except InvalidParameterError as e:
1359
- print(f"Error: {e.message}")
1360
- sys.stdout.flush()
1361
- raise e
842
+ return df
1362
843
 
1363
844
 
1364
- class FaultConditionEleven(FaultCondition):
845
+ class FaultConditionEleven(BaseFaultCondition, FaultConditionMixin):
1365
846
  """Class provides the definitions for Fault Condition 11.
1366
- Outside air temperature too low for 100% outdoor
1367
- air cooling in economizer cooling mode.
1368
- Economizer performance fault
847
+ Outdoor air temperature and mix air temperature should
848
+ be approx equal in economizer mode.
1369
849
 
1370
850
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc11.py -rP -s
1371
851
  """
1372
852
 
1373
- def __init__(self, dict_):
1374
- super().__init__()
1375
-
853
+ def _init_specific_attributes(self, dict_):
1376
854
  # Threshold parameters
1377
- self.delta_t_supply_fan = dict_.get("DELTA_T_SUPPLY_FAN", None)
1378
855
  self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
1379
- self.supply_degf_err_thres = dict_.get("SUPPLY_DEGF_ERR_THRES", None)
856
+ self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
1380
857
 
1381
858
  # Validate that threshold parameters are floats
1382
859
  for param, value in [
1383
- ("delta_t_supply_fan", self.delta_t_supply_fan),
1384
860
  ("outdoor_degf_err_thres", self.outdoor_degf_err_thres),
1385
- ("supply_degf_err_thres", self.supply_degf_err_thres),
861
+ ("mix_degf_err_thres", self.mix_degf_err_thres),
1386
862
  ]:
1387
863
  if not isinstance(value, float):
1388
864
  raise InvalidParameterError(
@@ -1390,123 +866,74 @@ class FaultConditionEleven(FaultCondition):
1390
866
  )
1391
867
 
1392
868
  # Other attributes
1393
- self.sat_setpoint_col = dict_.get("SAT_SETPOINT_COL", None)
1394
869
  self.oat_col = dict_.get("OAT_COL", None)
1395
- self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
870
+ self.mat_col = dict_.get("MAT_COL", None)
1396
871
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1397
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1398
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1399
-
1400
- self.equation_string = (
1401
- "fc11_flag = 1 if OAT < (SATSP - ΔT_fan - εSAT) in "
1402
- "economizer cooling mode for N consecutive values else 0 \n"
1403
- )
1404
- self.description_string = (
1405
- "Fault Condition 11: Outside air temperature too low for 100% outdoor air cooling "
1406
- "in economizer cooling mode (Economizer performance fault) \n"
1407
- )
1408
- self.required_column_description = (
1409
- "Required inputs are the supply air temperature setpoint, outside air temperature, "
1410
- "cooling signal, and economizer signal \n"
1411
- )
1412
- self.error_string = "One or more required columns are missing or None \n"
1413
-
1414
- self.set_attributes(dict_)
1415
872
 
1416
- # Set required columns specific to this fault condition
1417
- self.required_columns = [
1418
- self.sat_setpoint_col,
1419
- self.oat_col,
1420
- self.cooling_sig_col,
1421
- self.economizer_sig_col,
1422
- ]
1423
-
1424
- # Check if any of the required columns are None
1425
- if any(col is None for col in self.required_columns):
1426
- raise MissingColumnError(
1427
- f"{self.error_string}"
1428
- f"{self.equation_string}"
1429
- f"{self.description_string}"
1430
- f"{self.required_column_description}"
1431
- f"{self.required_columns}"
1432
- )
1433
-
1434
- # Ensure all required columns are strings
1435
- self.required_columns = [str(col) for col in self.required_columns]
1436
-
1437
- self.mapped_columns = (
1438
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
1439
- )
1440
-
1441
- def get_required_columns(self) -> str:
1442
- """Returns a string representation of the required columns."""
1443
- return (
1444
- f"{self.equation_string}"
1445
- f"{self.description_string}"
1446
- f"{self.required_column_description}"
1447
- f"{self.mapped_columns}"
873
+ # Set documentation strings
874
+ self.equation_string = (
875
+ "fc11_flag = 1 if |OAT - MAT| > √(εOAT² + εMAT²) in "
876
+ "economizer mode for N consecutive values else 0 \n"
877
+ )
878
+ self.description_string = (
879
+ "Fault Condition 11: Outdoor air temperature and mixed air temperature "
880
+ "should be approximately equal in economizer mode \n"
881
+ )
882
+ self.required_column_description = (
883
+ "Required inputs are the outside air temperature, mixed air temperature, "
884
+ "and economizer signal \n"
1448
885
  )
886
+ self.error_string = "One or more required columns are missing or None \n"
887
+
888
+ # Set required columns specific to this fault condition
889
+ self.required_columns = [
890
+ self.oat_col,
891
+ self.mat_col,
892
+ self.economizer_sig_col,
893
+ ]
1449
894
 
895
+ @FaultConditionMixin._handle_errors
1450
896
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1451
- try:
1452
- # Ensure all required columns are present
1453
- self.check_required_columns(df)
1454
-
1455
- # Check analog outputs [data with units of %] are floats only
1456
- columns_to_check = [
1457
- self.economizer_sig_col,
1458
- self.cooling_sig_col,
1459
- ]
1460
- self.check_analog_pct(df, columns_to_check)
1461
-
1462
- # Perform calculations without creating DataFrame columns
1463
- oat_plus_oaterror = df[self.oat_col] + self.outdoor_degf_err_thres
1464
- satsp_delta_saterr = (
1465
- df[self.sat_setpoint_col]
1466
- - self.delta_t_supply_fan
1467
- - self.supply_degf_err_thres
1468
- )
897
+ """Apply the fault condition to the DataFrame."""
898
+ # Apply common checks
899
+ self._apply_common_checks(df)
1469
900
 
1470
- combined_check = (
1471
- (oat_plus_oaterror < satsp_delta_saterr)
1472
- # Verify AHU is running in OS 3 cooling mode with 100% OA
1473
- & (df[self.cooling_sig_col] > 0.01)
1474
- & (df[self.economizer_sig_col] > 0.9)
1475
- )
901
+ # Check analog outputs [data with units of %] are floats only
902
+ columns_to_check = [self.economizer_sig_col]
903
+ self._apply_analog_checks(df, columns_to_check)
1476
904
 
1477
- # Rolling sum to count consecutive trues
1478
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
905
+ # Perform calculations
906
+ abs_mat_minus_oat = abs(df[self.mat_col] - df[self.oat_col])
907
+ mat_oat_sqrted = np.sqrt(
908
+ self.mix_degf_err_thres**2 + self.outdoor_degf_err_thres**2
909
+ )
1479
910
 
1480
- # Set flag to 1 if rolling sum equals the window size
1481
- df["fc11_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
911
+ combined_check = (
912
+ (abs_mat_minus_oat > mat_oat_sqrted)
913
+ # Verify AHU is running in economizer mode
914
+ & (df[self.economizer_sig_col] > 0.9)
915
+ )
1482
916
 
1483
- return df
917
+ # Set fault flag
918
+ self._set_fault_flag(df, combined_check, "fc11_flag")
1484
919
 
1485
- except MissingColumnError as e:
1486
- print(f"Error: {e.message}")
1487
- sys.stdout.flush()
1488
- raise e
1489
- except InvalidParameterError as e:
1490
- print(f"Error: {e.message}")
1491
- sys.stdout.flush()
1492
- raise e
920
+ return df
1493
921
 
1494
922
 
1495
- class FaultConditionTwelve(FaultCondition):
923
+ class FaultConditionTwelve(BaseFaultCondition, FaultConditionMixin):
1496
924
  """Class provides the definitions for Fault Condition 12.
1497
- Supply air temperature too high; should be less than
1498
- mix air temperature in economizer plus mech cooling mode.
925
+ Supply air temperature too high; should be less than mixed air temperature
926
+ in OS3 (economizer + mechanical cooling) and OS4 (mechanical cooling only) modes.
1499
927
 
1500
928
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc12.py -rP -s
1501
929
  """
1502
930
 
1503
- def __init__(self, dict_):
1504
- super().__init__()
1505
-
931
+ def _init_specific_attributes(self, dict_):
1506
932
  # Threshold parameters
1507
933
  self.delta_t_supply_fan = dict_.get("DELTA_T_SUPPLY_FAN", None)
1508
934
  self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
1509
935
  self.supply_degf_err_thres = dict_.get("SUPPLY_DEGF_ERR_THRES", None)
936
+ self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
1510
937
  self.ahu_min_oa_dpr = dict_.get("AHU_MIN_OA_DPR", None)
1511
938
 
1512
939
  # Validate that threshold parameters are floats
@@ -1514,6 +941,7 @@ class FaultConditionTwelve(FaultCondition):
1514
941
  ("delta_t_supply_fan", self.delta_t_supply_fan),
1515
942
  ("mix_degf_err_thres", self.mix_degf_err_thres),
1516
943
  ("supply_degf_err_thres", self.supply_degf_err_thres),
944
+ ("outdoor_degf_err_thres", self.outdoor_degf_err_thres),
1517
945
  ("ahu_min_oa_dpr", self.ahu_min_oa_dpr),
1518
946
  ]:
1519
947
  if not isinstance(value, float):
@@ -1524,129 +952,99 @@ class FaultConditionTwelve(FaultCondition):
1524
952
  # Other attributes
1525
953
  self.sat_col = dict_.get("SAT_COL", None)
1526
954
  self.mat_col = dict_.get("MAT_COL", None)
955
+ self.oat_col = dict_.get("OAT_COL", None)
1527
956
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
1528
957
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1529
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1530
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1531
958
 
959
+ # Set documentation strings
1532
960
  self.equation_string = (
1533
- "fc12_flag = 1 if SAT >= MAT + εMAT in "
1534
- "economizer + mech cooling mode for N consecutive values else 0 \n"
961
+ "fc12_flag = 1 if (SAT > MAT + εSAT) and "
962
+ "((CLG > 0 and ECO > 0.9) or (CLG > 0.9 and ECO = MIN_OA)) "
963
+ "for N consecutive values else 0 \n"
1535
964
  )
1536
965
  self.description_string = (
1537
966
  "Fault Condition 12: Supply air temperature too high; should be less than "
1538
- "mixed air temperature in economizer plus mechanical cooling mode \n"
967
+ "mixed air temperature in OS3 (economizer + mechanical cooling) and "
968
+ "OS4 (mechanical cooling only) modes \n"
1539
969
  )
1540
970
  self.required_column_description = (
1541
971
  "Required inputs are the supply air temperature, mixed air temperature, "
1542
- "cooling signal, and economizer signal \n"
972
+ "outside air temperature, cooling signal, and economizer signal \n"
1543
973
  )
1544
974
  self.error_string = "One or more required columns are missing or None \n"
1545
975
 
1546
- self.set_attributes(dict_)
1547
-
1548
976
  # Set required columns specific to this fault condition
1549
977
  self.required_columns = [
1550
978
  self.sat_col,
1551
979
  self.mat_col,
980
+ self.oat_col,
1552
981
  self.cooling_sig_col,
1553
982
  self.economizer_sig_col,
1554
983
  ]
1555
984
 
1556
- # Check if any of the required columns are None
1557
- if any(col is None for col in self.required_columns):
1558
- raise MissingColumnError(
1559
- f"{self.error_string}"
1560
- f"{self.equation_string}"
1561
- f"{self.description_string}"
1562
- f"{self.required_column_description}"
1563
- f"{self.required_columns}"
1564
- )
985
+ @FaultConditionMixin._handle_errors
986
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
987
+ """Apply the fault condition to the DataFrame."""
988
+ # Apply common checks
989
+ self._apply_common_checks(df)
1565
990
 
1566
- # Ensure all required columns are strings
1567
- self.required_columns = [str(col) for col in self.required_columns]
991
+ # Check analog outputs [data with units of %] are floats only
992
+ columns_to_check = [
993
+ self.economizer_sig_col,
994
+ self.cooling_sig_col,
995
+ ]
996
+ self._apply_analog_checks(df, columns_to_check)
1568
997
 
1569
- self.mapped_columns = (
1570
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
998
+ # Calculate the threshold for SAT vs MAT comparison
999
+ sat_mat_threshold = np.sqrt(
1000
+ self.supply_degf_err_thres**2 + self.mix_degf_err_thres**2
1571
1001
  )
1572
1002
 
1573
- def get_required_columns(self) -> str:
1574
- """Returns a string representation of the required columns."""
1575
- return (
1576
- f"{self.equation_string}"
1577
- f"{self.description_string}"
1578
- f"{self.required_column_description}"
1579
- f"{self.mapped_columns}"
1003
+ # Check if SAT is too high compared to MAT (accounting for supply fan heat)
1004
+ sat_too_high = df[self.sat_col] > (
1005
+ df[self.mat_col] + sat_mat_threshold + self.delta_t_supply_fan
1580
1006
  )
1581
1007
 
1582
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1583
- try:
1584
- # Ensure all required columns are present
1585
- self.check_required_columns(df)
1586
-
1587
- # Check analog outputs [data with units of %] are floats only
1588
- columns_to_check = [
1589
- self.economizer_sig_col,
1590
- self.cooling_sig_col,
1591
- ]
1592
- self.check_analog_pct(df, columns_to_check)
1593
-
1594
- # Perform calculations without creating DataFrame columns
1595
- sat_minus_saterr_delta_supply_fan = (
1596
- df[self.sat_col] - self.supply_degf_err_thres - self.delta_t_supply_fan
1597
- )
1598
- mat_plus_materr = df[self.mat_col] + self.mix_degf_err_thres
1599
-
1600
- # Combined check without adding to DataFrame columns
1601
- combined_check = operator.or_(
1602
- # OS4 AHU state cooling @ min OA
1603
- (sat_minus_saterr_delta_supply_fan > mat_plus_materr)
1604
- # Verify AHU in OS4 mode
1605
- & (df[self.cooling_sig_col] > 0.01)
1606
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr),
1607
- # OR
1608
- (sat_minus_saterr_delta_supply_fan > mat_plus_materr)
1609
- # Verify AHU is running in OS3 cooling mode in 100% OA
1610
- & (df[self.cooling_sig_col] > 0.01)
1611
- & (df[self.economizer_sig_col] > 0.9),
1612
- )
1008
+ # Check operating modes:
1009
+ # OS3: Economizer + mechanical cooling (ECO > 0.9 and CLG > 0)
1010
+ os3_mode = (df[self.economizer_sig_col] > 0.9) & (df[self.cooling_sig_col] > 0)
1613
1011
 
1614
- # Rolling sum to count consecutive trues
1615
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1012
+ # OS4: Mechanical cooling only (ECO = MIN_OA and CLG > 0.9)
1013
+ os4_mode = (df[self.economizer_sig_col] <= self.ahu_min_oa_dpr) & (
1014
+ df[self.cooling_sig_col] > 0.9
1015
+ )
1616
1016
 
1617
- # Set flag to 1 if rolling sum equals the window size
1618
- df["fc12_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1017
+ # Combine conditions:
1018
+ # Fault occurs when SAT is too high in either OS3 or OS4 mode
1019
+ combined_check = sat_too_high & (os3_mode | os4_mode)
1619
1020
 
1620
- return df
1021
+ # Set fault flag
1022
+ self._set_fault_flag(df, combined_check, "fc12_flag")
1621
1023
 
1622
- except MissingColumnError as e:
1623
- print(f"Error: {e.message}")
1624
- sys.stdout.flush()
1625
- raise e
1626
- except InvalidParameterError as e:
1627
- print(f"Error: {e.message}")
1628
- sys.stdout.flush()
1629
- raise e
1024
+ return df
1630
1025
 
1631
1026
 
1632
- class FaultConditionThirteen(FaultCondition):
1027
+ class FaultConditionThirteen(BaseFaultCondition, FaultConditionMixin):
1633
1028
  """Class provides the definitions for Fault Condition 13.
1634
- Supply air temperature too high in full cooling
1635
- in economizer plus mech cooling mode
1029
+ Supply air temperature too high in full cooling mode.
1030
+ This fault checks if SAT is too high compared to SAT setpoint
1031
+ in OS3 (economizer + mechanical cooling) and OS4 (mechanical cooling only) modes.
1636
1032
 
1637
1033
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc13.py -rP -s
1638
1034
  """
1639
1035
 
1640
- def __init__(self, dict_):
1641
- super().__init__()
1642
-
1036
+ def _init_specific_attributes(self, dict_):
1643
1037
  # Threshold parameters
1644
1038
  self.supply_degf_err_thres = dict_.get("SUPPLY_DEGF_ERR_THRES", None)
1039
+ self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
1040
+ self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
1645
1041
  self.ahu_min_oa_dpr = dict_.get("AHU_MIN_OA_DPR", None)
1646
1042
 
1647
1043
  # Validate that threshold parameters are floats
1648
1044
  for param, value in [
1649
1045
  ("supply_degf_err_thres", self.supply_degf_err_thres),
1046
+ ("mix_degf_err_thres", self.mix_degf_err_thres),
1047
+ ("outdoor_degf_err_thres", self.outdoor_degf_err_thres),
1650
1048
  ("ahu_min_oa_dpr", self.ahu_min_oa_dpr),
1651
1049
  ]:
1652
1050
  if not isinstance(value, float):
@@ -1656,19 +1054,19 @@ class FaultConditionThirteen(FaultCondition):
1656
1054
 
1657
1055
  # Other attributes
1658
1056
  self.sat_col = dict_.get("SAT_COL", None)
1659
- self.sat_setpoint_col = dict_.get("SAT_SETPOINT_COL", None)
1057
+ self.sat_sp_col = dict_.get("SAT_SP_COL", None)
1660
1058
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
1661
1059
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1662
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1663
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1664
1060
 
1061
+ # Set documentation strings
1665
1062
  self.equation_string = (
1666
- "fc13_flag = 1 if SAT > (SATSP + εSAT) in "
1667
- "economizer + mech cooling mode for N consecutive values else 0 \n"
1063
+ "fc13_flag = 1 if (SAT > SATSP + εSAT) and "
1064
+ "((CLG > 0.9 and ECO > 0.9) or (CLG > 0.9 and ECO = MIN_OA)) "
1065
+ "for N consecutive values else 0 \n"
1668
1066
  )
1669
1067
  self.description_string = (
1670
- "Fault Condition 13: Supply air temperature too high in full cooling "
1671
- "in economizer plus mechanical cooling mode \n"
1068
+ "Fault Condition 13: Supply air temperature too high in full cooling mode "
1069
+ "in OS3 (economizer + mechanical cooling) and OS4 (mechanical cooling only) modes \n"
1672
1070
  )
1673
1071
  self.required_column_description = (
1674
1072
  "Required inputs are the supply air temperature, supply air temperature setpoint, "
@@ -1676,111 +1074,75 @@ class FaultConditionThirteen(FaultCondition):
1676
1074
  )
1677
1075
  self.error_string = "One or more required columns are missing or None \n"
1678
1076
 
1679
- self.set_attributes(dict_)
1680
-
1681
1077
  # Set required columns specific to this fault condition
1682
1078
  self.required_columns = [
1683
1079
  self.sat_col,
1684
- self.sat_setpoint_col,
1080
+ self.sat_sp_col,
1685
1081
  self.cooling_sig_col,
1686
1082
  self.economizer_sig_col,
1687
1083
  ]
1688
1084
 
1689
- # Check if any of the required columns are None
1690
- if any(col is None for col in self.required_columns):
1691
- raise MissingColumnError(
1692
- f"{self.error_string}"
1693
- f"{self.equation_string}"
1694
- f"{self.description_string}"
1695
- f"{self.required_column_description}"
1696
- f"{self.required_columns}"
1697
- )
1085
+ @FaultConditionMixin._handle_errors
1086
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1087
+ """Apply the fault condition to the DataFrame."""
1088
+ # Apply common checks
1089
+ self._apply_common_checks(df)
1698
1090
 
1699
- # Ensure all required columns are strings
1700
- self.required_columns = [str(col) for col in self.required_columns]
1091
+ # Check analog outputs [data with units of %] are floats only
1092
+ columns_to_check = [
1093
+ self.economizer_sig_col,
1094
+ self.cooling_sig_col,
1095
+ ]
1096
+ self._apply_analog_checks(df, columns_to_check)
1701
1097
 
1702
- self.mapped_columns = (
1703
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
1098
+ # Check if SAT is too high compared to setpoint
1099
+ sat_too_high = df[self.sat_col] > (
1100
+ df[self.sat_sp_col] + self.supply_degf_err_thres
1704
1101
  )
1705
1102
 
1706
- def get_required_columns(self) -> str:
1707
- """Returns a string representation of the required columns."""
1708
- return (
1709
- f"{self.equation_string}"
1710
- f"{self.description_string}"
1711
- f"{self.required_column_description}"
1712
- f"{self.mapped_columns}"
1103
+ # Check operating modes:
1104
+ # OS3: Economizer + full mechanical cooling (ECO > 0.9 and CLG > 0.9)
1105
+ os3_mode = (df[self.economizer_sig_col] > 0.9) & (
1106
+ df[self.cooling_sig_col] > 0.9
1713
1107
  )
1714
1108
 
1715
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1716
- try:
1717
- # Ensure all required columns are present
1718
- self.check_required_columns(df)
1719
-
1720
- # Check analog outputs [data with units of %] are floats only
1721
- columns_to_check = [
1722
- self.economizer_sig_col,
1723
- self.cooling_sig_col,
1724
- ]
1725
- self.check_analog_pct(df, columns_to_check)
1726
-
1727
- # Perform calculation without creating DataFrame columns
1728
- sat_greater_than_sp_calc = (
1729
- df[self.sat_col]
1730
- > df[self.sat_setpoint_col] + self.supply_degf_err_thres
1731
- )
1732
-
1733
- # Combined check without adding to DataFrame columns
1734
- combined_check = operator.or_(
1735
- # OS4 AHU state cooling @ min OA
1736
- (sat_greater_than_sp_calc)
1737
- & (df[self.cooling_sig_col] > 0.01)
1738
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr),
1739
- # OR verify AHU is running in OS 3 cooling mode in 100% OA
1740
- (sat_greater_than_sp_calc)
1741
- & (df[self.cooling_sig_col] > 0.01)
1742
- & (df[self.economizer_sig_col] > 0.9),
1743
- )
1744
-
1745
- # Rolling sum to count consecutive trues
1746
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1109
+ # OS4: Full mechanical cooling only (ECO = MIN_OA and CLG > 0.9)
1110
+ os4_mode = (df[self.economizer_sig_col] <= self.ahu_min_oa_dpr) & (
1111
+ df[self.cooling_sig_col] > 0.9
1112
+ )
1747
1113
 
1748
- # Set flag to 1 if rolling sum equals the window size
1749
- df["fc13_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1114
+ # Combine conditions:
1115
+ # Fault occurs when SAT is too high in either OS3 or OS4 mode with full cooling
1116
+ combined_check = sat_too_high & (os3_mode | os4_mode)
1750
1117
 
1751
- return df
1118
+ # Set fault flag
1119
+ self._set_fault_flag(df, combined_check, "fc13_flag")
1752
1120
 
1753
- except MissingColumnError as e:
1754
- print(f"Error: {e.message}")
1755
- sys.stdout.flush()
1756
- raise e
1757
- except InvalidParameterError as e:
1758
- print(f"Error: {e.message}")
1759
- sys.stdout.flush()
1760
- raise e
1121
+ return df
1761
1122
 
1762
1123
 
1763
- class FaultConditionFourteen(FaultCondition):
1124
+ class FaultConditionFourteen(BaseFaultCondition, FaultConditionMixin):
1764
1125
  """Class provides the definitions for Fault Condition 14.
1765
- Temperature drop across inactive cooling coil.
1766
- Requires coil leaving temp sensor.
1126
+ Temperature drop across inactive cooling coil in OS1 (heating) and OS2 (economizer) modes.
1127
+ This fault checks if there is an unexpected temperature drop across the cooling coil
1128
+ when it should be inactive.
1767
1129
 
1768
1130
  py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc14.py -rP -s
1769
1131
  """
1770
1132
 
1771
- def __init__(self, dict_):
1772
- super().__init__()
1773
-
1133
+ def _init_specific_attributes(self, dict_):
1774
1134
  # Threshold parameters
1775
1135
  self.delta_t_supply_fan = dict_.get("DELTA_T_SUPPLY_FAN", None)
1776
1136
  self.coil_temp_enter_err_thres = dict_.get("COIL_TEMP_ENTER_ERR_THRES", None)
1777
- self.coil_temp_leav_err_thres = dict_.get("COIL_TEMP_LEAV_ERR_THRES", None)
1137
+ self.coil_temp_leave_err_thres = dict_.get("COIL_TEMP_LEAV_ERR_THRES", None)
1138
+ self.ahu_min_oa_dpr = dict_.get("AHU_MIN_OA_DPR", None)
1778
1139
 
1779
1140
  # Validate that threshold parameters are floats
1780
1141
  for param, value in [
1781
1142
  ("delta_t_supply_fan", self.delta_t_supply_fan),
1782
1143
  ("coil_temp_enter_err_thres", self.coil_temp_enter_err_thres),
1783
- ("coil_temp_leav_err_thres", self.coil_temp_leav_err_thres),
1144
+ ("coil_temp_leave_err_thres", self.coil_temp_leave_err_thres),
1145
+ ("ahu_min_oa_dpr", self.ahu_min_oa_dpr),
1784
1146
  ]:
1785
1147
  if not isinstance(value, float):
1786
1148
  raise InvalidParameterError(
@@ -1790,30 +1152,26 @@ class FaultConditionFourteen(FaultCondition):
1790
1152
  # Other attributes
1791
1153
  self.clg_coil_enter_temp_col = dict_.get("CLG_COIL_ENTER_TEMP_COL", None)
1792
1154
  self.clg_coil_leave_temp_col = dict_.get("CLG_COIL_LEAVE_TEMP_COL", None)
1793
- self.ahu_min_oa_dpr = dict_.get("AHU_MIN_OA_DPR", None)
1794
1155
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
1795
1156
  self.heating_sig_col = dict_.get("HEATING_SIG_COL", None)
1796
1157
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1797
1158
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
1798
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1799
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1800
1159
 
1160
+ # Set documentation strings
1801
1161
  self.equation_string = (
1802
- "fc14_flag = 1 if ΔT_coil >= √(εcoil_enter² + εcoil_leave²) + ΔT_fan "
1803
- "in inactive cooling coil mode for N consecutive values else 0 \n"
1162
+ "fc14_flag = 1 if (CLG_LEAVE < CLG_ENTER - √(εENTER² + εLEAVE²)) "
1163
+ "in OS1 (heating) or OS2 (economizer) modes for N consecutive values else 0 \n"
1804
1164
  )
1805
1165
  self.description_string = (
1806
1166
  "Fault Condition 14: Temperature drop across inactive cooling coil "
1807
- "detected, requiring coil leaving temperature sensor \n"
1167
+ "in OS1 (heating) and OS2 (economizer) modes \n"
1808
1168
  )
1809
1169
  self.required_column_description = (
1810
- "Required inputs are the cooling coil entering temperature, cooling coil leaving temperature, "
1170
+ "Required inputs are the cooling coil entering and leaving air temperatures, "
1811
1171
  "cooling signal, heating signal, economizer signal, and supply fan VFD speed \n"
1812
1172
  )
1813
1173
  self.error_string = "One or more required columns are missing or None \n"
1814
1174
 
1815
- self.set_attributes(dict_)
1816
-
1817
1175
  # Set required columns specific to this fault condition
1818
1176
  self.required_columns = [
1819
1177
  self.clg_coil_enter_temp_col,
@@ -1824,106 +1182,79 @@ class FaultConditionFourteen(FaultCondition):
1824
1182
  self.supply_vfd_speed_col,
1825
1183
  ]
1826
1184
 
1827
- # Check if any of the required columns are None
1828
- if any(col is None for col in self.required_columns):
1829
- raise MissingColumnError(
1830
- f"{self.error_string}"
1831
- f"{self.equation_string}"
1832
- f"{self.description_string}"
1833
- f"{self.required_column_description}"
1834
- f"{self.required_columns}"
1835
- )
1836
-
1837
- # Ensure all required columns are strings
1838
- self.required_columns = [str(col) for col in self.required_columns]
1185
+ @FaultConditionMixin._handle_errors
1186
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1187
+ """Apply the fault condition to the DataFrame."""
1188
+ # Apply common checks
1189
+ self._apply_common_checks(df)
1839
1190
 
1840
- self.mapped_columns = (
1841
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
1842
- )
1191
+ # Check analog outputs [data with units of %] are floats only
1192
+ columns_to_check = [
1193
+ self.cooling_sig_col,
1194
+ self.heating_sig_col,
1195
+ self.economizer_sig_col,
1196
+ self.supply_vfd_speed_col,
1197
+ ]
1198
+ self._apply_analog_checks(df, columns_to_check)
1843
1199
 
1844
- def get_required_columns(self) -> str:
1845
- """Returns a string representation of the required columns."""
1846
- return (
1847
- f"{self.equation_string}"
1848
- f"{self.description_string}"
1849
- f"{self.required_column_description}"
1850
- f"{self.mapped_columns}"
1200
+ # Calculate the threshold for temperature drop
1201
+ temp_drop_threshold = np.sqrt(
1202
+ self.coil_temp_enter_err_thres**2 + self.coil_temp_leave_err_thres**2
1851
1203
  )
1852
1204
 
1853
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1854
- try:
1855
- # Ensure all required columns are present
1856
- self.check_required_columns(df)
1857
-
1858
- # Check analog outputs [data with units of %] are floats only
1859
- columns_to_check = [
1860
- self.economizer_sig_col,
1861
- self.cooling_sig_col,
1862
- self.heating_sig_col,
1863
- self.supply_vfd_speed_col,
1864
- ]
1865
- self.check_analog_pct(df, columns_to_check)
1866
-
1867
- # Calculate necessary checks
1868
- clg_delta_temp = (
1869
- df[self.clg_coil_enter_temp_col] - df[self.clg_coil_leave_temp_col]
1870
- )
1871
- clg_delta_sqrted = (
1872
- np.sqrt(
1873
- self.coil_temp_enter_err_thres**2 + self.coil_temp_leav_err_thres**2
1874
- )
1875
- + self.delta_t_supply_fan
1876
- )
1205
+ # Check if there's a significant temperature drop across the cooling coil
1206
+ temp_drop = df[self.clg_coil_enter_temp_col] - df[self.clg_coil_leave_temp_col]
1207
+ significant_temp_drop = temp_drop > temp_drop_threshold
1877
1208
 
1878
- # Perform combined checks without adding intermediate columns to DataFrame
1879
- combined_check = operator.or_(
1880
- (clg_delta_temp >= clg_delta_sqrted)
1881
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
1882
- & (df[self.cooling_sig_col] < 0.1), # OR
1883
- (clg_delta_temp >= clg_delta_sqrted)
1884
- & (df[self.heating_sig_col] > 0.0)
1885
- & (df[self.supply_vfd_speed_col] > 0.0),
1886
- )
1209
+ # Check operating modes:
1210
+ # OS1: Heating mode (HTG > 0, CLG = 0, ECO = MIN_OA)
1211
+ os1_mode = (
1212
+ (df[self.heating_sig_col] > 0.0)
1213
+ & (df[self.cooling_sig_col] == 0.0)
1214
+ & (df[self.economizer_sig_col] <= self.ahu_min_oa_dpr)
1215
+ )
1887
1216
 
1888
- # Rolling sum to count consecutive trues
1889
- rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1217
+ # OS2: Economizer mode (HTG = 0, CLG = 0, ECO > MIN_OA)
1218
+ os2_mode = (
1219
+ (df[self.heating_sig_col] == 0.0)
1220
+ & (df[self.cooling_sig_col] == 0.0)
1221
+ & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
1222
+ )
1890
1223
 
1891
- # Set flag to 1 if rolling sum equals the window size
1892
- df["fc14_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1224
+ # Combine conditions:
1225
+ # Fault occurs when there's a significant temperature drop across an inactive cooling coil
1226
+ # in either OS1 (heating) or OS2 (economizer) mode
1227
+ combined_check = significant_temp_drop & (os1_mode | os2_mode)
1893
1228
 
1894
- return df
1229
+ # Set fault flag
1230
+ self._set_fault_flag(df, combined_check, "fc14_flag")
1895
1231
 
1896
- except MissingColumnError as e:
1897
- print(f"Error: {e.message}")
1898
- sys.stdout.flush()
1899
- raise e
1900
- except InvalidParameterError as e:
1901
- print(f"Error: {e.message}")
1902
- sys.stdout.flush()
1903
- raise e
1232
+ return df
1904
1233
 
1905
1234
 
1906
- class FaultConditionFifteen(FaultCondition):
1235
+ class FaultConditionFifteen(BaseFaultCondition, FaultConditionMixin):
1907
1236
  """Class provides the definitions for Fault Condition 15.
1908
- Temperature rise across inactive heating coil.
1909
- Requires coil leaving temp sensor.
1237
+ Temperature rise across inactive heating coil in OS2 (economizer),
1238
+ OS3 (economizer + mechanical cooling), and OS4 (mechanical cooling only) modes.
1239
+ This fault checks if there is an unexpected temperature rise across the heating coil
1240
+ when it should be inactive.
1910
1241
 
1911
- > py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc15.py -rP -s
1242
+ py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc15.py -rP -s
1912
1243
  """
1913
1244
 
1914
- def __init__(self, dict_):
1915
- super().__init__()
1916
-
1245
+ def _init_specific_attributes(self, dict_):
1917
1246
  # Threshold parameters
1918
- self.delta_supply_fan = dict_.get("DELTA_SUPPLY_FAN", None)
1247
+ self.delta_t_supply_fan = dict_.get("DELTA_SUPPLY_FAN", None)
1919
1248
  self.coil_temp_enter_err_thres = dict_.get("COIL_TEMP_ENTER_ERR_THRES", None)
1920
- self.coil_temp_leav_err_thres = dict_.get("COIL_TEMP_LEAV_ERR_THRES", None)
1249
+ self.coil_temp_leave_err_thres = dict_.get("COIL_TEMP_LEAV_ERR_THRES", None)
1250
+ self.ahu_min_oa_dpr = dict_.get("AHU_MIN_OA_DPR", None)
1921
1251
 
1922
1252
  # Validate that threshold parameters are floats
1923
1253
  for param, value in [
1924
- ("delta_supply_fan", self.delta_supply_fan),
1254
+ ("delta_t_supply_fan", self.delta_t_supply_fan),
1925
1255
  ("coil_temp_enter_err_thres", self.coil_temp_enter_err_thres),
1926
- ("coil_temp_leav_err_thres", self.coil_temp_leav_err_thres),
1256
+ ("coil_temp_leave_err_thres", self.coil_temp_leave_err_thres),
1257
+ ("ahu_min_oa_dpr", self.ahu_min_oa_dpr),
1927
1258
  ]:
1928
1259
  if not isinstance(value, float):
1929
1260
  raise InvalidParameterError(
@@ -1933,30 +1264,28 @@ class FaultConditionFifteen(FaultCondition):
1933
1264
  # Other attributes
1934
1265
  self.htg_coil_enter_temp_col = dict_.get("HTG_COIL_ENTER_TEMP_COL", None)
1935
1266
  self.htg_coil_leave_temp_col = dict_.get("HTG_COIL_LEAVE_TEMP_COL", None)
1936
- self.ahu_min_oa_dpr = dict_.get("AHU_MIN_OA_DPR", None)
1937
1267
  self.cooling_sig_col = dict_.get("COOLING_SIG_COL", None)
1938
1268
  self.heating_sig_col = dict_.get("HEATING_SIG_COL", None)
1939
1269
  self.economizer_sig_col = dict_.get("ECONOMIZER_SIG_COL", None)
1940
1270
  self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
1941
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1942
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)
1943
1271
 
1272
+ # Set documentation strings
1944
1273
  self.equation_string = (
1945
- "fc15_flag = 1 if ΔT_coil >= √(εcoil_enter² + εcoil_leave²) + ΔT_fan "
1946
- "in inactive heating coil mode for N consecutive values else 0 \n"
1274
+ "fc15_flag = 1 if (HTG_LEAVE > HTG_ENTER + √(εENTER² + εLEAVE²) + ΔTfan) "
1275
+ "in OS2 (economizer), OS3 (economizer + mechanical cooling), or "
1276
+ "OS4 (mechanical cooling only) modes for N consecutive values else 0 \n"
1947
1277
  )
1948
1278
  self.description_string = (
1949
1279
  "Fault Condition 15: Temperature rise across inactive heating coil "
1950
- "detected, requiring coil leaving temperature sensor \n"
1280
+ "in OS2 (economizer), OS3 (economizer + mechanical cooling), and "
1281
+ "OS4 (mechanical cooling only) modes \n"
1951
1282
  )
1952
1283
  self.required_column_description = (
1953
- "Required inputs are the heating coil entering temperature, heating coil leaving temperature, "
1284
+ "Required inputs are the heating coil entering and leaving air temperatures, "
1954
1285
  "cooling signal, heating signal, economizer signal, and supply fan VFD speed \n"
1955
1286
  )
1956
1287
  self.error_string = "One or more required columns are missing or None \n"
1957
1288
 
1958
- self.set_attributes(dict_)
1959
-
1960
1289
  # Set required columns specific to this fault condition
1961
1290
  self.required_columns = [
1962
1291
  self.htg_coil_enter_temp_col,
@@ -1967,129 +1296,86 @@ class FaultConditionFifteen(FaultCondition):
1967
1296
  self.supply_vfd_speed_col,
1968
1297
  ]
1969
1298
 
1970
- # Check if any of the required columns are None
1971
- if any(col is None for col in self.required_columns):
1972
- raise MissingColumnError(
1973
- f"{self.error_string}"
1974
- f"{self.equation_string}"
1975
- f"{self.description_string}"
1976
- f"{self.required_column_description}"
1977
- f"{self.required_columns}"
1978
- )
1979
-
1980
- # Ensure all required columns are strings
1981
- self.required_columns = [str(col) for col in self.required_columns]
1982
-
1983
- self.mapped_columns = (
1984
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
1985
- )
1299
+ @FaultConditionMixin._handle_errors
1300
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1301
+ """Apply the fault condition to the DataFrame."""
1302
+ # Apply common checks
1303
+ self._apply_common_checks(df)
1986
1304
 
1987
- def get_required_columns(self) -> str:
1988
- """Returns a string representation of the required columns."""
1989
- return (
1990
- f"{self.equation_string}"
1991
- f"{self.description_string}"
1992
- f"{self.required_column_description}"
1993
- f"{self.mapped_columns}"
1994
- )
1305
+ # Check analog outputs [data with units of %] are floats only
1306
+ columns_to_check = [
1307
+ self.cooling_sig_col,
1308
+ self.heating_sig_col,
1309
+ self.economizer_sig_col,
1310
+ self.supply_vfd_speed_col,
1311
+ ]
1312
+ self._apply_analog_checks(df, columns_to_check)
1995
1313
 
1996
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
1997
- try:
1998
- # Ensure all required columns are present
1999
- self.check_required_columns(df)
2000
-
2001
- if self.troubleshoot_mode:
2002
- self.troubleshoot_cols(df)
2003
-
2004
- # Check analog outputs [data with units of %] are floats only
2005
- columns_to_check = [
2006
- self.economizer_sig_col,
2007
- self.cooling_sig_col,
2008
- self.heating_sig_col,
2009
- self.supply_vfd_speed_col,
2010
- ]
2011
- self.check_analog_pct(df, columns_to_check)
2012
-
2013
- # Create helper columns
2014
- df["htg_delta_temp"] = (
2015
- df[self.htg_coil_leave_temp_col] - df[self.htg_coil_enter_temp_col]
1314
+ # Calculate the threshold for temperature rise, including supply fan heat
1315
+ temp_rise_threshold = (
1316
+ np.sqrt(
1317
+ self.coil_temp_enter_err_thres**2 + self.coil_temp_leave_err_thres**2
2016
1318
  )
1319
+ + self.delta_t_supply_fan
1320
+ )
2017
1321
 
2018
- df["htg_delta_sqrted"] = (
2019
- np.sqrt(
2020
- self.coil_temp_enter_err_thres**2 + self.coil_temp_leav_err_thres**2
2021
- )
2022
- + self.delta_supply_fan
2023
- )
1322
+ # Check if there's a significant temperature rise across the heating coil
1323
+ temp_rise = df[self.htg_coil_leave_temp_col] - df[self.htg_coil_enter_temp_col]
1324
+ significant_temp_rise = temp_rise > temp_rise_threshold
2024
1325
 
2025
- df["combined_check"] = (
2026
- (
2027
- (df["htg_delta_temp"] >= df["htg_delta_sqrted"])
2028
- # verify AHU is in OS2 only free cooling mode
2029
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
2030
- & (df[self.cooling_sig_col] < 0.1)
2031
- )
2032
- | (
2033
- (df["htg_delta_temp"] >= df["htg_delta_sqrted"])
2034
- # OS4 AHU state clg @ min OA
2035
- & (df[self.cooling_sig_col] > 0.01)
2036
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
2037
- )
2038
- | (
2039
- (df["htg_delta_temp"] >= df["htg_delta_sqrted"])
2040
- # verify AHU is running in OS 3 clg mode in 100 OA
2041
- & (df[self.cooling_sig_col] > 0.01)
2042
- & (df[self.economizer_sig_col] > 0.9)
2043
- )
2044
- )
1326
+ # Check operating modes:
1327
+ # OS2: Economizer mode (HTG = 0, CLG = 0, ECO > MIN_OA)
1328
+ os2_mode = (
1329
+ (df[self.heating_sig_col] == 0.0)
1330
+ & (df[self.cooling_sig_col] == 0.0)
1331
+ & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
1332
+ )
2045
1333
 
2046
- # Rolling sum to count consecutive trues
2047
- rolling_sum = (
2048
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
2049
- )
2050
- # Set flag to 1 if rolling sum equals the window size
2051
- df["fc15_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1334
+ # OS3: Economizer + mechanical cooling (HTG = 0, CLG > 0, ECO > 0.9)
1335
+ os3_mode = (
1336
+ (df[self.heating_sig_col] == 0.0)
1337
+ & (df[self.cooling_sig_col] > 0.0)
1338
+ & (df[self.economizer_sig_col] > 0.9)
1339
+ )
2052
1340
 
2053
- if self.troubleshoot_mode:
2054
- print("Troubleshoot mode enabled - not removing helper columns")
2055
- sys.stdout.flush()
1341
+ # OS4: Mechanical cooling only (HTG = 0, CLG > 0, ECO = MIN_OA)
1342
+ os4_mode = (
1343
+ (df[self.heating_sig_col] == 0.0)
1344
+ & (df[self.cooling_sig_col] > 0.0)
1345
+ & (df[self.economizer_sig_col] <= self.ahu_min_oa_dpr)
1346
+ )
2056
1347
 
2057
- # Optionally remove temporary columns
2058
- df.drop(
2059
- columns=["htg_delta_temp", "htg_delta_sqrted", "combined_check"],
2060
- inplace=True,
2061
- )
1348
+ # Combine conditions:
1349
+ # Fault occurs when there's a significant temperature rise across an inactive heating coil
1350
+ # in OS2 (economizer), OS3 (economizer + mechanical cooling), or OS4 (mechanical cooling only) mode
1351
+ combined_check = significant_temp_rise & (os2_mode | os3_mode | os4_mode)
2062
1352
 
2063
- return df
1353
+ # Set fault flag
1354
+ self._set_fault_flag(df, combined_check, "fc15_flag")
2064
1355
 
2065
- except MissingColumnError as e:
2066
- print(f"Error: {e.message}")
2067
- sys.stdout.flush()
2068
- raise e
2069
- except InvalidParameterError as e:
2070
- print(f"Error: {e.message}")
2071
- sys.stdout.flush()
2072
- raise e
1356
+ return df
2073
1357
 
2074
1358
 
2075
- class FaultConditionSixteen(FaultCondition):
1359
+ class FaultConditionSixteen(BaseFaultCondition, FaultConditionMixin):
2076
1360
  """Class provides the definitions for Fault Condition 16.
2077
- ERV Ineffective Process based on outdoor air temperature ranges.
2078
- """
1361
+ ERV effectiveness should be within specified thresholds based on OAT.
1362
+ This fault checks if the ERV (Energy Recovery Ventilator) is operating
1363
+ within expected efficiency ranges in both heating and cooling modes.
2079
1364
 
2080
- def __init__(self, dict_):
2081
- super().__init__()
2082
-
2083
- # Threshold parameters for efficiency ranges based on heating and cooling
2084
- self.erv_efficiency_min_heating = dict_.get("ERV_EFFICIENCY_MIN_HEATING", 0.7)
2085
- self.erv_efficiency_max_heating = dict_.get("ERV_EFFICIENCY_MAX_HEATING", 0.8)
2086
- self.erv_efficiency_min_cooling = dict_.get("ERV_EFFICIENCY_MIN_COOLING", 0.5)
2087
- self.erv_efficiency_max_cooling = dict_.get("ERV_EFFICIENCY_MAX_COOLING", 0.6)
1365
+ py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc16.py -rP -s
1366
+ """
2088
1367
 
2089
- self.oat_low_threshold = dict_.get("OAT_LOW_THRES", 32.0)
2090
- self.oat_high_threshold = dict_.get("OAT_HIGH_THRES", 80.0)
1368
+ def _init_specific_attributes(self, dict_):
1369
+ # Threshold parameters
1370
+ self.erv_efficiency_min_heating = dict_.get("ERV_EFFICIENCY_MIN_HEATING", None)
1371
+ self.erv_efficiency_max_heating = dict_.get("ERV_EFFICIENCY_MAX_HEATING", None)
1372
+ self.erv_efficiency_min_cooling = dict_.get("ERV_EFFICIENCY_MIN_COOLING", None)
1373
+ self.erv_efficiency_max_cooling = dict_.get("ERV_EFFICIENCY_MAX_COOLING", None)
1374
+ self.oat_low_threshold = dict_.get("OAT_LOW_THRESHOLD", None)
1375
+ self.oat_high_threshold = dict_.get("OAT_HIGH_THRESHOLD", None)
1376
+ self.oat_rat_delta_min = dict_.get("OAT_RAT_DELTA_MIN", None)
2091
1377
 
2092
- # Validate that threshold parameters are floats and within 0.0 and 1.0 for efficiency values
1378
+ # Validate that threshold parameters are floats
2093
1379
  for param, value in [
2094
1380
  ("erv_efficiency_min_heating", self.erv_efficiency_min_heating),
2095
1381
  ("erv_efficiency_max_heating", self.erv_efficiency_max_heating),
@@ -2097,49 +1383,47 @@ class FaultConditionSixteen(FaultCondition):
2097
1383
  ("erv_efficiency_max_cooling", self.erv_efficiency_max_cooling),
2098
1384
  ("oat_low_threshold", self.oat_low_threshold),
2099
1385
  ("oat_high_threshold", self.oat_high_threshold),
1386
+ ("oat_rat_delta_min", self.oat_rat_delta_min),
2100
1387
  ]:
2101
1388
  if not isinstance(value, float):
2102
1389
  raise InvalidParameterError(
2103
1390
  f"The parameter '{param}' should be a float, but got {type(value).__name__}."
2104
1391
  )
2105
- if "erv_efficiency" in param and not (0.0 <= value <= 1.0):
1392
+
1393
+ # Validate that efficiency values are between 0.0 and 1.0
1394
+ for param, value in [
1395
+ ("ERV_EFFICIENCY_MIN_HEATING", self.erv_efficiency_min_heating),
1396
+ ("ERV_EFFICIENCY_MAX_HEATING", self.erv_efficiency_max_heating),
1397
+ ("ERV_EFFICIENCY_MIN_COOLING", self.erv_efficiency_min_cooling),
1398
+ ("ERV_EFFICIENCY_MAX_COOLING", self.erv_efficiency_max_cooling),
1399
+ ]:
1400
+ if not 0.0 <= value <= 1.0:
2106
1401
  raise InvalidParameterError(
2107
- f"The parameter '{param}' should be a float between 0.0 and 1.0 to represent a percentage, but got {value}."
1402
+ f"The parameter '{param}' should be a float between 0.0 and 1.0, but got {value}."
2108
1403
  )
2109
1404
 
2110
1405
  # Other attributes
2111
- self.erv_oat_enter_col = dict_.get("ERV_OAT_ENTER_COL", "erv_oat_enter")
2112
- self.erv_oat_leaving_col = dict_.get("ERV_OAT_LEAVING_COL", "erv_oat_leaving")
2113
- self.erv_eat_enter_col = dict_.get("ERV_EAT_ENTER_COL", "erv_eat_enter")
2114
- self.erv_eat_leaving_col = dict_.get("ERV_EAT_LEAVING_COL", "erv_eat_leaving")
2115
- self.supply_vfd_speed_col = dict_.get(
2116
- "SUPPLY_VFD_SPEED_COL", "supply_vfd_speed"
2117
- )
2118
- self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", 1)
2119
- self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
1406
+ self.erv_oat_enter_col = dict_.get("ERV_OAT_ENTER_COL", None)
1407
+ self.erv_oat_leaving_col = dict_.get("ERV_OAT_LEAVING_COL", None)
1408
+ self.erv_eat_enter_col = dict_.get("ERV_EAT_ENTER_COL", None)
1409
+ self.erv_eat_leaving_col = dict_.get("ERV_EAT_LEAVING_COL", None)
1410
+ self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
2120
1411
 
1412
+ # Set documentation strings
2121
1413
  self.equation_string = (
2122
- "fc16_flag = 1 if temperature deltas and expected efficiency is ineffective "
1414
+ "fc16_flag = 1 if ERV effectiveness is outside expected range "
1415
+ "(heating: εmin_htg ≤ ε ≤ εmax_htg, cooling: εmin_clg ≤ ε ≤ εmax_clg) "
2123
1416
  "for N consecutive values else 0 \n"
2124
1417
  )
2125
1418
  self.description_string = (
2126
- "Fault Condition 16: ERV is an ineffective heat transfer fault. "
2127
- "This fault occurs when the ERV's efficiency "
2128
- "is outside the acceptable range based on the delta temperature across the "
2129
- "ERV outside air enter temperature and ERV outside air leaving temperature, "
2130
- "indicating poor heat transfer. "
2131
- "It considers both heating and cooling conditions where each have acceptable "
2132
- "ranges in percentage for expected heat transfer efficiency. The percentage needs "
2133
- "to be a float between 0.0 and 1.0."
1419
+ "Fault Condition 16: ERV effectiveness should be within specified "
1420
+ "thresholds based on OAT \n"
2134
1421
  )
2135
1422
  self.required_column_description = (
2136
- "Required inputs are the ERV outside air entering temperature, ERV outside air leaving temperature, "
2137
- "ERV exhaust entering temperature, ERV exhaust leaving temperature, "
2138
- "and AHU supply fan VFD speed."
1423
+ "Required inputs are the ERV outdoor air entering and leaving temperatures, "
1424
+ "ERV exhaust air entering and leaving temperatures, and supply fan VFD speed \n"
2139
1425
  )
2140
- self.error_string = "One or more required columns are missing or None."
2141
-
2142
- self.set_attributes(dict_)
1426
+ self.error_string = "One or more required columns are missing or None \n"
2143
1427
 
2144
1428
  # Set required columns specific to this fault condition
2145
1429
  self.required_columns = [
@@ -2150,104 +1434,54 @@ class FaultConditionSixteen(FaultCondition):
2150
1434
  self.supply_vfd_speed_col,
2151
1435
  ]
2152
1436
 
2153
- # Check if any of the required columns are None
2154
- if any(col is None for col in self.required_columns):
2155
- raise MissingColumnError(
2156
- f"{self.error_string}\n"
2157
- f"{self.equation_string}\n"
2158
- f"{self.description_string}\n"
2159
- f"{self.required_column_description}\n"
2160
- f"Missing columns: {self.required_columns}"
2161
- )
2162
-
2163
- # Ensure all required columns are strings
2164
- self.required_columns = [str(col) for col in self.required_columns]
2165
-
2166
- self.mapped_columns = (
2167
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
2168
- )
2169
-
2170
- def get_required_columns(self) -> str:
2171
- """Returns a string representation of the required columns."""
2172
- return (
2173
- f"{self.equation_string}"
2174
- f"{self.description_string}\n"
2175
- f"{self.required_column_description}\n"
2176
- f"{self.mapped_columns}"
2177
- )
2178
-
2179
- def calculate_erv_efficiency(self, df: pd.DataFrame) -> pd.DataFrame:
2180
- # Calculate the temperature differences
2181
- delta_temp_oa = df[self.erv_oat_leaving_col] - df[self.erv_oat_enter_col]
2182
- delta_temp_ea = df[self.erv_eat_enter_col] - df[self.erv_oat_enter_col]
2183
-
2184
- # Use the absolute value to handle both heating and cooling applications
2185
- df["erv_efficiency_oa"] = np.abs(delta_temp_oa) / np.abs(delta_temp_ea)
2186
-
2187
- return df
2188
-
1437
+ @FaultConditionMixin._handle_errors
2189
1438
  def apply(self, df: pd.DataFrame) -> pd.DataFrame:
2190
- try:
2191
- # Calculate ERV efficiency
2192
- df = self.calculate_erv_efficiency(df)
2193
-
2194
- # Fan must be on for a fault to be considered
2195
- fan_on = df[self.supply_vfd_speed_col] > 0.1
1439
+ """Apply the fault condition to the DataFrame."""
1440
+ # Apply common checks
1441
+ self._apply_common_checks(df)
2196
1442
 
2197
- # Combined heating and cooling checks
2198
- cold_outside = df[self.erv_oat_enter_col] <= self.oat_low_threshold
2199
- hot_outside = df[self.erv_oat_enter_col] >= self.oat_high_threshold
1443
+ # Check analog outputs [data with units of %] are floats only
1444
+ columns_to_check = [self.supply_vfd_speed_col]
1445
+ self._apply_analog_checks(df, columns_to_check)
2200
1446
 
2201
- heating_fault = (
2202
- (
2203
- (df["erv_efficiency_oa"] < self.erv_efficiency_min_heating)
2204
- | (df["erv_efficiency_oa"] > self.erv_efficiency_max_heating)
2205
- )
2206
- & cold_outside
2207
- & fan_on
2208
- )
1447
+ # Calculate temperature differences
1448
+ oat_rat_delta = abs(df[self.erv_oat_enter_col] - df[self.erv_eat_enter_col])
2209
1449
 
2210
- cooling_fault = (
2211
- (
2212
- (df["erv_efficiency_oa"] < self.erv_efficiency_min_cooling)
2213
- | (df["erv_efficiency_oa"] > self.erv_efficiency_max_cooling)
2214
- )
2215
- & hot_outside
2216
- & fan_on
2217
- )
1450
+ # Calculate ERV effectiveness
1451
+ # ε = (T_leaving - T_entering) / (T_exhaust - T_entering)
1452
+ erv_effectiveness = (
1453
+ df[self.erv_oat_leaving_col] - df[self.erv_oat_enter_col]
1454
+ ) / (df[self.erv_eat_enter_col] - df[self.erv_oat_enter_col])
2218
1455
 
2219
- df["combined_checks"] = heating_fault | cooling_fault
1456
+ # Determine operating mode based on OAT
1457
+ heating_mode = df[self.erv_oat_enter_col] < self.oat_low_threshold
1458
+ cooling_mode = df[self.erv_oat_enter_col] > self.oat_high_threshold
2220
1459
 
2221
- # Apply rolling sum
2222
- df["fc16_flag"] = (
2223
- df["combined_checks"]
2224
- .rolling(window=self.rolling_window_size)
2225
- .sum()
2226
- .ge(self.rolling_window_size)
2227
- .astype(int)
2228
- )
1460
+ # Check effectiveness against thresholds
1461
+ low_effectiveness_htg = heating_mode & (
1462
+ erv_effectiveness < self.erv_efficiency_min_heating
1463
+ )
1464
+ high_effectiveness_htg = heating_mode & (
1465
+ erv_effectiveness > self.erv_efficiency_max_heating
1466
+ )
1467
+ low_effectiveness_clg = cooling_mode & (
1468
+ erv_effectiveness < self.erv_efficiency_min_cooling
1469
+ )
1470
+ high_effectiveness_clg = cooling_mode & (
1471
+ erv_effectiveness > self.erv_efficiency_max_cooling
1472
+ )
2229
1473
 
2230
- if self.troubleshoot_mode:
2231
- print("Troubleshoot mode enabled - not removing helper columns")
2232
- sys.stdout.flush()
2233
-
2234
- # Drop helper cols if not in troubleshoot mode
2235
- if not self.troubleshoot_mode:
2236
- df.drop(
2237
- columns=[
2238
- "combined_checks",
2239
- "erv_efficiency_oa",
2240
- ],
2241
- inplace=True,
2242
- )
1474
+ # Combine conditions:
1475
+ # Fault occurs when ERV effectiveness is outside expected range
1476
+ # and there's sufficient temperature difference between OAT and RAT
1477
+ combined_check = (oat_rat_delta >= self.oat_rat_delta_min) & (
1478
+ low_effectiveness_htg
1479
+ | high_effectiveness_htg
1480
+ | low_effectiveness_clg
1481
+ | high_effectiveness_clg
1482
+ )
2243
1483
 
2244
- return df
1484
+ # Set fault flag
1485
+ self._set_fault_flag(df, combined_check, "fc16_flag")
2245
1486
 
2246
- except MissingColumnError as e:
2247
- print(f"Error: {e.message}")
2248
- sys.stdout.flush()
2249
- raise e
2250
- except InvalidParameterError as e:
2251
- print(f"Error: {e.message}")
2252
- sys.stdout.flush()
2253
- raise e
1487
+ return df