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