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