open-fdd 0.1.5__py3-none-any.whl → 0.1.7__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 (66) hide show
  1. open_fdd/air_handling_unit/faults/__init__.py +331 -300
  2. open_fdd/air_handling_unit/faults/helper_utils.py +3 -0
  3. open_fdd/air_handling_unit/faults/shared_utils.py +16 -1
  4. open_fdd/air_handling_unit/reports/__init__.py +107 -0
  5. open_fdd/air_handling_unit/reports/fault_report.py +1 -0
  6. open_fdd/tests/ahu/test_ahu_fc1.py +1 -0
  7. open_fdd/tests/ahu/test_ahu_fc16.py +205 -0
  8. {open_fdd-0.1.5.dist-info → open_fdd-0.1.7.dist-info}/METADATA +4 -3
  9. open_fdd-0.1.7.dist-info/RECORD +31 -0
  10. {open_fdd-0.1.5.dist-info → open_fdd-0.1.7.dist-info}/WHEEL +1 -1
  11. open_fdd/air_handling_unit/faults/fault_condition_eight.py +0 -127
  12. open_fdd/air_handling_unit/faults/fault_condition_eleven.py +0 -126
  13. open_fdd/air_handling_unit/faults/fault_condition_fifteen.py +0 -152
  14. open_fdd/air_handling_unit/faults/fault_condition_five.py +0 -123
  15. open_fdd/air_handling_unit/faults/fault_condition_four.py +0 -168
  16. open_fdd/air_handling_unit/faults/fault_condition_fourteen.py +0 -143
  17. open_fdd/air_handling_unit/faults/fault_condition_nine.py +0 -128
  18. open_fdd/air_handling_unit/faults/fault_condition_one.py +0 -112
  19. open_fdd/air_handling_unit/faults/fault_condition_seven.py +0 -114
  20. open_fdd/air_handling_unit/faults/fault_condition_six.py +0 -181
  21. open_fdd/air_handling_unit/faults/fault_condition_ten.py +0 -123
  22. open_fdd/air_handling_unit/faults/fault_condition_thirteen.py +0 -127
  23. open_fdd/air_handling_unit/faults/fault_condition_three.py +0 -113
  24. open_fdd/air_handling_unit/faults/fault_condition_twelve.py +0 -132
  25. open_fdd/air_handling_unit/faults/fault_condition_two.py +0 -113
  26. open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_1.jpg +0 -0
  27. open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_2.jpg +0 -0
  28. open_fdd/air_handling_unit/images/example1.jpg +0 -0
  29. open_fdd/air_handling_unit/images/example2.jpg +0 -0
  30. open_fdd/air_handling_unit/images/fc10_definition.png +0 -0
  31. open_fdd/air_handling_unit/images/fc11_definition.png +0 -0
  32. open_fdd/air_handling_unit/images/fc12_definition.png +0 -0
  33. open_fdd/air_handling_unit/images/fc13_definition.png +0 -0
  34. open_fdd/air_handling_unit/images/fc1_definition.png +0 -0
  35. open_fdd/air_handling_unit/images/fc1_report_screenshot_all.png +0 -0
  36. open_fdd/air_handling_unit/images/fc2_definition.png +0 -0
  37. open_fdd/air_handling_unit/images/fc3_definition.png +0 -0
  38. open_fdd/air_handling_unit/images/fc4_definition.png +0 -0
  39. open_fdd/air_handling_unit/images/fc5_definition.png +0 -0
  40. open_fdd/air_handling_unit/images/fc6_definition.png +0 -0
  41. open_fdd/air_handling_unit/images/fc7_definition.png +0 -0
  42. open_fdd/air_handling_unit/images/fc8_definition.png +0 -0
  43. open_fdd/air_handling_unit/images/fc9_definition.png +0 -0
  44. open_fdd/air_handling_unit/images/latex_generator.py +0 -175
  45. open_fdd/air_handling_unit/images/params.docx +0 -0
  46. open_fdd/air_handling_unit/images/params.pdf +0 -0
  47. open_fdd/air_handling_unit/images/plot_for_repo.png +0 -0
  48. open_fdd/air_handling_unit/reports/base_report.py +0 -47
  49. open_fdd/air_handling_unit/reports/report_fc1.py +0 -115
  50. open_fdd/air_handling_unit/reports/report_fc10.py +0 -126
  51. open_fdd/air_handling_unit/reports/report_fc11.py +0 -128
  52. open_fdd/air_handling_unit/reports/report_fc12.py +0 -126
  53. open_fdd/air_handling_unit/reports/report_fc13.py +0 -126
  54. open_fdd/air_handling_unit/reports/report_fc14.py +0 -124
  55. open_fdd/air_handling_unit/reports/report_fc15.py +0 -124
  56. open_fdd/air_handling_unit/reports/report_fc2.py +0 -119
  57. open_fdd/air_handling_unit/reports/report_fc3.py +0 -119
  58. open_fdd/air_handling_unit/reports/report_fc4.py +0 -148
  59. open_fdd/air_handling_unit/reports/report_fc5.py +0 -132
  60. open_fdd/air_handling_unit/reports/report_fc6.py +0 -156
  61. open_fdd/air_handling_unit/reports/report_fc7.py +0 -126
  62. open_fdd/air_handling_unit/reports/report_fc8.py +0 -118
  63. open_fdd/air_handling_unit/reports/report_fc9.py +0 -120
  64. open_fdd-0.1.5.dist-info/RECORD +0 -83
  65. {open_fdd-0.1.5.dist-info → open_fdd-0.1.7.dist-info}/LICENSE +0 -0
  66. {open_fdd-0.1.5.dist-info → open_fdd-0.1.7.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ from open_fdd.air_handling_unit.faults.fault_condition import (
5
5
  MissingColumnError,
6
6
  InvalidParameterError,
7
7
  )
8
+ from open_fdd.air_handling_unit.faults.helper_utils import SharedUtils
8
9
  import operator
9
10
  import sys
10
11
 
@@ -12,6 +13,8 @@ import sys
12
13
  class FaultConditionOne(FaultCondition):
13
14
  """Class provides the definitions for Fault Condition 1.
14
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
15
18
  """
16
19
 
17
20
  def __init__(self, dict_):
@@ -101,34 +104,25 @@ class FaultConditionOne(FaultCondition):
101
104
  columns_to_check = [self.supply_vfd_speed_col]
102
105
  self.check_analog_pct(df, columns_to_check)
103
106
 
104
- df["static_check_"] = (
107
+ # Perform checks
108
+ static_check = (
105
109
  df[self.duct_static_col]
106
110
  < df[self.duct_static_setpoint_col] - self.duct_static_inches_err_thres
107
111
  )
108
- df["fan_check_"] = (
112
+ fan_check = (
109
113
  df[self.supply_vfd_speed_col]
110
114
  >= self.vfd_speed_percent_max - self.vfd_speed_percent_err_thres
111
115
  )
112
116
 
113
117
  # Combined condition check
114
- df["combined_check"] = df["static_check_"] & df["fan_check_"]
118
+ combined_check = static_check & fan_check
115
119
 
116
120
  # Rolling sum to count consecutive trues
117
- rolling_sum = (
118
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
119
- )
121
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
122
+
120
123
  # Set flag to 1 if rolling sum equals the window size
121
124
  df["fc1_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
122
125
 
123
- if self.troubleshoot_mode:
124
- print("Troubleshoot mode enabled - not removing helper columns")
125
- sys.stdout.flush()
126
-
127
- # Optionally remove temporary columns
128
- df.drop(
129
- columns=["static_check_", "fan_check_", "combined_check"], inplace=True
130
- )
131
-
132
126
  return df
133
127
 
134
128
  except MissingColumnError as e:
@@ -232,33 +226,23 @@ class FaultConditionTwo(FaultCondition):
232
226
  columns_to_check = [self.supply_vfd_speed_col]
233
227
  self.check_analog_pct(df, columns_to_check)
234
228
 
235
- # Fault condition-specific checks / flags
236
- df["mat_check"] = df[self.mat_col] + self.mix_degf_err_thres
237
- df["temp_min_check"] = np.minimum(
229
+ # Perform checks
230
+ mat_check = df[self.mat_col] + self.mix_degf_err_thres
231
+ temp_min_check = np.minimum(
238
232
  df[self.rat_col] - self.return_degf_err_thres,
239
233
  df[self.oat_col] - self.outdoor_degf_err_thres,
240
234
  )
241
235
 
242
- df["combined_check"] = (df["mat_check"] < df["temp_min_check"]) & (
236
+ combined_check = (mat_check < temp_min_check) & (
243
237
  df[self.supply_vfd_speed_col] > 0.01
244
238
  )
245
239
 
246
240
  # Rolling sum to count consecutive trues
247
- rolling_sum = (
248
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
249
- )
241
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
242
+
250
243
  # Set flag to 1 if rolling sum equals the window size
251
244
  df["fc2_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
252
245
 
253
- if self.troubleshoot_mode:
254
- print("Troubleshoot mode enabled - not removing helper columns")
255
- sys.stdout.flush()
256
-
257
- # Optionally remove temporary columns
258
- df.drop(
259
- columns=["mat_check", "temp_min_check", "combined_check"], inplace=True
260
- )
261
-
262
246
  return df
263
247
 
264
248
  except MissingColumnError as e:
@@ -362,33 +346,23 @@ class FaultConditionThree(FaultCondition):
362
346
  columns_to_check = [self.supply_vfd_speed_col]
363
347
  self.check_analog_pct(df, columns_to_check)
364
348
 
365
- # Fault condition-specific checks / flags
366
- df["mat_check"] = df[self.mat_col] - self.mix_degf_err_thres
367
- df["temp_max_check"] = np.maximum(
349
+ # Perform checks
350
+ mat_check = df[self.mat_col] - self.mix_degf_err_thres
351
+ temp_max_check = np.maximum(
368
352
  df[self.rat_col] + self.return_degf_err_thres,
369
353
  df[self.oat_col] + self.outdoor_degf_err_thres,
370
354
  )
371
355
 
372
- df["combined_check"] = (df["mat_check"] > df["temp_max_check"]) & (
356
+ combined_check = (mat_check > temp_max_check) & (
373
357
  df[self.supply_vfd_speed_col] > 0.01
374
358
  )
375
359
 
376
360
  # Rolling sum to count consecutive trues
377
- rolling_sum = (
378
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
379
- )
361
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
362
+
380
363
  # Set flag to 1 if rolling sum equals the window size
381
364
  df["fc3_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
382
365
 
383
- if self.troubleshoot_mode:
384
- print("Troubleshoot mode enabled - not removing helper columns")
385
- sys.stdout.flush()
386
-
387
- # Optionally remove temporary columns
388
- df.drop(
389
- columns=["mat_check", "temp_max_check", "combined_check"], inplace=True
390
- )
391
-
392
366
  return df
393
367
 
394
368
  except MissingColumnError as e:
@@ -675,40 +649,28 @@ class FaultConditionFive(FaultCondition):
675
649
  # Ensure all required columns are present
676
650
  self.check_required_columns(df)
677
651
 
678
- if self.troubleshoot_mode:
679
- self.troubleshoot_cols(df)
680
-
681
652
  # Check analog outputs [data with units of %] are floats only
682
653
  columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
654
+ self.check_analog_pct(df, columns_to_check)
683
655
 
684
- for col in columns_to_check:
685
- self.check_analog_pct(df, [col])
686
-
687
- df["sat_check"] = df[self.sat_col] + self.supply_degf_err_thres
688
- df["mat_check"] = (
656
+ # Perform checks
657
+ sat_check = df[self.sat_col] + self.supply_degf_err_thres
658
+ mat_check = (
689
659
  df[self.mat_col] - self.mix_degf_err_thres + self.delta_t_supply_fan
690
660
  )
691
661
 
692
- df["combined_check"] = (
693
- (df["sat_check"] <= df["mat_check"])
662
+ combined_check = (
663
+ (sat_check <= mat_check)
694
664
  & (df[self.heating_sig_col] > 0.01)
695
665
  & (df[self.supply_vfd_speed_col] > 0.01)
696
666
  )
697
667
 
698
668
  # Rolling sum to count consecutive trues
699
- rolling_sum = (
700
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
701
- )
669
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
670
+
702
671
  # Set flag to 1 if rolling sum equals the window size
703
672
  df["fc5_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
704
673
 
705
- if self.troubleshoot_mode:
706
- print("Troubleshoot mode enabled - not removing helper columns")
707
- sys.stdout.flush()
708
-
709
- # Optionally remove temporary columns
710
- df.drop(columns=["mat_check", "sat_check", "combined_check"], inplace=True)
711
-
712
674
  return df
713
675
 
714
676
  except MissingColumnError as e:
@@ -839,8 +801,12 @@ class FaultConditionSix(FaultCondition):
839
801
  # Ensure all required columns are present
840
802
  self.check_required_columns(df)
841
803
 
842
- if self.troubleshoot_mode:
843
- self.troubleshoot_cols(df)
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()
844
810
 
845
811
  # Check analog outputs [data with units of %] are floats only
846
812
  columns_to_check = [
@@ -849,75 +815,45 @@ class FaultConditionSix(FaultCondition):
849
815
  self.heating_sig_col,
850
816
  self.cooling_sig_col,
851
817
  ]
818
+ self.check_analog_pct(df, columns_to_check)
852
819
 
853
- for col in columns_to_check:
854
- self.check_analog_pct(df, [col])
855
-
856
- # Create helper columns
857
- df["rat_minus_oat"] = abs(df[self.rat_col] - df[self.oat_col])
858
- df["percent_oa_calc"] = (df[self.mat_col] - df[self.rat_col]) / (
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]) / (
859
823
  df[self.oat_col] - df[self.rat_col]
860
824
  )
861
825
 
862
- # Weed out any negative values
863
- df["percent_oa_calc"] = df["percent_oa_calc"].apply(
864
- lambda x: x if x > 0 else 0
865
- )
826
+ # Replace negative values in percent_oa_calc with zero using vectorized operation
827
+ percent_oa_calc = percent_oa_calc.clip(lower=0)
866
828
 
867
- df["perc_OAmin"] = (
868
- self.ahu_min_oa_cfm_design / df[self.supply_fan_air_volume_col]
869
- )
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)
870
831
 
871
- df["percent_oa_calc_minus_perc_OAmin"] = abs(
872
- df["percent_oa_calc"] - df["perc_OAmin"]
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)
873
838
  )
874
839
 
875
- df["combined_check"] = operator.or_(
876
- # OS 1 htg mode
877
- (
878
- (df["rat_minus_oat"] >= self.oat_rat_delta_min)
879
- & (df["percent_oa_calc_minus_perc_OAmin"] > self.airflow_err_thres)
880
- )
881
- # Verify AHU is running in OS 1 htg mode in min OA
882
- & (
883
- (df[self.heating_sig_col] > 0.0)
884
- & (df[self.supply_vfd_speed_col] > 0.0)
885
- ), # OR
886
- # OS 4 mech clg mode
887
- (
888
- (df["rat_minus_oat"] >= self.oat_rat_delta_min)
889
- & (df["percent_oa_calc_minus_perc_OAmin"] > self.airflow_err_thres)
890
- )
891
- # Verify AHU is running in OS 4 clg mode in min OA
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)
892
843
  & (df[self.heating_sig_col] == 0.0)
893
844
  & (df[self.cooling_sig_col] > 0.0)
894
845
  & (df[self.supply_vfd_speed_col] > 0.0)
895
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr),
846
+ & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
896
847
  )
897
848
 
849
+ combined_check = os1_htg_mode_check | os4_clg_mode_check
850
+
898
851
  # Rolling sum to count consecutive trues
899
- rolling_sum = (
900
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
901
- )
852
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
853
+
902
854
  # Set flag to 1 if rolling sum equals the window size
903
855
  df["fc6_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
904
856
 
905
- if self.troubleshoot_mode:
906
- print("Troubleshoot mode enabled - not removing helper columns")
907
- sys.stdout.flush()
908
-
909
- # Optionally remove temporary columns
910
- df.drop(
911
- columns=[
912
- "rat_minus_oat",
913
- "percent_oa_calc",
914
- "perc_OAmin",
915
- "percent_oa_calc_minus_perc_OAmin",
916
- "combined_check",
917
- ],
918
- inplace=True,
919
- )
920
-
921
857
  return df
922
858
 
923
859
  except MissingColumnError as e:
@@ -1013,36 +949,25 @@ class FaultConditionSeven(FaultCondition):
1013
949
  # Ensure all required columns are present
1014
950
  self.check_required_columns(df)
1015
951
 
1016
- if self.troubleshoot_mode:
1017
- self.troubleshoot_cols(df)
1018
-
1019
952
  # Check analog outputs [data with units of %] are floats only
1020
953
  columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
1021
954
  self.check_analog_pct(df, columns_to_check)
1022
955
 
1023
- # Fault condition-specific checks / flags
1024
- df["sat_check"] = df[self.sat_setpoint_col] - self.supply_degf_err_thres
956
+ # Perform checks
957
+ sat_check = df[self.sat_setpoint_col] - self.supply_degf_err_thres
1025
958
 
1026
- df["combined_check"] = (
1027
- (df[self.sat_col] < df["sat_check"])
959
+ combined_check = (
960
+ (df[self.sat_col] < sat_check)
1028
961
  & (df[self.heating_sig_col] > 0.9)
1029
962
  & (df[self.supply_vfd_speed_col] > 0)
1030
963
  )
1031
964
 
1032
965
  # Rolling sum to count consecutive trues
1033
- rolling_sum = (
1034
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
1035
- )
966
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
967
+
1036
968
  # Set flag to 1 if rolling sum equals the window size
1037
969
  df["fc7_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1038
970
 
1039
- if self.troubleshoot_mode:
1040
- print("Troubleshoot mode enabled - not removing helper columns")
1041
- sys.stdout.flush()
1042
-
1043
- # Optionally remove temporary columns
1044
- df.drop(columns=["sat_check", "combined_check"], inplace=True)
1045
-
1046
971
  return df
1047
972
 
1048
973
  except MissingColumnError as e:
@@ -1147,9 +1072,6 @@ class FaultConditionEight(FaultCondition):
1147
1072
  # Ensure all required columns are present
1148
1073
  self.check_required_columns(df)
1149
1074
 
1150
- if self.troubleshoot_mode:
1151
- self.troubleshoot_cols(df)
1152
-
1153
1075
  # Check analog outputs [data with units of %] are floats only
1154
1076
  columns_to_check = [
1155
1077
  self.economizer_sig_col,
@@ -1157,36 +1079,26 @@ class FaultConditionEight(FaultCondition):
1157
1079
  ]
1158
1080
  self.check_analog_pct(df, columns_to_check)
1159
1081
 
1160
- df["sat_fan_mat"] = abs(
1082
+ # Perform checks
1083
+ sat_fan_mat = abs(
1161
1084
  df[self.sat_col] - self.delta_t_supply_fan - df[self.mat_col]
1162
1085
  )
1163
- df["sat_mat_sqrted"] = np.sqrt(
1086
+ sat_mat_sqrted = np.sqrt(
1164
1087
  self.supply_degf_err_thres**2 + self.mix_degf_err_thres**2
1165
1088
  )
1166
1089
 
1167
- df["combined_check"] = (
1168
- (df["sat_fan_mat"] > df["sat_mat_sqrted"])
1090
+ combined_check = (
1091
+ (sat_fan_mat > sat_mat_sqrted)
1169
1092
  & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
1170
1093
  & (df[self.cooling_sig_col] < 0.1)
1171
1094
  )
1172
1095
 
1173
1096
  # Rolling sum to count consecutive trues
1174
- rolling_sum = (
1175
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
1176
- )
1097
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1098
+
1177
1099
  # Set flag to 1 if rolling sum equals the window size
1178
1100
  df["fc8_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1179
1101
 
1180
- if self.troubleshoot_mode:
1181
- print("Troubleshoot mode enabled - not removing helper columns")
1182
- sys.stdout.flush()
1183
-
1184
- # Optionally remove temporary columns
1185
- df.drop(
1186
- columns=["sat_fan_mat", "sat_mat_sqrted", "combined_check"],
1187
- inplace=True,
1188
- )
1189
-
1190
1102
  return df
1191
1103
 
1192
1104
  except MissingColumnError as e:
@@ -1291,9 +1203,6 @@ class FaultConditionNine(FaultCondition):
1291
1203
  # Ensure all required columns are present
1292
1204
  self.check_required_columns(df)
1293
1205
 
1294
- if self.troubleshoot_mode:
1295
- self.troubleshoot_cols(df)
1296
-
1297
1206
  # Check analog outputs [data with units of %] are floats only
1298
1207
  columns_to_check = [
1299
1208
  self.economizer_sig_col,
@@ -1301,38 +1210,27 @@ class FaultConditionNine(FaultCondition):
1301
1210
  ]
1302
1211
  self.check_analog_pct(df, columns_to_check)
1303
1212
 
1304
- # Create helper columns
1305
- df["oat_minus_oaterror"] = df[self.oat_col] - self.outdoor_degf_err_thres
1306
- df["satsp_delta_saterr"] = (
1213
+ # Perform calculations
1214
+ oat_minus_oaterror = df[self.oat_col] - self.outdoor_degf_err_thres
1215
+ satsp_delta_saterr = (
1307
1216
  df[self.sat_setpoint_col]
1308
1217
  - self.delta_t_supply_fan
1309
1218
  + self.supply_degf_err_thres
1310
1219
  )
1311
1220
 
1312
- df["combined_check"] = (
1313
- (df["oat_minus_oaterror"] > df["satsp_delta_saterr"])
1221
+ combined_check = (
1222
+ (oat_minus_oaterror > satsp_delta_saterr)
1314
1223
  # verify AHU is in OS2 only free cooling mode
1315
1224
  & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
1316
1225
  & (df[self.cooling_sig_col] < 0.1)
1317
1226
  )
1318
1227
 
1319
1228
  # Rolling sum to count consecutive trues
1320
- rolling_sum = (
1321
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
1322
- )
1229
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1230
+
1323
1231
  # Set flag to 1 if rolling sum equals the window size
1324
1232
  df["fc9_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1325
1233
 
1326
- if self.troubleshoot_mode:
1327
- print("Troubleshoot mode enabled - not removing helper columns")
1328
- sys.stdout.flush()
1329
-
1330
- # Optionally remove temporary columns
1331
- df.drop(
1332
- columns=["oat_minus_oaterror", "satsp_delta_saterr", "combined_check"],
1333
- inplace=True,
1334
- )
1335
-
1336
1234
  return df
1337
1235
 
1338
1236
  except MissingColumnError as e:
@@ -1433,9 +1331,6 @@ class FaultConditionTen(FaultCondition):
1433
1331
  # Ensure all required columns are present
1434
1332
  self.check_required_columns(df)
1435
1333
 
1436
- if self.troubleshoot_mode:
1437
- self.troubleshoot_cols(df)
1438
-
1439
1334
  # Check analog outputs [data with units of %] are floats only
1440
1335
  columns_to_check = [
1441
1336
  self.economizer_sig_col,
@@ -1443,35 +1338,25 @@ class FaultConditionTen(FaultCondition):
1443
1338
  ]
1444
1339
  self.check_analog_pct(df, columns_to_check)
1445
1340
 
1446
- df["abs_mat_minus_oat"] = abs(df[self.mat_col] - df[self.oat_col])
1447
- df["mat_oat_sqrted"] = np.sqrt(
1341
+ # Perform calculations
1342
+ abs_mat_minus_oat = abs(df[self.mat_col] - df[self.oat_col])
1343
+ mat_oat_sqrted = np.sqrt(
1448
1344
  self.mix_degf_err_thres**2 + self.outdoor_degf_err_thres**2
1449
1345
  )
1450
1346
 
1451
- df["combined_check"] = (
1452
- (df["abs_mat_minus_oat"] > df["mat_oat_sqrted"])
1453
- # verify AHU is running in OS 3 clg mode in min OA
1347
+ combined_check = (
1348
+ (abs_mat_minus_oat > mat_oat_sqrted)
1349
+ # Verify AHU is running in OS 3 cooling mode with minimum OA
1454
1350
  & (df[self.cooling_sig_col] > 0.01)
1455
1351
  & (df[self.economizer_sig_col] > 0.9)
1456
1352
  )
1457
1353
 
1458
1354
  # Rolling sum to count consecutive trues
1459
- rolling_sum = (
1460
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
1461
- )
1355
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1356
+
1462
1357
  # Set flag to 1 if rolling sum equals the window size
1463
1358
  df["fc10_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1464
1359
 
1465
- if self.troubleshoot_mode:
1466
- print("Troubleshoot mode enabled - not removing helper columns")
1467
- sys.stdout.flush()
1468
-
1469
- # Optionally remove temporary columns
1470
- df.drop(
1471
- columns=["abs_mat_minus_oat", "mat_oat_sqrted", "combined_check"],
1472
- inplace=True,
1473
- )
1474
-
1475
1360
  return df
1476
1361
 
1477
1362
  except MissingColumnError as e:
@@ -1575,9 +1460,6 @@ class FaultConditionEleven(FaultCondition):
1575
1460
  # Ensure all required columns are present
1576
1461
  self.check_required_columns(df)
1577
1462
 
1578
- if self.troubleshoot_mode:
1579
- self.troubleshoot_cols(df)
1580
-
1581
1463
  # Check analog outputs [data with units of %] are floats only
1582
1464
  columns_to_check = [
1583
1465
  self.economizer_sig_col,
@@ -1585,37 +1467,27 @@ class FaultConditionEleven(FaultCondition):
1585
1467
  ]
1586
1468
  self.check_analog_pct(df, columns_to_check)
1587
1469
 
1588
- df["oat_plus_oaterror"] = df[self.oat_col] + self.outdoor_degf_err_thres
1589
- df["satsp_delta_saterr"] = (
1470
+ # Perform calculations without creating DataFrame columns
1471
+ oat_plus_oaterror = df[self.oat_col] + self.outdoor_degf_err_thres
1472
+ satsp_delta_saterr = (
1590
1473
  df[self.sat_setpoint_col]
1591
1474
  - self.delta_t_supply_fan
1592
1475
  - self.supply_degf_err_thres
1593
1476
  )
1594
1477
 
1595
- df["combined_check"] = (
1596
- (df["oat_plus_oaterror"] < df["satsp_delta_saterr"])
1597
- # verify ahu is running in OS 3 clg mode in 100 OA
1478
+ combined_check = (
1479
+ (oat_plus_oaterror < satsp_delta_saterr)
1480
+ # Verify AHU is running in OS 3 cooling mode with 100% OA
1598
1481
  & (df[self.cooling_sig_col] > 0.01)
1599
1482
  & (df[self.economizer_sig_col] > 0.9)
1600
1483
  )
1601
1484
 
1602
1485
  # Rolling sum to count consecutive trues
1603
- rolling_sum = (
1604
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
1605
- )
1486
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1487
+
1606
1488
  # Set flag to 1 if rolling sum equals the window size
1607
1489
  df["fc11_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1608
1490
 
1609
- if self.troubleshoot_mode:
1610
- print("Troubleshoot mode enabled - not removing helper columns")
1611
- sys.stdout.flush()
1612
-
1613
- # Optionally remove temporary columns
1614
- df.drop(
1615
- columns=["oat_plus_oaterror", "satsp_delta_saterr", "combined_check"],
1616
- inplace=True,
1617
- )
1618
-
1619
1491
  return df
1620
1492
 
1621
1493
  except MissingColumnError as e:
@@ -1720,9 +1592,6 @@ class FaultConditionTwelve(FaultCondition):
1720
1592
  # Ensure all required columns are present
1721
1593
  self.check_required_columns(df)
1722
1594
 
1723
- if self.troubleshoot_mode:
1724
- self.troubleshoot_cols(df)
1725
-
1726
1595
  # Check analog outputs [data with units of %] are floats only
1727
1596
  columns_to_check = [
1728
1597
  self.economizer_sig_col,
@@ -1730,45 +1599,32 @@ class FaultConditionTwelve(FaultCondition):
1730
1599
  ]
1731
1600
  self.check_analog_pct(df, columns_to_check)
1732
1601
 
1733
- # Create helper columns
1734
- df["sat_minus_saterr_delta_supply_fan"] = (
1602
+ # Perform calculations without creating DataFrame columns
1603
+ sat_minus_saterr_delta_supply_fan = (
1735
1604
  df[self.sat_col] - self.supply_degf_err_thres - self.delta_t_supply_fan
1736
1605
  )
1737
- df["mat_plus_materr"] = df[self.mat_col] + self.mix_degf_err_thres
1606
+ mat_plus_materr = df[self.mat_col] + self.mix_degf_err_thres
1738
1607
 
1739
- df["combined_check"] = operator.or_(
1740
- # OS4 AHU state clg @ min OA
1741
- (df["sat_minus_saterr_delta_supply_fan"] > df["mat_plus_materr"])
1742
- # verify AHU in OS4 mode
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
1743
1613
  & (df[self.cooling_sig_col] > 0.01)
1744
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr), # OR
1745
- (df["sat_minus_saterr_delta_supply_fan"] > df["mat_plus_materr"])
1746
- # verify AHU is running in OS 3 clg mode in 100 OA
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
1747
1618
  & (df[self.cooling_sig_col] > 0.01)
1748
1619
  & (df[self.economizer_sig_col] > 0.9),
1749
1620
  )
1750
1621
 
1751
1622
  # Rolling sum to count consecutive trues
1752
- rolling_sum = (
1753
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
1754
- )
1623
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1624
+
1755
1625
  # Set flag to 1 if rolling sum equals the window size
1756
1626
  df["fc12_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1757
1627
 
1758
- if self.troubleshoot_mode:
1759
- print("Troubleshoot mode enabled - not removing helper columns")
1760
- sys.stdout.flush()
1761
-
1762
- # Optionally remove temporary columns
1763
- df.drop(
1764
- columns=[
1765
- "sat_minus_saterr_delta_supply_fan",
1766
- "mat_plus_materr",
1767
- "combined_check",
1768
- ],
1769
- inplace=True,
1770
- )
1771
-
1772
1628
  return df
1773
1629
 
1774
1630
  except MissingColumnError as e:
@@ -1869,9 +1725,6 @@ class FaultConditionThirteen(FaultCondition):
1869
1725
  # Ensure all required columns are present
1870
1726
  self.check_required_columns(df)
1871
1727
 
1872
- if self.troubleshoot_mode:
1873
- self.troubleshoot_cols(df)
1874
-
1875
1728
  # Check analog outputs [data with units of %] are floats only
1876
1729
  columns_to_check = [
1877
1730
  self.economizer_sig_col,
@@ -1879,40 +1732,30 @@ class FaultConditionThirteen(FaultCondition):
1879
1732
  ]
1880
1733
  self.check_analog_pct(df, columns_to_check)
1881
1734
 
1882
- # Create helper columns
1883
- df["sat_greater_than_sp_calc"] = (
1735
+ # Perform calculation without creating DataFrame columns
1736
+ sat_greater_than_sp_calc = (
1884
1737
  df[self.sat_col]
1885
1738
  > df[self.sat_setpoint_col] + self.supply_degf_err_thres
1886
1739
  )
1887
1740
 
1888
- df["combined_check"] = operator.or_(
1889
- ((df["sat_greater_than_sp_calc"]))
1890
- # OS4 AHU state clg @ min OA
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)
1891
1745
  & (df[self.cooling_sig_col] > 0.01)
1892
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr), # OR
1893
- ((df["sat_greater_than_sp_calc"]))
1894
- # verify ahu is running in OS 3 clg mode in 100 OA
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)
1895
1749
  & (df[self.cooling_sig_col] > 0.01)
1896
1750
  & (df[self.economizer_sig_col] > 0.9),
1897
1751
  )
1898
1752
 
1899
1753
  # Rolling sum to count consecutive trues
1900
- rolling_sum = (
1901
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
1902
- )
1754
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1755
+
1903
1756
  # Set flag to 1 if rolling sum equals the window size
1904
1757
  df["fc13_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
1905
1758
 
1906
- if self.troubleshoot_mode:
1907
- print("Troubleshoot mode enabled - not removing helper columns")
1908
- sys.stdout.flush()
1909
-
1910
- # Optionally remove temporary columns
1911
- df.drop(
1912
- columns=["sat_greater_than_sp_calc", "combined_check"],
1913
- inplace=True,
1914
- )
1915
-
1916
1759
  return df
1917
1760
 
1918
1761
  except MissingColumnError as e:
@@ -2020,9 +1863,6 @@ class FaultConditionFourteen(FaultCondition):
2020
1863
  # Ensure all required columns are present
2021
1864
  self.check_required_columns(df)
2022
1865
 
2023
- if self.troubleshoot_mode:
2024
- self.troubleshoot_cols(df)
2025
-
2026
1866
  # Check analog outputs [data with units of %] are floats only
2027
1867
  columns_to_check = [
2028
1868
  self.economizer_sig_col,
@@ -2032,46 +1872,33 @@ class FaultConditionFourteen(FaultCondition):
2032
1872
  ]
2033
1873
  self.check_analog_pct(df, columns_to_check)
2034
1874
 
2035
- # Create helper columns
2036
- df["clg_delta_temp"] = (
1875
+ # Calculate necessary checks
1876
+ clg_delta_temp = (
2037
1877
  df[self.clg_coil_enter_temp_col] - df[self.clg_coil_leave_temp_col]
2038
1878
  )
2039
-
2040
- df["clg_delta_sqrted"] = (
1879
+ clg_delta_sqrted = (
2041
1880
  np.sqrt(
2042
1881
  self.coil_temp_enter_err_thres**2 + self.coil_temp_leav_err_thres**2
2043
1882
  )
2044
1883
  + self.delta_t_supply_fan
2045
1884
  )
2046
1885
 
2047
- df["combined_check"] = operator.or_(
2048
- (df["clg_delta_temp"] >= df["clg_delta_sqrted"])
2049
- # verify AHU is in OS2 only free cooling mode
1886
+ # Perform combined checks without adding intermediate columns to DataFrame
1887
+ combined_check = operator.or_(
1888
+ (clg_delta_temp >= clg_delta_sqrted)
2050
1889
  & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
2051
1890
  & (df[self.cooling_sig_col] < 0.1), # OR
2052
- (df["clg_delta_temp"] >= df["clg_delta_sqrted"])
2053
- # verify AHU is running in OS 1 at near full heat
1891
+ (clg_delta_temp >= clg_delta_sqrted)
2054
1892
  & (df[self.heating_sig_col] > 0.0)
2055
1893
  & (df[self.supply_vfd_speed_col] > 0.0),
2056
1894
  )
2057
1895
 
2058
1896
  # Rolling sum to count consecutive trues
2059
- rolling_sum = (
2060
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
2061
- )
1897
+ rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()
1898
+
2062
1899
  # Set flag to 1 if rolling sum equals the window size
2063
1900
  df["fc14_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
2064
1901
 
2065
- if self.troubleshoot_mode:
2066
- print("Troubleshoot mode enabled - not removing helper columns")
2067
- sys.stdout.flush()
2068
-
2069
- # Optionally remove temporary columns
2070
- df.drop(
2071
- columns=["clg_delta_temp", "clg_delta_sqrted", "combined_check"],
2072
- inplace=True,
2073
- )
2074
-
2075
1902
  return df
2076
1903
 
2077
1904
  except MissingColumnError as e:
@@ -2251,3 +2078,207 @@ class FaultConditionFifteen(FaultCondition):
2251
2078
  print(f"Error: {e.message}")
2252
2079
  sys.stdout.flush()
2253
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