open-fdd 0.1.6__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.
@@ -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
 
@@ -800,6 +801,13 @@ class FaultConditionSix(FaultCondition):
800
801
  # Ensure all required columns are present
801
802
  self.check_required_columns(df)
802
803
 
804
+ # Check for zeros in the columns that could lead to division by zero errors
805
+ cols_to_check = [self.rat_col, self.oat_col, self.supply_fan_air_volume_col]
806
+ if df[cols_to_check].eq(0).any().any():
807
+ print(f"Warning: Zero values found in columns: {cols_to_check}")
808
+ print("This may cause division by zero errors.")
809
+ sys.stdout.flush()
810
+
803
811
  # Check analog outputs [data with units of %] are floats only
804
812
  columns_to_check = [
805
813
  self.supply_vfd_speed_col,
@@ -2088,6 +2096,7 @@ class FaultConditionSixteen(FaultCondition):
2088
2096
 
2089
2097
  self.oat_low_threshold = dict_.get("OAT_LOW_THRES", 32.0)
2090
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)
2091
2100
 
2092
2101
  # Validate that threshold parameters are floats and within 0.0 and 1.0 for efficiency values
2093
2102
  for param, value in [
@@ -2097,6 +2106,7 @@ class FaultConditionSixteen(FaultCondition):
2097
2106
  ("erv_efficiency_max_cooling", self.erv_efficiency_max_cooling),
2098
2107
  ("oat_low_threshold", self.oat_low_threshold),
2099
2108
  ("oat_high_threshold", self.oat_high_threshold),
2109
+ ("oat_rat_delta_min", self.oat_rat_delta_min),
2100
2110
  ]:
2101
2111
  if not isinstance(value, float):
2102
2112
  raise InvalidParameterError(
@@ -2177,6 +2187,15 @@ class FaultConditionSixteen(FaultCondition):
2177
2187
  )
2178
2188
 
2179
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
+
2180
2199
  # Calculate the temperature differences
2181
2200
  delta_temp_oa = df[self.erv_oat_leaving_col] - df[self.erv_oat_enter_col]
2182
2201
  delta_temp_ea = df[self.erv_eat_enter_col] - df[self.erv_oat_enter_col]
@@ -2194,31 +2213,43 @@ class FaultConditionSixteen(FaultCondition):
2194
2213
  # Fan must be on for a fault to be considered
2195
2214
  fan_on = df[self.supply_vfd_speed_col] > 0.1
2196
2215
 
2197
- # Combined heating and cooling checks
2216
+ # Determine if the conditions are for heating or cooling based on OAT
2198
2217
  cold_outside = df[self.erv_oat_enter_col] <= self.oat_low_threshold
2199
2218
  hot_outside = df[self.erv_oat_enter_col] >= self.oat_high_threshold
2200
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
2201
2228
  heating_fault = (
2202
2229
  (
2203
2230
  (df["erv_efficiency_oa"] < self.erv_efficiency_min_heating)
2204
2231
  | (df["erv_efficiency_oa"] > self.erv_efficiency_max_heating)
2205
2232
  )
2206
2233
  & cold_outside
2234
+ & good_delta_check
2207
2235
  & fan_on
2208
2236
  )
2209
2237
 
2238
+ # Apply cooling fault logic
2210
2239
  cooling_fault = (
2211
2240
  (
2212
2241
  (df["erv_efficiency_oa"] < self.erv_efficiency_min_cooling)
2213
2242
  | (df["erv_efficiency_oa"] > self.erv_efficiency_max_cooling)
2214
2243
  )
2215
2244
  & hot_outside
2245
+ & good_delta_check
2216
2246
  & fan_on
2217
2247
  )
2218
2248
 
2249
+ # Combine the faults
2219
2250
  df["combined_checks"] = heating_fault | cooling_fault
2220
2251
 
2221
- # Apply rolling sum
2252
+ # Apply rolling sum to combined checks to account for rolling window
2222
2253
  df["fc16_flag"] = (
2223
2254
  df["combined_checks"]
2224
2255
  .rolling(window=self.rolling_window_size)
@@ -11,6 +11,9 @@ class HelperUtils:
11
11
  def set_config_dict(self, config_dict):
12
12
  self.config_dict = config_dict
13
13
 
14
+ def clean_nan_values(self, df):
15
+ return SharedUtils.clean_nan_values(df)
16
+
14
17
  def float_int_check_err(self, col):
15
18
  return SharedUtils.float_int_check_err(col)
16
19
 
@@ -48,8 +48,10 @@ class SharedUtils:
48
48
  """
49
49
 
50
50
  print(
51
- "Warning: If data has a one minute or less sampling frequency a rolling average will be automatically applied"
51
+ "Warning: If data has a one minute or less sampling \n"
52
+ "frequency a rolling average will be automatically applied"
52
53
  )
54
+
53
55
  sys.stdout.flush()
54
56
 
55
57
  time_diff = df.index.to_series().diff().iloc[1:]
@@ -73,3 +75,16 @@ class SharedUtils:
73
75
  )
74
76
  sys.stdout.flush()
75
77
  return df
78
+
79
+ @staticmethod
80
+ def clean_nan_values(df: pd.DataFrame) -> pd.DataFrame:
81
+ for col in df.columns:
82
+ if df[col].isnull().any():
83
+ print(f"NaN values found in column: {col}")
84
+
85
+ # Remove rows with any NaN values, then forward and backfill
86
+ df = df.dropna().ffill().bfill()
87
+ print("DataFrame has been cleaned for NaNs")
88
+ print("and has also been forward and backfilled.")
89
+ sys.stdout.flush()
90
+ return df
@@ -914,16 +914,8 @@ class FaultCodeSixteenReport(BaseFaultReport):
914
914
  # Calculate the efficiency before plotting using FaultConditionSixteen method
915
915
  df = self.fc16.calculate_erv_efficiency(df)
916
916
 
917
- print("=" * 50)
918
- print("Info: ERV calculated efficiency ")
919
- print("summary statistics ")
920
- print(df["erv_efficiency_oa"].describe())
921
- print("=" * 50)
922
-
923
- sys.stdout.flush()
924
-
925
- # Create the plot with four subplots
926
- fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1, figsize=(25, 10))
917
+ # Create the plot with five subplots
918
+ fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(5, 1, figsize=(25, 14))
927
919
  fig.suptitle("Fault Conditions 16 Plot")
928
920
 
929
921
  # Plot ERV Outdoor Air Side Temps
@@ -953,6 +945,27 @@ class FaultCodeSixteenReport(BaseFaultReport):
953
945
  ax4.set_ylabel("Fault Flags")
954
946
  ax4.legend(loc="best")
955
947
 
948
+ # New Plot: Compare Distribution of OAT (Overall vs Fault True)
949
+ fault_true_df = df[df[self.fault_col] == 1]
950
+ data_to_plot = [
951
+ df[self.erv_oat_enter_col].dropna(), # Overall OAT
952
+ fault_true_df[self.erv_oat_enter_col].dropna(), # OAT when Fault is True
953
+ ]
954
+ ax5.boxplot(
955
+ data_to_plot,
956
+ vert=False,
957
+ patch_artist=True,
958
+ labels=["Overall OAT", "OAT when Fault 16 is True"],
959
+ boxprops=dict(facecolor="lightblue"),
960
+ medianprops=dict(color="red"),
961
+ showmeans=True,
962
+ meanline=True,
963
+ meanprops=dict(color="green"),
964
+ )
965
+ ax5.set_xlabel("Outside Air Temperature (°F)")
966
+ ax5.set_title("Comparison of OAT Distribution")
967
+ ax5.grid(True)
968
+
956
969
  plt.tight_layout(rect=[0, 0.03, 1, 0.95])
957
970
  plt.show()
958
971
  plt.close()
@@ -12,6 +12,7 @@ TEST_DUCT_STATIC_ERR_THRESHOLD = 0.1
12
12
  TEST_DUCT_STATIC_COL = "duct_static"
13
13
  TEST_DUCT_STATIC_SETPOINT_COL = "duct_static_setpoint"
14
14
  TEST_SUPPLY_VFD_SPEED_COL = "supply_vfd_speed"
15
+
15
16
  ROLLING_WINDOW_SIZE = 5
16
17
 
17
18
  # Initialize FaultConditionOne with a dictionary
@@ -13,7 +13,6 @@ $ py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc16.py -rP -s
13
13
  ERV effectiveness should be within specified thresholds based on OAT.
14
14
  """
15
15
 
16
-
17
16
  # Constants
18
17
  TEST_ERV_EFFICIENCY_MIN_HEATING = 0.65
19
18
  TEST_ERV_EFFICIENCY_MAX_HEATING = 0.8
@@ -21,7 +20,7 @@ TEST_ERV_EFFICIENCY_MIN_COOLING = 0.45
21
20
  TEST_ERV_EFFICIENCY_MAX_COOLING = 0.6
22
21
  TEST_OAT_LOW_THRESHOLD = 32.0
23
22
  TEST_OAT_HIGH_THRESHOLD = 80.0
24
- TEST_ERV_DEGF_ERR_THRES = 2.0
23
+ TEST_OAT_RAT_DELTA_THRES = 15.0
25
24
  TEST_ERV_OAT_ENTER_COL = "erv_oat_enter"
26
25
  TEST_ERV_OAT_LEAVING_COL = "erv_oat_leaving"
27
26
  TEST_ERV_EAT_ENTER_COL = "erv_eat_enter"
@@ -37,7 +36,7 @@ fault_condition_params = {
37
36
  "ERV_EFFICIENCY_MAX_COOLING": TEST_ERV_EFFICIENCY_MAX_COOLING,
38
37
  "OAT_LOW_THRESHOLD": TEST_OAT_LOW_THRESHOLD,
39
38
  "OAT_HIGH_THRESHOLD": TEST_OAT_HIGH_THRESHOLD,
40
- "ERV_DEGF_ERR_THRES": TEST_ERV_DEGF_ERR_THRES,
39
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
41
40
  "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
42
41
  "ERV_OAT_LEAVING_COL": TEST_ERV_OAT_LEAVING_COL,
43
42
  "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
@@ -107,6 +106,22 @@ class TestFaultConditionSixteen:
107
106
  )
108
107
  assert actual == expected, message
109
108
 
109
+ def test_fault_with_insufficient_oat_rat_delta(self):
110
+ """Test that no fault is raised when the OAT-RAT delta is below the minimum threshold."""
111
+ data = {
112
+ TEST_ERV_OAT_ENTER_COL: [10, 10, 10, 10, 10, 10],
113
+ TEST_ERV_OAT_LEAVING_COL: [20.0, 20.5, 20.8, 20.6, 20.2, 20.4],
114
+ TEST_ERV_EAT_ENTER_COL: [24, 24, 24, 24, 24, 24], # Low delta with OAT
115
+ TEST_ERV_EAT_LEAVING_COL: [22, 22.5, 22.2, 22.4, 22.1, 22.3],
116
+ TEST_SUPPLY_VFD_SPEED_COL: [0.5, 0.6, 0.5, 0.7, 0.5, 0.6],
117
+ }
118
+ df = pd.DataFrame(data)
119
+ results = fc16.apply(df)
120
+ actual = results["fc16_flag"].sum()
121
+ expected = 0 # No fault should be triggered due to insufficient delta
122
+ message = f"FC16 fault_with_insufficient_oat_rat_delta actual is {actual} and expected is {expected}"
123
+ assert actual == expected, message
124
+
110
125
 
111
126
  class TestFaultOnInvalidParams:
112
127
 
@@ -121,7 +136,7 @@ class TestFaultOnInvalidParams:
121
136
  "ERV_EFFICIENCY_MAX_COOLING": 0.6,
122
137
  "OAT_LOW_THRESHOLD": 32.0,
123
138
  "OAT_HIGH_THRESHOLD": 80.0,
124
- "ERV_DEGF_ERR_THRES": 2.0,
139
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
125
140
  "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
126
141
  "ERV_OAT_LEAVING_COL": TEST_ERV_OAT_LEAVING_COL,
127
142
  "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
@@ -142,7 +157,7 @@ class TestFaultOnInvalidParams:
142
157
  "ERV_EFFICIENCY_MAX_COOLING": 0.6,
143
158
  "OAT_LOW_THRESHOLD": 32.0,
144
159
  "OAT_HIGH_THRESHOLD": 80.0,
145
- "ERV_DEGF_ERR_THRES": 2.0,
160
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
146
161
  "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
147
162
  "ERV_OAT_LEAVING_COL": TEST_ERV_OAT_LEAVING_COL,
148
163
  "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
@@ -166,7 +181,7 @@ class TestFaultOnMissingColumns:
166
181
  "ERV_EFFICIENCY_MAX_COOLING": 0.6,
167
182
  "OAT_LOW_THRESHOLD": 32.0,
168
183
  "OAT_HIGH_THRESHOLD": 80.0,
169
- "ERV_DEGF_ERR_THRES": 2.0,
184
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
170
185
  "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
171
186
  "ERV_OAT_LEAVING_COL": None, # Missing column
172
187
  "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: open_fdd
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: A package for fault detection and diagnosis in HVAC systems
5
5
  Home-page: https://github.com/bbartling/open-fdd
6
6
  Author: Ben Bartling
@@ -1,21 +1,21 @@
1
1
  open_fdd/__init__.py,sha256=iGj8QTOZJUTE4nNnBiCHXEXsOdV6YvKcGiLrnOusJCg,1411
2
2
  open_fdd/air_handling_unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- open_fdd/air_handling_unit/faults/__init__.py,sha256=NBdGNlp0MTUPNB-43zgnYs3iYqFhEeNa3q_VgDPGAoo,91603
3
+ open_fdd/air_handling_unit/faults/__init__.py,sha256=Gc2VjfRDEX0-JcIrPuV692T3Ek3GGlTgrscqUXslrwY,93217
4
4
  open_fdd/air_handling_unit/faults/fault_condition.py,sha256=UN2_k2AZWMDoO4oPAnJW3fvhkHlMkLXZikzU2lXAOuQ,2364
5
- open_fdd/air_handling_unit/faults/helper_utils.py,sha256=-Bd1mMMLnFmpNPe7eC23kMJxPtazsIyz5P7W6Yqlu4w,12580
6
- open_fdd/air_handling_unit/faults/shared_utils.py,sha256=Kp8ZUBhtrh-jU2Q_bbXTpnbVVUAyacp41tDMknY50i4,2252
7
- open_fdd/air_handling_unit/reports/__init__.py,sha256=iXIqEaaO_8yRnlVUuYMcc_L0xQf3iqSYi3ykIzeGBf8,40124
5
+ open_fdd/air_handling_unit/faults/helper_utils.py,sha256=el_s2BC9Q14GXUGnazOiBBhzVUMsPBP9sxEG1UIQ6Gw,12668
6
+ open_fdd/air_handling_unit/faults/shared_utils.py,sha256=rAx27IsG_7OEHLLFHool6o9LAuaMyYisit9gLu8ZVQk,2802
7
+ open_fdd/air_handling_unit/reports/__init__.py,sha256=JccL19Spj6vE0Jo_57CmZEFucu4WbCPj1-pIwR5LmPg,40741
8
8
  open_fdd/air_handling_unit/reports/fault_report.py,sha256=QxYLJzoLTwf1N0nls2XMmhHJvBSgGCBNT0KxA59QG4Y,1442
9
9
  open_fdd/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  open_fdd/tests/ahu/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- open_fdd/tests/ahu/test_ahu_fc1.py,sha256=ojpdYGZtuIYAKnZ4W9KhxQuoyXnGEI5N7braQXh3kAw,5437
11
+ open_fdd/tests/ahu/test_ahu_fc1.py,sha256=Pw5XlQ19c5Sit0Lyuor8L8lKgxl5PFn4UhruIHyKDfc,5439
12
12
  open_fdd/tests/ahu/test_ahu_fc10.py,sha256=niYL7fi6OlgP0wnF8hNh9A07PLzHiRZkyRkrA1zoL2s,4577
13
13
  open_fdd/tests/ahu/test_ahu_fc11.py,sha256=mdXlGiEMPkPfshf3NN_nJavL74e4HCmkJQMu86aZc6Q,4723
14
14
  open_fdd/tests/ahu/test_ahu_fc12.py,sha256=5T-XcM6xm9KHrc121uPGC9JWLCYehrAYk0KcbmGgYjw,5848
15
15
  open_fdd/tests/ahu/test_ahu_fc13.py,sha256=vJlSy4e2WV9hx02P0SiJ75I1DWL2lZ0p7-AWlw97pks,5725
16
16
  open_fdd/tests/ahu/test_ahu_fc14.py,sha256=MU0LKqIuoQ_dJ0Kij8_A0YyimCMvUwL6IlMwpQhDbqI,8052
17
17
  open_fdd/tests/ahu/test_ahu_fc15.py,sha256=SIolJ9vnJwnpKfRW2ALugYWySuIiZ9_repWP9fdb5H4,7661
18
- open_fdd/tests/ahu/test_ahu_fc16.py,sha256=3xhNfN2pKon-2_jyCUSh_JEKSv9jYGiTxRjLOL7uvVI,8052
18
+ open_fdd/tests/ahu/test_ahu_fc16.py,sha256=f656EZCWEKOXE2GZHUayaLc8bjMZWX_QB99QDBJxF4Y,9020
19
19
  open_fdd/tests/ahu/test_ahu_fc2.py,sha256=CjmO_WUyaSHs17ifCCew3GBJ43nYG55uGL0vHDZpAq8,4736
20
20
  open_fdd/tests/ahu/test_ahu_fc3.py,sha256=NB6pOXDS-R4P0LNoRN8ItAqhhLnGnGuAHZha32Qw-hE,4658
21
21
  open_fdd/tests/ahu/test_ahu_fc4.py,sha256=vV8jEnFuNGLfhCoTVz29RsIcoDpDOMWg722G0aBEXaE,6304
@@ -24,8 +24,8 @@ open_fdd/tests/ahu/test_ahu_fc6.py,sha256=66dwv0EBU_ujZK-J9Ki5a3fnXlk17nOwmtKDiQ
24
24
  open_fdd/tests/ahu/test_ahu_fc7.py,sha256=sABbw2m7WlAXbsqfDD323vfEfg606ThI0QzQyB-OjFo,2469
25
25
  open_fdd/tests/ahu/test_ahu_fc8.py,sha256=UZy6BP2PgV1FROUPqMORTx8YnT5ZvqVDhut_Ar81494,4663
26
26
  open_fdd/tests/ahu/test_ahu_fc9.py,sha256=b-eIzhNzjZUjVNsP0JAHkOgZu-BtDuPeNnblVVm-jU8,4796
27
- open_fdd-0.1.6.dist-info/LICENSE,sha256=eghao_GGx_0gB2Sll3x2vV29knONEzUQKrkaXpX1F7w,1087
28
- open_fdd-0.1.6.dist-info/METADATA,sha256=IER_eG7nwqbDIoDA2a9LyFgh20pOeBYhAGHlhT1PFLo,6958
29
- open_fdd-0.1.6.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
30
- open_fdd-0.1.6.dist-info/top_level.txt,sha256=Q7sB6UB2d8Ch1v_xIsTiNegmgcCXPkwkrxK3ug6VEOs,9
31
- open_fdd-0.1.6.dist-info/RECORD,,
27
+ open_fdd-0.1.7.dist-info/LICENSE,sha256=eghao_GGx_0gB2Sll3x2vV29knONEzUQKrkaXpX1F7w,1087
28
+ open_fdd-0.1.7.dist-info/METADATA,sha256=891NmSrFes9HOcjBiW0Lp7ixCbjN4lhorEWtOWICg0E,6958
29
+ open_fdd-0.1.7.dist-info/WHEEL,sha256=uCRv0ZEik_232NlR4YDw4Pv3Ajt5bKvMH13NUU7hFuI,91
30
+ open_fdd-0.1.7.dist-info/top_level.txt,sha256=Q7sB6UB2d8Ch1v_xIsTiNegmgcCXPkwkrxK3ug6VEOs,9
31
+ open_fdd-0.1.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (74.0.0)
2
+ Generator: setuptools (74.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5