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
@@ -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
@@ -1,6 +1,9 @@
1
1
  import matplotlib.pyplot as plt
2
2
  from open_fdd.air_handling_unit.reports.fault_report import BaseFaultReport
3
+ from open_fdd.air_handling_unit.faults import FaultConditionSixteen
3
4
  import pandas as pd
5
+ import numpy as np
6
+ import sys
4
7
 
5
8
 
6
9
  class FaultCodeOneReport(BaseFaultReport):
@@ -892,3 +895,107 @@ class FaultCodeFifteenReport(BaseFaultReport):
892
895
  ),
893
896
  }
894
897
  return summary
898
+
899
+
900
+ class FaultCodeSixteenReport(BaseFaultReport):
901
+ def __init__(self, config):
902
+ super().__init__(config, "fc16_flag")
903
+
904
+ self.supply_vfd_speed_col = config["SUPPLY_VFD_SPEED_COL"]
905
+ self.erv_oat_enter_col = config["ERV_OAT_ENTER_COL"]
906
+ self.erv_oat_leaving_col = config["ERV_OAT_LEAVING_COL"]
907
+ self.erv_eat_enter_col = config["ERV_EAT_ENTER_COL"]
908
+ self.erv_eat_leaving_col = config["ERV_EAT_LEAVING_COL"]
909
+
910
+ # Instantiate FaultConditionSixteen to access its methods
911
+ self.fc16 = FaultConditionSixteen(config)
912
+
913
+ def create_plot(self, df: pd.DataFrame):
914
+ # Calculate the efficiency before plotting using FaultConditionSixteen method
915
+ df = self.fc16.calculate_erv_efficiency(df)
916
+
917
+ # Create the plot with five subplots
918
+ fig, (ax1, ax2, ax3, ax4, ax5) = plt.subplots(5, 1, figsize=(25, 14))
919
+ fig.suptitle("Fault Conditions 16 Plot")
920
+
921
+ # Plot ERV Outdoor Air Side Temps
922
+ ax1.plot(df.index, df[self.erv_oat_enter_col], label="Enter", color="blue")
923
+ ax1.plot(df.index, df[self.erv_oat_leaving_col], label="Leaving", color="green")
924
+ ax1.legend(loc="best")
925
+ ax1.set_ylabel("ERV Outdoor Air Side Temps °F")
926
+
927
+ # Plot ERV Exhaust Air Side Temps
928
+ ax2.plot(df.index, df[self.erv_eat_enter_col], label="Enter", color="red")
929
+ ax2.plot(
930
+ df.index, df[self.erv_eat_leaving_col], label="Leaving", color="purple"
931
+ )
932
+ ax2.legend(loc="best")
933
+ ax2.set_ylabel("ERV Exhaust Air Side Temps °F")
934
+
935
+ # Plot ERV Efficiency
936
+ ax3.plot(
937
+ df.index, df["erv_efficiency_oa"], label="ERV Efficiency OA", color="b"
938
+ )
939
+ ax3.legend(loc="best")
940
+ ax3.set_ylabel("ERV Efficiency OA")
941
+
942
+ # Plot Fault Flags
943
+ ax4.plot(df.index, df[self.fault_col], label="Fault", color="k")
944
+ ax4.set_xlabel("Date")
945
+ ax4.set_ylabel("Fault Flags")
946
+ ax4.legend(loc="best")
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
+
969
+ plt.tight_layout(rect=[0, 0.03, 1, 0.95])
970
+ plt.show()
971
+ plt.close()
972
+
973
+ def summarize_fault_times(self, df: pd.DataFrame) -> dict:
974
+ delta = df.index.to_series().diff()
975
+ summary = {
976
+ "total_days": round(delta.sum() / pd.Timedelta(days=1), 2),
977
+ "total_hours": round(delta.sum() / pd.Timedelta(hours=1)),
978
+ "hours_fc16_mode": round(
979
+ (delta * df[self.fault_col]).sum() / pd.Timedelta(hours=1)
980
+ ),
981
+ "percent_true": round(df[self.fault_col].mean() * 100, 2),
982
+ "percent_false": round((100 - df[self.fault_col].mean() * 100), 2),
983
+ "flag_true_erv_oat_enter_temp": round(
984
+ df[self.erv_oat_enter_col].where(df[self.fault_col] == 1).mean(), 2
985
+ ),
986
+ "flag_true_erv_oat_leave_temp": round(
987
+ df[self.erv_oat_leaving_col].where(df[self.fault_col] == 1).mean(), 2
988
+ ),
989
+ "flag_true_erv_eat_enter_temp": round(
990
+ df[self.erv_eat_enter_col].where(df[self.fault_col] == 1).mean(), 2
991
+ ),
992
+ "flag_true_erv_eat_leave_temp": round(
993
+ df[self.erv_eat_leaving_col].where(df[self.fault_col] == 1).mean(), 2
994
+ ),
995
+ "hours_motor_runtime": round(
996
+ (delta * df[self.supply_vfd_speed_col].gt(0.01).astype(int)).sum()
997
+ / pd.Timedelta(hours=1),
998
+ 2,
999
+ ),
1000
+ }
1001
+ return summary
@@ -35,6 +35,7 @@ class BaseFaultReport:
35
35
  sys.stdout.flush()
36
36
 
37
37
  if df[self.fault_col].max() != 0:
38
+ self.create_plot(df)
38
39
  self.create_hist_plot(df)
39
40
  else:
40
41
  print("NO FAULTS FOUND - Skipping time-of-day Histogram plot")
@@ -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
@@ -0,0 +1,205 @@
1
+ import pandas as pd
2
+ import pytest
3
+ from open_fdd.air_handling_unit.faults import FaultConditionSixteen
4
+ from open_fdd.air_handling_unit.faults.fault_condition import (
5
+ InvalidParameterError,
6
+ MissingColumnError,
7
+ )
8
+
9
+ """
10
+ To see print statements in pytest run with:
11
+ $ py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc16.py -rP -s
12
+
13
+ ERV effectiveness should be within specified thresholds based on OAT.
14
+ """
15
+
16
+ # Constants
17
+ TEST_ERV_EFFICIENCY_MIN_HEATING = 0.65
18
+ TEST_ERV_EFFICIENCY_MAX_HEATING = 0.8
19
+ TEST_ERV_EFFICIENCY_MIN_COOLING = 0.45
20
+ TEST_ERV_EFFICIENCY_MAX_COOLING = 0.6
21
+ TEST_OAT_LOW_THRESHOLD = 32.0
22
+ TEST_OAT_HIGH_THRESHOLD = 80.0
23
+ TEST_OAT_RAT_DELTA_THRES = 15.0
24
+ TEST_ERV_OAT_ENTER_COL = "erv_oat_enter"
25
+ TEST_ERV_OAT_LEAVING_COL = "erv_oat_leaving"
26
+ TEST_ERV_EAT_ENTER_COL = "erv_eat_enter"
27
+ TEST_ERV_EAT_LEAVING_COL = "erv_eat_leaving"
28
+ TEST_SUPPLY_VFD_SPEED_COL = "supply_vfd_speed"
29
+ ROLLING_WINDOW_SIZE = 1
30
+
31
+ # Initialize FaultConditionSixteen with a dictionary
32
+ fault_condition_params = {
33
+ "ERV_EFFICIENCY_MIN_HEATING": TEST_ERV_EFFICIENCY_MIN_HEATING,
34
+ "ERV_EFFICIENCY_MAX_HEATING": TEST_ERV_EFFICIENCY_MAX_HEATING,
35
+ "ERV_EFFICIENCY_MIN_COOLING": TEST_ERV_EFFICIENCY_MIN_COOLING,
36
+ "ERV_EFFICIENCY_MAX_COOLING": TEST_ERV_EFFICIENCY_MAX_COOLING,
37
+ "OAT_LOW_THRESHOLD": TEST_OAT_LOW_THRESHOLD,
38
+ "OAT_HIGH_THRESHOLD": TEST_OAT_HIGH_THRESHOLD,
39
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
40
+ "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
41
+ "ERV_OAT_LEAVING_COL": TEST_ERV_OAT_LEAVING_COL,
42
+ "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
43
+ "ERV_EAT_LEAVING_COL": TEST_ERV_EAT_LEAVING_COL,
44
+ "SUPPLY_VFD_SPEED_COL": TEST_SUPPLY_VFD_SPEED_COL,
45
+ "TROUBLESHOOT_MODE": False,
46
+ "ROLLING_WINDOW_SIZE": ROLLING_WINDOW_SIZE,
47
+ }
48
+
49
+ fc16 = FaultConditionSixteen(fault_condition_params)
50
+
51
+
52
+ class TestFaultConditionSixteen:
53
+
54
+ def no_fault_htg_df(self) -> pd.DataFrame:
55
+ data = {
56
+ TEST_ERV_OAT_ENTER_COL: [10, 10, 10, 10, 10, 10],
57
+ TEST_ERV_OAT_LEAVING_COL: [50.0, 50.5, 50.8, 50.6, 50.2, 50.4],
58
+ TEST_ERV_EAT_ENTER_COL: [70, 70, 70, 70, 70, 70],
59
+ TEST_ERV_EAT_LEAVING_COL: [60, 60.5, 60.2, 60.4, 60.1, 60.3],
60
+ TEST_SUPPLY_VFD_SPEED_COL: [0.5, 0.6, 0.5, 0.7, 0.5, 0.6],
61
+ }
62
+ return pd.DataFrame(data)
63
+
64
+ def fault_htg_df_low_eff(self) -> pd.DataFrame:
65
+ data = {
66
+ TEST_ERV_OAT_ENTER_COL: [10, 10, 10, 10, 10, 10],
67
+ TEST_ERV_OAT_LEAVING_COL: [20.0, 20.5, 20.8, 20.6, 20.2, 20.4],
68
+ TEST_ERV_EAT_ENTER_COL: [70, 70, 70, 70, 70, 70],
69
+ TEST_ERV_EAT_LEAVING_COL: [60, 60.5, 60.2, 60.4, 60.1, 60.3],
70
+ TEST_SUPPLY_VFD_SPEED_COL: [0.5, 0.6, 0.5, 0.7, 0.5, 0.6],
71
+ }
72
+ return pd.DataFrame(data)
73
+
74
+ def fault_htg_df_high_eff(self) -> pd.DataFrame:
75
+ data = {
76
+ TEST_ERV_OAT_ENTER_COL: [10, 10, 10, 10, 10, 10],
77
+ TEST_ERV_OAT_LEAVING_COL: [90.0, 90.5, 90.8, 90.6, 90.2, 90.4],
78
+ TEST_ERV_EAT_ENTER_COL: [70, 70, 70, 70, 70, 70],
79
+ TEST_ERV_EAT_LEAVING_COL: [60, 60.5, 60.2, 60.4, 60.1, 60.3],
80
+ TEST_SUPPLY_VFD_SPEED_COL: [0.5, 0.6, 0.5, 0.7, 0.5, 0.6],
81
+ }
82
+ return pd.DataFrame(data)
83
+
84
+ def test_no_fault_htg(self):
85
+ results = fc16.apply(self.no_fault_htg_df())
86
+ actual = results["fc16_flag"].sum()
87
+ expected = 0
88
+ message = f"FC16 no_fault_htg actual is {actual} and expected is {expected}"
89
+ assert actual == expected, message
90
+
91
+ def test_fault_htg_low_eff(self):
92
+ results = fc16.apply(self.fault_htg_df_low_eff())
93
+ actual = results["fc16_flag"].sum()
94
+ expected = 6
95
+ message = (
96
+ f"FC16 fault_htg_low_eff actual is {actual} and expected is {expected}"
97
+ )
98
+ assert actual == expected, message
99
+
100
+ def test_fault_htg_high_eff(self):
101
+ results = fc16.apply(self.fault_htg_df_high_eff())
102
+ actual = results["fc16_flag"].sum()
103
+ expected = 6
104
+ message = (
105
+ f"FC16 fault_htg_high_eff actual is {actual} and expected is {expected}"
106
+ )
107
+ assert actual == expected, message
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
+
125
+
126
+ class TestFaultOnInvalidParams:
127
+
128
+ def test_invalid_param_type(self):
129
+ """Test that InvalidParameterError is raised for non-float parameters."""
130
+ with pytest.raises(InvalidParameterError) as excinfo:
131
+ FaultConditionSixteen(
132
+ {
133
+ "ERV_EFFICIENCY_MIN_HEATING": "0.65", # Invalid, should be float
134
+ "ERV_EFFICIENCY_MAX_HEATING": 0.8,
135
+ "ERV_EFFICIENCY_MIN_COOLING": 0.45,
136
+ "ERV_EFFICIENCY_MAX_COOLING": 0.6,
137
+ "OAT_LOW_THRESHOLD": 32.0,
138
+ "OAT_HIGH_THRESHOLD": 80.0,
139
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
140
+ "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
141
+ "ERV_OAT_LEAVING_COL": TEST_ERV_OAT_LEAVING_COL,
142
+ "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
143
+ "ERV_EAT_LEAVING_COL": TEST_ERV_EAT_LEAVING_COL,
144
+ "SUPPLY_VFD_SPEED_COL": TEST_SUPPLY_VFD_SPEED_COL,
145
+ }
146
+ )
147
+ assert "should be a float" in str(excinfo.value)
148
+
149
+ def test_invalid_efficiency_value(self):
150
+ """Test that InvalidParameterError is raised if efficiency values are out of 0.0 - 1.0 range."""
151
+ with pytest.raises(InvalidParameterError) as excinfo:
152
+ FaultConditionSixteen(
153
+ {
154
+ "ERV_EFFICIENCY_MIN_HEATING": 75.0, # Invalid, should be between 0.0 and 1.0
155
+ "ERV_EFFICIENCY_MAX_HEATING": 0.8,
156
+ "ERV_EFFICIENCY_MIN_COOLING": 0.45,
157
+ "ERV_EFFICIENCY_MAX_COOLING": 0.6,
158
+ "OAT_LOW_THRESHOLD": 32.0,
159
+ "OAT_HIGH_THRESHOLD": 80.0,
160
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
161
+ "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
162
+ "ERV_OAT_LEAVING_COL": TEST_ERV_OAT_LEAVING_COL,
163
+ "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
164
+ "ERV_EAT_LEAVING_COL": TEST_ERV_EAT_LEAVING_COL,
165
+ "SUPPLY_VFD_SPEED_COL": TEST_SUPPLY_VFD_SPEED_COL,
166
+ }
167
+ )
168
+ assert "should be a float between 0.0 and 1.0" in str(excinfo.value)
169
+
170
+
171
+ class TestFaultOnMissingColumns:
172
+
173
+ def test_missing_column(self):
174
+ """Test that MissingColumnError is raised if any required column is None or missing."""
175
+ with pytest.raises(MissingColumnError) as excinfo:
176
+ FaultConditionSixteen(
177
+ {
178
+ "ERV_EFFICIENCY_MIN_HEATING": 0.65,
179
+ "ERV_EFFICIENCY_MAX_HEATING": 0.8,
180
+ "ERV_EFFICIENCY_MIN_COOLING": 0.45,
181
+ "ERV_EFFICIENCY_MAX_COOLING": 0.6,
182
+ "OAT_LOW_THRESHOLD": 32.0,
183
+ "OAT_HIGH_THRESHOLD": 80.0,
184
+ "OAT_RAT_DELTA_MIN": TEST_OAT_RAT_DELTA_THRES,
185
+ "ERV_OAT_ENTER_COL": TEST_ERV_OAT_ENTER_COL,
186
+ "ERV_OAT_LEAVING_COL": None, # Missing column
187
+ "ERV_EAT_ENTER_COL": TEST_ERV_EAT_ENTER_COL,
188
+ "ERV_EAT_LEAVING_COL": TEST_ERV_EAT_LEAVING_COL,
189
+ "SUPPLY_VFD_SPEED_COL": TEST_SUPPLY_VFD_SPEED_COL,
190
+ }
191
+ ).apply(
192
+ pd.DataFrame(
193
+ {
194
+ TEST_ERV_OAT_ENTER_COL: [10, 10, 10],
195
+ TEST_ERV_EAT_ENTER_COL: [70, 70, 70],
196
+ TEST_ERV_EAT_LEAVING_COL: [60, 60, 60],
197
+ TEST_SUPPLY_VFD_SPEED_COL: [0.5, 0.5, 0.5],
198
+ }
199
+ )
200
+ )
201
+ assert "One or more required columns are missing or None" in str(excinfo.value)
202
+
203
+
204
+ if __name__ == "__main__":
205
+ pytest.main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: open_fdd
3
- Version: 0.1.5
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
@@ -47,10 +47,10 @@ The following are key objectives to enhance this project into a fully interactiv
47
47
 
48
48
  ### In Progress
49
49
  - [ ] Create IPython notebook tutorials showcasing AHU FDD examples, incorporating BRICK metadata integration.
50
- - [ ] Develop a comprehensive guide on a github.io website (or other?) for defining fault parameters, including error thresholds and other critical settings.
50
+ - [ ] Extend the project to include `central_plant` fault conditions, IPython reports, and example applications for boiler and chiller systems.
51
+
51
52
 
52
53
  ### Upcoming
53
- - [ ] Extend the project to include `central_plant` fault conditions, IPython reports, and example applications.
54
54
  - [ ] Design `energy_efficiency` fault detection modules, including IPython reports and examples focused on optimizing energy consumption.
55
55
  - [ ] Develop `metering` fault conditions, along with IPython reports and examples, potentially modeling utility metering data.
56
56
  - [ ] Implement SQL integration examples for reading data from a time series database, writing back to SQL, and visualizing faults in Grafana.
@@ -58,6 +58,7 @@ The following are key objectives to enhance this project into a fully interactiv
58
58
  ### Future Considerations
59
59
  Explore additional features and enhancements as the project evolves.
60
60
  - [ ] Explore additional features and enhancements as the project evolves.
61
+ - [ ] Develop a comprehensive guide on a github.io website (or other?) for defining fault parameters, including error thresholds and other critical settings.
61
62
 
62
63
 
63
64
  ## Contribute
@@ -0,0 +1,31 @@
1
+ open_fdd/__init__.py,sha256=iGj8QTOZJUTE4nNnBiCHXEXsOdV6YvKcGiLrnOusJCg,1411
2
+ open_fdd/air_handling_unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ open_fdd/air_handling_unit/faults/__init__.py,sha256=Gc2VjfRDEX0-JcIrPuV692T3Ek3GGlTgrscqUXslrwY,93217
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=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
+ open_fdd/air_handling_unit/reports/fault_report.py,sha256=QxYLJzoLTwf1N0nls2XMmhHJvBSgGCBNT0KxA59QG4Y,1442
9
+ open_fdd/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ open_fdd/tests/ahu/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ open_fdd/tests/ahu/test_ahu_fc1.py,sha256=Pw5XlQ19c5Sit0Lyuor8L8lKgxl5PFn4UhruIHyKDfc,5439
12
+ open_fdd/tests/ahu/test_ahu_fc10.py,sha256=niYL7fi6OlgP0wnF8hNh9A07PLzHiRZkyRkrA1zoL2s,4577
13
+ open_fdd/tests/ahu/test_ahu_fc11.py,sha256=mdXlGiEMPkPfshf3NN_nJavL74e4HCmkJQMu86aZc6Q,4723
14
+ open_fdd/tests/ahu/test_ahu_fc12.py,sha256=5T-XcM6xm9KHrc121uPGC9JWLCYehrAYk0KcbmGgYjw,5848
15
+ open_fdd/tests/ahu/test_ahu_fc13.py,sha256=vJlSy4e2WV9hx02P0SiJ75I1DWL2lZ0p7-AWlw97pks,5725
16
+ open_fdd/tests/ahu/test_ahu_fc14.py,sha256=MU0LKqIuoQ_dJ0Kij8_A0YyimCMvUwL6IlMwpQhDbqI,8052
17
+ open_fdd/tests/ahu/test_ahu_fc15.py,sha256=SIolJ9vnJwnpKfRW2ALugYWySuIiZ9_repWP9fdb5H4,7661
18
+ open_fdd/tests/ahu/test_ahu_fc16.py,sha256=f656EZCWEKOXE2GZHUayaLc8bjMZWX_QB99QDBJxF4Y,9020
19
+ open_fdd/tests/ahu/test_ahu_fc2.py,sha256=CjmO_WUyaSHs17ifCCew3GBJ43nYG55uGL0vHDZpAq8,4736
20
+ open_fdd/tests/ahu/test_ahu_fc3.py,sha256=NB6pOXDS-R4P0LNoRN8ItAqhhLnGnGuAHZha32Qw-hE,4658
21
+ open_fdd/tests/ahu/test_ahu_fc4.py,sha256=vV8jEnFuNGLfhCoTVz29RsIcoDpDOMWg722G0aBEXaE,6304
22
+ open_fdd/tests/ahu/test_ahu_fc5.py,sha256=TpSQTBIF591Q3mVaBeJ6HyqPWhVHmD_gSZNEIFT6yyg,6538
23
+ open_fdd/tests/ahu/test_ahu_fc6.py,sha256=66dwv0EBU_ujZK-J9Ki5a3fnXlk17nOwmtKDiQOHdbM,10351
24
+ open_fdd/tests/ahu/test_ahu_fc7.py,sha256=sABbw2m7WlAXbsqfDD323vfEfg606ThI0QzQyB-OjFo,2469
25
+ open_fdd/tests/ahu/test_ahu_fc8.py,sha256=UZy6BP2PgV1FROUPqMORTx8YnT5ZvqVDhut_Ar81494,4663
26
+ open_fdd/tests/ahu/test_ahu_fc9.py,sha256=b-eIzhNzjZUjVNsP0JAHkOgZu-BtDuPeNnblVVm-jU8,4796
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 (72.1.0)
2
+ Generator: setuptools (74.1.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,127 +0,0 @@
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
- )
7
- import sys
8
-
9
-
10
- class FaultConditionEight(FaultCondition):
11
- """Class provides the definitions for Fault Condition 8.
12
- Supply air temperature and mix air temperature should
13
- be approx equal in economizer mode.
14
- """
15
-
16
- def __init__(self, dict_):
17
- super().__init__()
18
- self.delta_t_supply_fan = float
19
- self.mix_degf_err_thres = float
20
- self.supply_degf_err_thres = float
21
- self.ahu_min_oa_dpr = float
22
- self.mat_col = str
23
- self.sat_col = str
24
- self.economizer_sig_col = str
25
- self.cooling_sig_col = str
26
- self.troubleshoot_mode = bool # default should be False
27
- self.rolling_window_size = int
28
-
29
- self.equation_string = (
30
- "fc8_flag = 1 if |SAT - MAT - ΔT_fan| > √(εSAT² + εMAT²) "
31
- "in economizer mode for N consecutive values else 0 \n"
32
- )
33
- self.description_string = (
34
- "Fault Condition 8: Supply air temperature and mixed air temperature should "
35
- "be approximately equal in economizer mode \n"
36
- )
37
- self.required_column_description = (
38
- "Required inputs are the mixed air temperature, supply air temperature, "
39
- "economizer signal, and cooling signal \n"
40
- )
41
- self.error_string = f"One or more required columns are missing or None \n"
42
-
43
- self.set_attributes(dict_)
44
-
45
- # Set required columns specific to this fault condition
46
- self.required_columns = [
47
- self.mat_col,
48
- self.sat_col,
49
- self.economizer_sig_col,
50
- self.cooling_sig_col,
51
- ]
52
-
53
- # Check if any of the required columns are None
54
- if any(col is None for col in self.required_columns):
55
- raise MissingColumnError(
56
- f"{self.error_string}"
57
- f"{self.equation_string}"
58
- f"{self.description_string}"
59
- f"{self.required_column_description}"
60
- f"{self.required_columns}"
61
- )
62
-
63
- # Ensure all required columns are strings
64
- self.required_columns = [str(col) for col in self.required_columns]
65
-
66
- self.mapped_columns = (
67
- f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
68
- )
69
-
70
- def get_required_columns(self) -> str:
71
- """Returns a string representation of the required columns."""
72
- return (
73
- f"{self.equation_string}"
74
- f"{self.description_string}"
75
- f"{self.required_column_description}"
76
- f"{self.mapped_columns}"
77
- )
78
-
79
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
80
- try:
81
- # Ensure all required columns are present
82
- self.check_required_columns(df)
83
-
84
- if self.troubleshoot_mode:
85
- self.troubleshoot_cols(df)
86
-
87
- # Check analog outputs [data with units of %] are floats only
88
- columns_to_check = [
89
- self.economizer_sig_col,
90
- self.cooling_sig_col,
91
- ]
92
-
93
- self.check_analog_pct(df, columns_to_check)
94
-
95
- df["sat_fan_mat"] = abs(
96
- df[self.sat_col] - self.delta_t_supply_fan - df[self.mat_col]
97
- )
98
- df["sat_mat_sqrted"] = np.sqrt(
99
- self.supply_degf_err_thres**2 + self.mix_degf_err_thres**2
100
- )
101
-
102
- df["combined_check"] = (
103
- (df["sat_fan_mat"] > df["sat_mat_sqrted"])
104
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
105
- & (df[self.cooling_sig_col] < 0.1)
106
- )
107
-
108
- # Rolling sum to count consecutive trues
109
- rolling_sum = (
110
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
111
- )
112
- # Set flag to 1 if rolling sum equals the window size
113
- df["fc8_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
114
-
115
- if self.troubleshoot_mode:
116
- print("Troubleshoot mode enabled - not removing helper columns")
117
- sys.stdout.flush()
118
- del df["sat_fan_mat"]
119
- del df["sat_mat_sqrted"]
120
- del df["combined_check"]
121
-
122
- return df
123
-
124
- except MissingColumnError as e:
125
- print(f"Error: {e.message}")
126
- sys.stdout.flush()
127
- raise e