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