open-fdd 0.1.1__py3-none-any.whl → 0.1.4__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 (63) hide show
  1. open_fdd/air_handling_unit/faults/__init__.py +2253 -0
  2. open_fdd/air_handling_unit/faults/fault_condition.py +38 -18
  3. open_fdd/air_handling_unit/faults/fault_condition_eight.py +91 -31
  4. open_fdd/air_handling_unit/faults/fault_condition_eleven.py +93 -35
  5. open_fdd/air_handling_unit/faults/fault_condition_fifteen.py +111 -49
  6. open_fdd/air_handling_unit/faults/fault_condition_five.py +89 -34
  7. open_fdd/air_handling_unit/faults/fault_condition_four.py +136 -61
  8. open_fdd/air_handling_unit/faults/fault_condition_fourteen.py +103 -40
  9. open_fdd/air_handling_unit/faults/fault_condition_nine.py +95 -35
  10. open_fdd/air_handling_unit/faults/fault_condition_one.py +83 -31
  11. open_fdd/air_handling_unit/faults/fault_condition_seven.py +85 -26
  12. open_fdd/air_handling_unit/faults/fault_condition_six.py +134 -73
  13. open_fdd/air_handling_unit/faults/fault_condition_ten.py +91 -30
  14. open_fdd/air_handling_unit/faults/fault_condition_thirteen.py +95 -34
  15. open_fdd/air_handling_unit/faults/fault_condition_three.py +84 -29
  16. open_fdd/air_handling_unit/faults/fault_condition_twelve.py +98 -37
  17. open_fdd/air_handling_unit/faults/fault_condition_two.py +84 -32
  18. open_fdd/air_handling_unit/faults/helper_utils.py +295 -93
  19. open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_1.jpg +0 -0
  20. open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_2.jpg +0 -0
  21. open_fdd/air_handling_unit/images/example1.jpg +0 -0
  22. open_fdd/air_handling_unit/images/example2.jpg +0 -0
  23. open_fdd/air_handling_unit/images/fc10_definition.png +0 -0
  24. open_fdd/air_handling_unit/images/fc11_definition.png +0 -0
  25. open_fdd/air_handling_unit/images/fc12_definition.png +0 -0
  26. open_fdd/air_handling_unit/images/fc13_definition.png +0 -0
  27. open_fdd/air_handling_unit/images/fc1_definition.png +0 -0
  28. open_fdd/air_handling_unit/images/fc1_report_screenshot_all.png +0 -0
  29. open_fdd/air_handling_unit/images/fc2_definition.png +0 -0
  30. open_fdd/air_handling_unit/images/fc3_definition.png +0 -0
  31. open_fdd/air_handling_unit/images/fc4_definition.png +0 -0
  32. open_fdd/air_handling_unit/images/fc5_definition.png +0 -0
  33. open_fdd/air_handling_unit/images/fc6_definition.png +0 -0
  34. open_fdd/air_handling_unit/images/fc7_definition.png +0 -0
  35. open_fdd/air_handling_unit/images/fc8_definition.png +0 -0
  36. open_fdd/air_handling_unit/images/fc9_definition.png +0 -0
  37. open_fdd/air_handling_unit/images/latex_generator.py +175 -0
  38. open_fdd/air_handling_unit/images/params.docx +0 -0
  39. open_fdd/air_handling_unit/images/params.pdf +0 -0
  40. open_fdd/air_handling_unit/images/plot_for_repo.png +0 -0
  41. open_fdd/air_handling_unit/reports/base_report.py +47 -0
  42. open_fdd/air_handling_unit/reports/report_fc7.py +3 -1
  43. open_fdd/tests/ahu/test_ahu_fc1.py +18 -1
  44. open_fdd/tests/ahu/test_ahu_fc10.py +1 -1
  45. open_fdd/tests/ahu/test_ahu_fc11.py +1 -1
  46. open_fdd/tests/ahu/test_ahu_fc12.py +1 -1
  47. open_fdd/tests/ahu/test_ahu_fc13.py +1 -1
  48. open_fdd/tests/ahu/test_ahu_fc14.py +1 -1
  49. open_fdd/tests/ahu/test_ahu_fc15.py +1 -1
  50. open_fdd/tests/ahu/test_ahu_fc2.py +1 -1
  51. open_fdd/tests/ahu/test_ahu_fc3.py +1 -1
  52. open_fdd/tests/ahu/test_ahu_fc4.py +2 -2
  53. open_fdd/tests/ahu/test_ahu_fc5.py +1 -1
  54. open_fdd/tests/ahu/test_ahu_fc6.py +2 -2
  55. open_fdd/tests/ahu/test_ahu_fc7.py +1 -1
  56. open_fdd/tests/ahu/test_ahu_fc8.py +1 -1
  57. open_fdd/tests/ahu/test_ahu_fc9.py +1 -1
  58. {open_fdd-0.1.1.dist-info → open_fdd-0.1.4.dist-info}/METADATA +34 -5
  59. open_fdd-0.1.4.dist-info/RECORD +82 -0
  60. open_fdd-0.1.1.dist-info/RECORD +0 -59
  61. {open_fdd-0.1.1.dist-info → open_fdd-0.1.4.dist-info}/LICENSE +0 -0
  62. {open_fdd-0.1.1.dist-info → open_fdd-0.1.4.dist-info}/WHEEL +0 -0
  63. {open_fdd-0.1.1.dist-info → open_fdd-0.1.4.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,9 @@
1
1
  import pandas as pd
2
2
  import pandas.api.types as pdtypes
3
- from open_fdd.air_handling_unit.faults.fault_condition import FaultCondition
4
- from open_fdd.air_handling_unit.faults.helper_utils import HelperUtils
3
+ from open_fdd.air_handling_unit.faults.fault_condition import (
4
+ FaultCondition,
5
+ MissingColumnError,
6
+ )
5
7
  import sys
6
8
 
7
9
 
@@ -13,6 +15,7 @@ class FaultConditionFive(FaultCondition):
13
15
  """
14
16
 
15
17
  def __init__(self, dict_):
18
+ super().__init__()
16
19
  self.mix_degf_err_thres = float
17
20
  self.supply_degf_err_thres = float
18
21
  self.delta_t_supply_fan = float
@@ -23,46 +26,98 @@ class FaultConditionFive(FaultCondition):
23
26
  self.troubleshoot_mode = bool # default to False
24
27
  self.rolling_window_size = int
25
28
 
29
+ self.equation_string = (
30
+ "fc5_flag = 1 if (SAT + εSAT <= MAT - εMAT + ΔT_supply_fan) and "
31
+ "(heating signal > 0) and (VFDSPD > 0) for N consecutive values else 0 \n"
32
+ )
33
+ self.description_string = (
34
+ "Fault Condition 5: SAT too low; should be higher than MAT in HTG MODE, "
35
+ "potential broken heating valve or mechanical issue \n"
36
+ )
37
+ self.required_column_description = (
38
+ "Required inputs are the mixed air temperature, supply air temperature, "
39
+ "heating signal, and supply fan VFD speed \n"
40
+ )
41
+ self.error_string = f"One or more required columns are missing or None \n"
42
+
26
43
  self.set_attributes(dict_)
27
44
 
28
- # fault only active if fan is running and htg vlv is modulating
29
- # OS 1 is heating mode only fault
30
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
31
- if self.troubleshoot_mode:
32
- self.troubleshoot_cols(df)
45
+ # Set required columns specific to this fault condition
46
+ self.required_columns = [
47
+ self.mat_col,
48
+ self.sat_col,
49
+ self.heating_sig_col,
50
+ self.supply_vfd_speed_col,
51
+ ]
33
52
 
34
- # check analog outputs [data with units of %] are floats only
35
- columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
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
+ )
36
62
 
37
- for col in columns_to_check:
38
- self.check_analog_pct(df, [col])
63
+ # Ensure all required columns are strings
64
+ self.required_columns = [str(col) for col in self.required_columns]
39
65
 
40
- df["sat_check"] = df[self.sat_col] + self.supply_degf_err_thres
41
- df["mat_check"] = (
42
- df[self.mat_col] - self.mix_degf_err_thres + self.delta_t_supply_fan
66
+ self.mapped_columns = (
67
+ f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
43
68
  )
44
69
 
45
- df["combined_check"] = (
46
- (df["sat_check"] <= df["mat_check"])
47
- # this is to make fault only active in OS1 for htg mode only
48
- # and fan is running. Some control programming may use htg
49
- # vlv when AHU is off to prevent low limit freeze alarms
50
- & (df[self.heating_sig_col] > 0.01)
51
- & (df[self.supply_vfd_speed_col] > 0.01)
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}"
52
77
  )
53
78
 
54
- # Rolling sum to count consecutive trues
55
- rolling_sum = (
56
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
57
- )
58
- # Set flag to 1 if rolling sum equals the window size
59
- df["fc5_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
79
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
80
+ try:
81
+ # Ensure all required columns are present
82
+ self.check_required_columns(df)
60
83
 
61
- if self.troubleshoot_mode:
62
- print("Troubleshoot mode enabled - not removing helper columns")
63
- sys.stdout.flush()
64
- del df["mat_check"]
65
- del df["sat_check"]
66
- del df["combined_check"]
84
+ if self.troubleshoot_mode:
85
+ self.troubleshoot_cols(df)
67
86
 
68
- return df
87
+ # Check analog outputs [data with units of %] are floats only
88
+ columns_to_check = [self.supply_vfd_speed_col, self.heating_sig_col]
89
+
90
+ for col in columns_to_check:
91
+ self.check_analog_pct(df, [col])
92
+
93
+ df["sat_check"] = df[self.sat_col] + self.supply_degf_err_thres
94
+ df["mat_check"] = (
95
+ df[self.mat_col] - self.mix_degf_err_thres + self.delta_t_supply_fan
96
+ )
97
+
98
+ df["combined_check"] = (
99
+ (df["sat_check"] <= df["mat_check"])
100
+ & (df[self.heating_sig_col] > 0.01)
101
+ & (df[self.supply_vfd_speed_col] > 0.01)
102
+ )
103
+
104
+ # Rolling sum to count consecutive trues
105
+ rolling_sum = (
106
+ df["combined_check"].rolling(window=self.rolling_window_size).sum()
107
+ )
108
+ # Set flag to 1 if rolling sum equals the window size
109
+ df["fc5_flag"] = (rolling_sum == self.rolling_window_size).astype(int)
110
+
111
+ if self.troubleshoot_mode:
112
+ print("Troubleshoot mode enabled - not removing helper columns")
113
+ sys.stdout.flush()
114
+ del df["mat_check"]
115
+ del df["sat_check"]
116
+ del df["combined_check"]
117
+
118
+ return df
119
+
120
+ except MissingColumnError as e:
121
+ print(f"Error: {e.message}")
122
+ sys.stdout.flush()
123
+ raise e
@@ -1,6 +1,8 @@
1
1
  import pandas as pd
2
- from open_fdd.air_handling_unit.faults.fault_condition import FaultCondition
3
- from open_fdd.air_handling_unit.faults.helper_utils import HelperUtils
2
+ from open_fdd.air_handling_unit.faults.fault_condition import (
3
+ FaultCondition,
4
+ MissingColumnError,
5
+ )
4
6
  import sys
5
7
 
6
8
 
@@ -8,14 +10,14 @@ class FaultConditionFour(FaultCondition):
8
10
  """Class provides the definitions for Fault Condition 4.
9
11
 
10
12
  This fault flags excessive operating states on the AHU
11
- if its hunting between heating, econ, econ+mech, and
13
+ if it's hunting between heating, econ, econ+mech, and
12
14
  a mech clg modes. The code counts how many operating
13
15
  changes in an hour and will throw a fault if there is
14
16
  excessive OS changes to flag control sys hunting.
15
-
16
17
  """
17
18
 
18
19
  def __init__(self, dict_):
20
+ super().__init__()
19
21
  self.delta_os_max = float
20
22
  self.ahu_min_oa_dpr = float
21
23
  self.economizer_sig_col = str
@@ -24,70 +26,143 @@ class FaultConditionFour(FaultCondition):
24
26
  self.supply_vfd_speed_col = str
25
27
  self.troubleshoot_mode = bool # default to False
26
28
 
27
- self.set_attributes(dict_)
29
+ self.equation_string = (
30
+ "fc4_flag = 1 if excessive mode changes (> δOS_max) occur "
31
+ "within an hour across heating, econ, econ+mech, mech clg, and min OA modes \n"
32
+ )
33
+ self.description_string = "Fault Condition 4: Excessive AHU operating state changes detected (hunting behavior) \n"
34
+ self.required_column_description = (
35
+ "Required inputs are the economizer signal, supply fan VFD speed, "
36
+ "and optionally heating and cooling signals \n"
37
+ )
38
+ self.error_string = f"One or more required columns are missing or None \n"
28
39
 
29
- # adds in these boolean columns to the dataframe
30
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
31
- if self.troubleshoot_mode:
32
- self.troubleshoot_cols(df)
40
+ self.set_attributes(dict_)
33
41
 
34
- # check analog outputs [data with units of %] are floats only
35
- columns_to_check = [
42
+ # Set required columns, making heating and cooling optional
43
+ self.required_columns = [
36
44
  self.economizer_sig_col,
37
- self.heating_sig_col,
38
- self.cooling_sig_col,
39
45
  self.supply_vfd_speed_col,
40
46
  ]
41
47
 
42
- for col in columns_to_check:
43
- self.check_analog_pct(df, [col])
44
-
45
- print("=" * 50)
46
- print("Warning: The program is in FC4 and resampling the data")
47
- print("to compute AHU OS state changes per hour")
48
- print("to flag any hunting issue")
49
- print("and this usually takes a while to run...")
50
- print("=" * 50)
51
-
52
- sys.stdout.flush()
53
-
54
- # AHU htg only mode based on OA damper @ min oa and only htg pid/vlv modulating
55
- df["heating_mode"] = (
56
- (df[self.heating_sig_col] > 0)
57
- & (df[self.cooling_sig_col] == 0)
58
- & (df[self.supply_vfd_speed_col] > 0)
59
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
60
- )
61
-
62
- # AHU econ only mode based on OA damper modulating and clg htg = zero
63
- df["econ_only_cooling_mode"] = (
64
- (df[self.heating_sig_col] == 0)
65
- & (df[self.cooling_sig_col] == 0)
66
- & (df[self.supply_vfd_speed_col] > 0)
67
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
68
- )
69
-
70
- # AHU econ+mech clg mode based on OA damper modulating for cooling and clg pid/vlv modulating
71
- df["econ_plus_mech_cooling_mode"] = (
72
- (df[self.heating_sig_col] == 0)
73
- & (df[self.cooling_sig_col] > 0)
74
- & (df[self.supply_vfd_speed_col] > 0)
75
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
48
+ # If heating or cooling columns are provided, add them to the required columns
49
+ if self.heating_sig_col:
50
+ self.required_columns.append(self.heating_sig_col)
51
+ if self.cooling_sig_col:
52
+ self.required_columns.append(self.cooling_sig_col)
53
+
54
+ # Check if any of the required columns are None
55
+ if any(col is None for col in self.required_columns):
56
+ raise MissingColumnError(
57
+ f"{self.error_string}"
58
+ f"{self.equation_string}"
59
+ f"{self.description_string}"
60
+ f"{self.required_column_description}"
61
+ f"{self.required_columns}"
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)}"
76
68
  )
77
69
 
78
- # AHU mech mode based on OA damper @ min OA and clg pid/vlv modulating
79
- df["mech_cooling_only_mode"] = (
80
- (df[self.heating_sig_col] == 0)
81
- & (df[self.cooling_sig_col] > 0)
82
- & (df[self.supply_vfd_speed_col] > 0)
83
- & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
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}"
84
77
  )
85
78
 
86
- # Fill non-finite values with zero or drop them
87
- df = df.fillna(0)
88
-
89
- df = df.astype(int)
90
- df = df.resample("60min").apply(lambda x: (x.eq(1) & x.shift().ne(1)).sum())
91
-
92
- df["fc4_flag"] = df[df.columns].gt(self.delta_os_max).any(axis=1).astype(int)
93
- return df
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 the optional columns are not present, create them with all values set to 0.0
85
+ if self.heating_sig_col not in df.columns:
86
+ df[self.heating_sig_col] = 0.0
87
+ if self.cooling_sig_col not in df.columns:
88
+ df[self.cooling_sig_col] = 0.0
89
+
90
+ if self.troubleshoot_mode:
91
+ self.troubleshoot_cols(df)
92
+
93
+ # Check analog outputs [data with units of %] are floats only
94
+ columns_to_check = [
95
+ self.economizer_sig_col,
96
+ self.heating_sig_col,
97
+ self.cooling_sig_col,
98
+ self.supply_vfd_speed_col,
99
+ ]
100
+
101
+ for col in columns_to_check:
102
+ self.check_analog_pct(df, [col])
103
+
104
+ print("=" * 50)
105
+ print("Warning: The program is in FC4 and resampling the data")
106
+ print("to compute AHU OS state changes per hour")
107
+ print("to flag any hunting issue")
108
+ print("and this usually takes a while to run...")
109
+ print("=" * 50)
110
+
111
+ sys.stdout.flush()
112
+
113
+ # AHU htg only mode based on OA damper @ min oa and only htg pid/vlv modulating
114
+ df["heating_mode"] = (
115
+ (df[self.heating_sig_col] > 0)
116
+ & (df[self.cooling_sig_col] == 0)
117
+ & (df[self.supply_vfd_speed_col] > 0)
118
+ & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
119
+ )
120
+
121
+ # AHU econ only mode based on OA damper modulating and clg htg = zero
122
+ df["econ_only_cooling_mode"] = (
123
+ (df[self.heating_sig_col] == 0)
124
+ & (df[self.cooling_sig_col] == 0)
125
+ & (df[self.supply_vfd_speed_col] > 0)
126
+ & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
127
+ )
128
+
129
+ # AHU econ+mech clg mode based on OA damper modulating for cooling and clg pid/vlv modulating
130
+ df["econ_plus_mech_cooling_mode"] = (
131
+ (df[self.heating_sig_col] == 0)
132
+ & (df[self.cooling_sig_col] > 0)
133
+ & (df[self.supply_vfd_speed_col] > 0)
134
+ & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
135
+ )
136
+
137
+ # AHU mech mode based on OA damper @ min OA and clg pid/vlv modulating
138
+ df["mech_cooling_only_mode"] = (
139
+ (df[self.heating_sig_col] == 0)
140
+ & (df[self.cooling_sig_col] > 0)
141
+ & (df[self.supply_vfd_speed_col] > 0)
142
+ & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
143
+ )
144
+
145
+ # AHU minimum OA mode without heating or cooling (ventilation mode)
146
+ df["min_oa_mode_only"] = (
147
+ (df[self.heating_sig_col] == 0)
148
+ & (df[self.cooling_sig_col] == 0)
149
+ & (df[self.supply_vfd_speed_col] > 0)
150
+ & (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
151
+ )
152
+
153
+ # Fill non-finite values with zero or drop them
154
+ df = df.fillna(0)
155
+
156
+ df = df.astype(int)
157
+ df = df.resample("60min").apply(lambda x: (x.eq(1) & x.shift().ne(1)).sum())
158
+
159
+ df["fc4_flag"] = (
160
+ df[df.columns].gt(self.delta_os_max).any(axis=1).astype(int)
161
+ )
162
+
163
+ return df
164
+
165
+ except MissingColumnError as e:
166
+ print(f"Error: {e.message}")
167
+ sys.stdout.flush()
168
+ raise e
@@ -1,8 +1,10 @@
1
1
  import pandas as pd
2
2
  import numpy as np
3
- from open_fdd.air_handling_unit.faults.fault_condition import FaultCondition
4
- from open_fdd.air_handling_unit.faults.helper_utils import HelperUtils
5
3
  import operator
4
+ from open_fdd.air_handling_unit.faults.fault_condition import (
5
+ FaultCondition,
6
+ MissingColumnError,
7
+ )
6
8
  import sys
7
9
 
8
10
 
@@ -13,6 +15,7 @@ class FaultConditionFourteen(FaultCondition):
13
15
  """
14
16
 
15
17
  def __init__(self, dict_):
18
+ super().__init__()
16
19
  self.delta_t_supply_fan = float
17
20
  self.coil_temp_enter_err_thres = float
18
21
  self.coil_temp_leav_err_thres = float
@@ -26,55 +29,115 @@ class FaultConditionFourteen(FaultCondition):
26
29
  self.troubleshoot_mode = bool # default to False
27
30
  self.rolling_window_size = int
28
31
 
29
- self.set_attributes(dict_)
32
+ self.equation_string = (
33
+ "fc14_flag = 1 if ΔT_coil >= √(εcoil_enter² + εcoil_leave²) + ΔT_fan "
34
+ "in inactive cooling coil mode for N consecutive values else 0 \n"
35
+ )
36
+ self.description_string = (
37
+ "Fault Condition 14: Temperature drop across inactive cooling coil "
38
+ "detected, requiring coil leaving temperature sensor \n"
39
+ )
40
+ self.required_column_description = (
41
+ "Required inputs are the cooling coil entering temperature, cooling coil leaving temperature, "
42
+ "cooling signal, heating signal, economizer signal, and supply fan VFD speed \n"
43
+ )
44
+ self.error_string = f"One or more required columns are missing or None \n"
30
45
 
31
- def apply(self, df: pd.DataFrame) -> pd.DataFrame:
32
- if self.troubleshoot_mode:
33
- self.troubleshoot_cols(df)
46
+ self.set_attributes(dict_)
34
47
 
35
- # Check analog outputs [data with units of %] are floats only
36
- columns_to_check = [
37
- self.economizer_sig_col,
48
+ # Set required columns specific to this fault condition
49
+ self.required_columns = [
50
+ self.clg_coil_enter_temp_col,
51
+ self.clg_coil_leave_temp_col,
38
52
  self.cooling_sig_col,
39
53
  self.heating_sig_col,
54
+ self.economizer_sig_col,
40
55
  self.supply_vfd_speed_col,
41
56
  ]
42
- self.check_analog_pct(df, columns_to_check)
43
-
44
- # Create helper columns
45
- df["clg_delta_temp"] = (
46
- df[self.clg_coil_enter_temp_col] - df[self.clg_coil_leave_temp_col]
47
- )
48
57
 
49
- df["clg_delta_sqrted"] = (
50
- np.sqrt(
51
- self.coil_temp_enter_err_thres**2 + self.coil_temp_leav_err_thres**2
58
+ # Check if any of the required columns are None
59
+ if any(col is None for col in self.required_columns):
60
+ raise MissingColumnError(
61
+ f"{self.error_string}"
62
+ f"{self.equation_string}"
63
+ f"{self.description_string}"
64
+ f"{self.required_column_description}"
65
+ f"{self.required_columns}"
52
66
  )
53
- + self.delta_t_supply_fan
54
- )
55
67
 
56
- df["combined_check"] = operator.or_(
57
- (df["clg_delta_temp"] >= df["clg_delta_sqrted"])
58
- # verify AHU is in OS2 only free cooling mode
59
- & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
60
- & (df[self.cooling_sig_col] < 0.1), # OR
61
- (df["clg_delta_temp"] >= df["clg_delta_sqrted"])
62
- # verify AHU is running in OS 1 at near full heat
63
- & (df[self.heating_sig_col] > 0.0) & (df[self.supply_vfd_speed_col] > 0.0),
68
+ # Ensure all required columns are strings
69
+ self.required_columns = [str(col) for col in self.required_columns]
70
+
71
+ self.mapped_columns = (
72
+ f"Your config dictionary is mapped as: {', '.join(self.required_columns)}"
64
73
  )
65
74
 
66
- # Rolling sum to count consecutive trues
67
- rolling_sum = (
68
- df["combined_check"].rolling(window=self.rolling_window_size).sum()
75
+ def get_required_columns(self) -> str:
76
+ """Returns a string representation of the required columns."""
77
+ return (
78
+ f"{self.equation_string}"
79
+ f"{self.description_string}"
80
+ f"{self.required_column_description}"
81
+ f"{self.mapped_columns}"
69
82
  )
70
- # Set flag to 1 if rolling sum equals the window size
71
- df["fc14_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
72
83
 
73
- if self.troubleshoot_mode:
74
- print("Troubleshoot mode enabled - not removing helper columns")
75
- sys.stdout.flush()
76
- del df["clg_delta_temp"]
77
- del df["clg_delta_sqrted"]
78
- del df["combined_check"]
84
+ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
85
+ try:
86
+ # Ensure all required columns are present
87
+ self.check_required_columns(df)
79
88
 
80
- return df
89
+ if self.troubleshoot_mode:
90
+ self.troubleshoot_cols(df)
91
+
92
+ # Check analog outputs [data with units of %] are floats only
93
+ columns_to_check = [
94
+ self.economizer_sig_col,
95
+ self.cooling_sig_col,
96
+ self.heating_sig_col,
97
+ self.supply_vfd_speed_col,
98
+ ]
99
+ self.check_analog_pct(df, columns_to_check)
100
+
101
+ # Create helper columns
102
+ df["clg_delta_temp"] = (
103
+ df[self.clg_coil_enter_temp_col] - df[self.clg_coil_leave_temp_col]
104
+ )
105
+
106
+ df["clg_delta_sqrted"] = (
107
+ np.sqrt(
108
+ self.coil_temp_enter_err_thres**2 + self.coil_temp_leav_err_thres**2
109
+ )
110
+ + self.delta_t_supply_fan
111
+ )
112
+
113
+ df["combined_check"] = operator.or_(
114
+ (df["clg_delta_temp"] >= df["clg_delta_sqrted"])
115
+ # verify AHU is in OS2 only free cooling mode
116
+ & (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
117
+ & (df[self.cooling_sig_col] < 0.1), # OR
118
+ (df["clg_delta_temp"] >= df["clg_delta_sqrted"])
119
+ # verify AHU is running in OS 1 at near full heat
120
+ & (df[self.heating_sig_col] > 0.0)
121
+ & (df[self.supply_vfd_speed_col] > 0.0),
122
+ )
123
+
124
+ # Rolling sum to count consecutive trues
125
+ rolling_sum = (
126
+ df["combined_check"].rolling(window=self.rolling_window_size).sum()
127
+ )
128
+ # Set flag to 1 if rolling sum equals the window size
129
+ df["fc14_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
130
+
131
+ if self.troubleshoot_mode:
132
+ print("Troubleshoot mode enabled - not removing helper columns")
133
+ sys.stdout.flush()
134
+ del df["clg_delta_temp"]
135
+ del df["clg_delta_sqrted"]
136
+ del df["combined_check"]
137
+
138
+ return df
139
+
140
+ except MissingColumnError as e:
141
+ print(f"Error: {e.message}")
142
+ sys.stdout.flush()
143
+ raise e