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