open-fdd 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

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