open-fdd 0.1.0__py3-none-any.whl → 0.1.3__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/fault_condition.py +26 -8
- open_fdd/air_handling_unit/faults/fault_condition_eight.py +61 -37
- open_fdd/air_handling_unit/faults/fault_condition_eleven.py +59 -37
- open_fdd/air_handling_unit/faults/fault_condition_fifteen.py +77 -51
- open_fdd/air_handling_unit/faults/fault_condition_five.py +60 -41
- open_fdd/air_handling_unit/faults/fault_condition_four.py +108 -65
- open_fdd/air_handling_unit/faults/fault_condition_fourteen.py +71 -44
- open_fdd/air_handling_unit/faults/fault_condition_nine.py +60 -36
- open_fdd/air_handling_unit/faults/fault_condition_one.py +58 -37
- open_fdd/air_handling_unit/faults/fault_condition_seven.py +55 -32
- open_fdd/air_handling_unit/faults/fault_condition_six.py +100 -76
- open_fdd/air_handling_unit/faults/fault_condition_ten.py +62 -37
- open_fdd/air_handling_unit/faults/fault_condition_thirteen.py +61 -36
- open_fdd/air_handling_unit/faults/fault_condition_three.py +58 -33
- open_fdd/air_handling_unit/faults/fault_condition_twelve.py +63 -39
- open_fdd/air_handling_unit/faults/fault_condition_two.py +58 -36
- open_fdd/air_handling_unit/faults/helper_utils.py +294 -64
- open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_1.jpg +0 -0
- open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_2.jpg +0 -0
- open_fdd/air_handling_unit/images/example1.jpg +0 -0
- open_fdd/air_handling_unit/images/example2.jpg +0 -0
- open_fdd/air_handling_unit/images/fc10_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc11_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc12_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc13_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc1_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc1_report_screenshot_all.png +0 -0
- open_fdd/air_handling_unit/images/fc2_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc3_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc4_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc5_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc6_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc7_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc8_definition.png +0 -0
- open_fdd/air_handling_unit/images/fc9_definition.png +0 -0
- open_fdd/air_handling_unit/images/latex_generator.py +175 -0
- open_fdd/air_handling_unit/images/params.docx +0 -0
- open_fdd/air_handling_unit/images/params.pdf +0 -0
- open_fdd/air_handling_unit/images/plot_for_repo.png +0 -0
- open_fdd/air_handling_unit/reports/base_report.py +47 -0
- open_fdd/air_handling_unit/reports/report_fc7.py +3 -1
- open_fdd/tests/ahu/test_ahu_fc1.py +17 -0
- open_fdd/tests/ahu/test_ahu_fc4.py +127 -199
- open_fdd-0.1.3.dist-info/METADATA +87 -0
- {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/RECORD +48 -25
- open_fdd-0.1.0.dist-info/METADATA +0 -65
- {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/LICENSE +0 -0
- {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/WHEEL +0 -0
- {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/top_level.txt +0 -0
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,175 @@
|
|
1
|
+
import streamlit as st
|
2
|
+
|
3
|
+
|
4
|
+
# On windows 10 python 3.10.6
|
5
|
+
# py -3.10 -m streamlit run .\latex_generator.py
|
6
|
+
|
7
|
+
# display Fault Equation 1
|
8
|
+
st.title("Fault Equation 1")
|
9
|
+
st.caption("Duct static pressure too low with fan at full speed")
|
10
|
+
st.latex(
|
11
|
+
r"""
|
12
|
+
DSP < DPSP - eDSP \quad \text{and} \quad VFDSPD \geq 99\% - eVFDSPD
|
13
|
+
"""
|
14
|
+
)
|
15
|
+
|
16
|
+
# Display legend
|
17
|
+
st.markdown("Legend:")
|
18
|
+
st.markdown("- DSP: Duct Static Pressure")
|
19
|
+
st.markdown("- DPSP: Duct Static Pressure Setpoint")
|
20
|
+
st.markdown("- VFDSPD: VFD Speed Reference in Percent")
|
21
|
+
st.markdown("- eVFDSPD: VFD Speed Reference Error Threshold")
|
22
|
+
|
23
|
+
|
24
|
+
# display Fault Equation 2
|
25
|
+
st.title("Fault Equation 2")
|
26
|
+
st.caption('Mix air temperature too low; should be between outside and return')
|
27
|
+
st.latex(r'''
|
28
|
+
MAT_{avg} + eMAT < \min[(RAT_{avg} - eRAT), - OAT_{avg} - eOAT)]
|
29
|
+
''')
|
30
|
+
|
31
|
+
|
32
|
+
# display Fault Equation 3
|
33
|
+
st.title("Fault Equation 3")
|
34
|
+
st.caption('Mix air temperature too high; should be between outside and return')
|
35
|
+
st.latex(r'''
|
36
|
+
MAT_{avg} - eMAT > \min[(RAT_{avg} + eRAT), - OAT_{avg} + eOAT)]
|
37
|
+
''')
|
38
|
+
|
39
|
+
|
40
|
+
# display Fault Equation 4
|
41
|
+
st.title("Fault Equation 4")
|
42
|
+
st.caption('Too many AHU operating state changes due to PID hunting and/or excessive cycling during low load conditions.')
|
43
|
+
st.latex(r'''
|
44
|
+
\Delta OS > \Delta OS_{max}
|
45
|
+
''')
|
46
|
+
|
47
|
+
|
48
|
+
# display Fault Equation 5
|
49
|
+
st.title("Fault Equation 5")
|
50
|
+
st.caption('Supply air temperature too high')
|
51
|
+
st.latex(r'''
|
52
|
+
SAT_{avg} + eSAT \leq MAT_{avg} - eMAT + \Delta TSF
|
53
|
+
''')
|
54
|
+
|
55
|
+
|
56
|
+
# display Fault Equation 6
|
57
|
+
st.title("Fault Equation 6")
|
58
|
+
st.caption('Temperature and outside air percentage deviation from setpoints')
|
59
|
+
st.latex(r'''
|
60
|
+
|\text{RAT}_{\text{avg}} - \text{OAT}_{\text{avg}}| \geq \Delta T_{\text{min}} \quad \text{and} \quad |\%OA - \%OA_{\text{min}}| > eF
|
61
|
+
''')
|
62
|
+
|
63
|
+
|
64
|
+
# display Fault Equation 7
|
65
|
+
st.title("Fault Equation 7")
|
66
|
+
st.caption('Supply air temperature too low and heating coil status')
|
67
|
+
st.latex(r'''
|
68
|
+
\text{SAT}_{\text{avg}} < \text{SATSP} - eSAT \quad \text{and} \quad \text{HC} \geq 99\%
|
69
|
+
''')
|
70
|
+
|
71
|
+
# display Fault Equation 8
|
72
|
+
st.title("Fault Equation 8")
|
73
|
+
st.caption('Deviation between supply air temperature and mixed air temperature')
|
74
|
+
st.latex(r'''
|
75
|
+
| \text{SAT}_{\text{avg}} - \Delta \text{TSF} - \text{MAT}_{\text{avg}} | > \sqrt{{eSAT}^2 + {eMAT}^2}
|
76
|
+
''')
|
77
|
+
|
78
|
+
|
79
|
+
# display Fault Equation 9
|
80
|
+
st.title("Fault Equation 9")
|
81
|
+
st.caption('Outside air temperature deviation from setpoint')
|
82
|
+
st.latex(r'''
|
83
|
+
\text{OAT}_{\text{avg}} - eOAT > \text{SATSP} - \Delta \text{SF} + eSAT
|
84
|
+
''')
|
85
|
+
|
86
|
+
# display Fault Equation 10
|
87
|
+
st.title("Fault Equation 10")
|
88
|
+
st.caption('Temperature difference between mixed air and outside air')
|
89
|
+
st.latex(r'''
|
90
|
+
| \text{MAT}_{\text{avg}} - \text{OAT}_{\text{avg}} | > \sqrt{eMAT^2 + eOAT^2}
|
91
|
+
''')
|
92
|
+
|
93
|
+
# display Fault Equation 11
|
94
|
+
st.title("Fault Equation 11")
|
95
|
+
st.caption('Outside air temperature and supply air temperature deviation')
|
96
|
+
st.latex(r'''
|
97
|
+
\text{OAT}_{\text{avg}} + eOAT < \text{SATSP} - \Delta \text{TSF} - eSAT
|
98
|
+
''')
|
99
|
+
|
100
|
+
# display Fault Equation 12
|
101
|
+
st.title("Fault Equation 12")
|
102
|
+
st.caption('Supply air temperature deviation from mixed air temperature')
|
103
|
+
st.latex(r'''
|
104
|
+
\text{SAT}_{\text{avg}} - eSAT - \Delta \text{TSF} \geq \text{MAT}_{\text{avg}} + eMAT
|
105
|
+
''')
|
106
|
+
|
107
|
+
# display Fault Equation 13
|
108
|
+
st.title("Fault Equation 13")
|
109
|
+
st.caption('Supply air temperature too high')
|
110
|
+
st.latex(r'''
|
111
|
+
\text{SAT}_{\text{avg}} < \text{SATSP} + eSAT \quad \text{and} \quad \text{CC} \geq 99\%
|
112
|
+
''')
|
113
|
+
|
114
|
+
|
115
|
+
st.title("find_closest_weather_dates Function")
|
116
|
+
st.caption('Finding closest weather dates based on given criteria')
|
117
|
+
st.latex(r'''
|
118
|
+
1. A' = \{a \in A : a < d_{test}\} \\
|
119
|
+
2. B' = \{b \in B : b < d_{test}\} \\
|
120
|
+
3. C = \{a \in A' : a \notin B'\} \\
|
121
|
+
4. \text{if } |C| < 10 \text{ then remove } \max(A') \text{ and repeat step 3} \\
|
122
|
+
5. A = A \cap C, \text{calculate } \mu(A)
|
123
|
+
''')
|
124
|
+
st.caption('''
|
125
|
+
In this notation:
|
126
|
+
- $A$ represents the "all_data" dataset.
|
127
|
+
- $B$ represents the "suitable_baseline_no" dataset.
|
128
|
+
- $d_{test}$ is the "test_case_date".
|
129
|
+
- $A'$ and $B'$ are subsets of $A$ and $B$ that only include dates prior to $d_{test}$.
|
130
|
+
- $C$ is a set of dates in $A'$ not found in $B'$.
|
131
|
+
- $|C|$ represents the count of elements in set $C$.
|
132
|
+
- $\max(A')$ is the latest date in $A'$.
|
133
|
+
- $\mu(A)$ is the mean of the remaining elements in $A$ after filtering by set $C$.
|
134
|
+
''')
|
135
|
+
|
136
|
+
|
137
|
+
st.title("find_previous_10_days Function")
|
138
|
+
st.caption('Finding previous 10 weekdays based on given criteria')
|
139
|
+
st.latex(r'''
|
140
|
+
1. A' = \{a \in A : a < d_{test}\} \\
|
141
|
+
2. B' = \{b \in B : b < d_{test}\} \\
|
142
|
+
3. C = \{a \in A' : a \notin B'\} \\
|
143
|
+
4. \text{if } |C| < 10 \text{ then remove } \max(A') \text{ and repeat step 3} \\
|
144
|
+
5. A = A \cap C
|
145
|
+
''')
|
146
|
+
st.caption('''
|
147
|
+
In this notation:
|
148
|
+
- $A$ represents the "all_data" dataset.
|
149
|
+
- $B$ represents the "suitable_baseline_no" dataset.
|
150
|
+
- $d_{test}$ is the "test_case_date".
|
151
|
+
- $A'$ and $B'$ are subsets of $A$ and $B$ that only include dates prior to $d_{test}$.
|
152
|
+
- $C$ is a set of dates in $A'$ not found in $B'$.
|
153
|
+
- $|C|$ represents the count of elements in set $C$.
|
154
|
+
- $\max(A')$ is the latest date in $A'$.
|
155
|
+
''')
|
156
|
+
|
157
|
+
|
158
|
+
st.title("calculate_power_averages Function")
|
159
|
+
st.caption('Calculating average power for each type and time step')
|
160
|
+
st.latex(r'''
|
161
|
+
1. P_{type,i} = \{p : p \text{ is a power value at time step } t_i\} \quad \text{for each type and } i \in \{1,2,\dots,96\} \\
|
162
|
+
2. A_{type,i} = \frac{1}{|P_{type,i}|}\sum_{p \in P_{type,i}} p \quad \text{for each type and } i \in \{1,2,\dots,96\}
|
163
|
+
''')
|
164
|
+
st.caption('''
|
165
|
+
In this notation:
|
166
|
+
- $P_{type,i}$ represents the set of power values at time step $t_i$ for a specific power type (main, ahu, or solar).
|
167
|
+
- $p$ represents a power value in the set $P_{type,i}$.
|
168
|
+
- $|P_{type,i}|$ represents the count of elements in set $P_{type,i}$.
|
169
|
+
- $A_{type,i}$ represents the average power at time step $t_i$ for a specific power type.
|
170
|
+
''')
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
|
175
|
+
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import matplotlib.pyplot as plt
|
2
|
+
import pandas as pd
|
3
|
+
from io import BytesIO
|
4
|
+
import sys
|
5
|
+
|
6
|
+
class BaseReport:
|
7
|
+
def __init__(self, config):
|
8
|
+
self.config = config
|
9
|
+
|
10
|
+
def summarize_fault_times(self, df: pd.DataFrame, output_col: str) -> dict:
|
11
|
+
delta = df.index.to_series().diff().dt.total_seconds()
|
12
|
+
total_days = round(delta.sum() / 86400, 2)
|
13
|
+
total_hours = round(delta.sum() / 3600, 2)
|
14
|
+
hours_fault_mode = (delta * df[output_col]).sum() / 3600
|
15
|
+
percent_true = round(df[output_col].mean() * 100, 2)
|
16
|
+
percent_false = round((100 - percent_true), 2)
|
17
|
+
|
18
|
+
# Calculate motor runtime
|
19
|
+
motor_on = df[self.config['SUPPLY_VFD_SPEED_COL']].gt(.01).astype(int)
|
20
|
+
hours_motor_runtime = round((delta * motor_on).sum() / 3600, 2)
|
21
|
+
|
22
|
+
summary = {
|
23
|
+
'total_days': total_days,
|
24
|
+
'total_hours': total_hours,
|
25
|
+
'hours_fault_mode': hours_fault_mode,
|
26
|
+
'percent_true': percent_true,
|
27
|
+
'percent_false': percent_false,
|
28
|
+
'hours_motor_runtime': hours_motor_runtime
|
29
|
+
}
|
30
|
+
return summary
|
31
|
+
|
32
|
+
def create_hist_plot(self, df: pd.DataFrame, output_col: str):
|
33
|
+
df["hour_of_the_day"] = df.index.hour.where(df[output_col] == 1)
|
34
|
+
df = df.dropna(subset=["hour_of_the_day"])
|
35
|
+
print()
|
36
|
+
print("Time-of-day Histogram Data")
|
37
|
+
print(df["hour_of_the_day"])
|
38
|
+
print()
|
39
|
+
sys.stdout.flush()
|
40
|
+
|
41
|
+
fig, ax = plt.subplots(tight_layout=True, figsize=(25, 8))
|
42
|
+
ax.hist(df.hour_of_the_day.dropna(), bins=24)
|
43
|
+
ax.set_xlabel("Hour of the Day")
|
44
|
+
ax.set_ylabel("Frequency")
|
45
|
+
ax.set_title("Hour-Of-Day When Fault Flag is TRUE")
|
46
|
+
plt.show()
|
47
|
+
plt.close()
|
@@ -82,7 +82,9 @@ class FaultCodeSevenReport:
|
|
82
82
|
plt.close()
|
83
83
|
|
84
84
|
def display_report_in_ipython(self, df: pd.DataFrame, output_col: str = "fc7_flag"):
|
85
|
-
print(
|
85
|
+
print(
|
86
|
+
"Fault Condition 7: Supply air temperature too low its not making supply air temperature setpoint in full heating mode"
|
87
|
+
)
|
86
88
|
|
87
89
|
self.create_plot(df, output_col)
|
88
90
|
|
@@ -2,6 +2,8 @@ import pandas as pd
|
|
2
2
|
import pytest
|
3
3
|
from open_fdd.air_handling_unit.faults.fault_condition_one import FaultConditionOne
|
4
4
|
from open_fdd.air_handling_unit.faults.helper_utils import HelperUtils
|
5
|
+
from open_fdd.air_handling_unit.faults.fault_condition import MissingColumnError
|
6
|
+
|
5
7
|
|
6
8
|
# Constants
|
7
9
|
TEST_VFD_ERR_THRESHOLD = 0.05
|
@@ -27,6 +29,21 @@ fault_condition_params = {
|
|
27
29
|
fc1 = FaultConditionOne(fault_condition_params)
|
28
30
|
|
29
31
|
|
32
|
+
class TestMissingColumn:
|
33
|
+
|
34
|
+
def missing_col_df(self) -> pd.DataFrame:
|
35
|
+
data = {
|
36
|
+
TEST_DUCT_STATIC_COL: [0.99, 0.99, 0.99, 0.99, 0.99, 0.99],
|
37
|
+
# Missing TEST_SUPPLY_VFD_SPEED_COL
|
38
|
+
TEST_DUCT_STATIC_SETPOINT_COL: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
|
39
|
+
}
|
40
|
+
return pd.DataFrame(data)
|
41
|
+
|
42
|
+
def test_missing_column(self):
|
43
|
+
with pytest.raises(MissingColumnError):
|
44
|
+
fc1.apply(self.missing_col_df())
|
45
|
+
|
46
|
+
|
30
47
|
class TestNoFault:
|
31
48
|
|
32
49
|
def no_fault_df(self) -> pd.DataFrame:
|
@@ -1,200 +1,128 @@
|
|
1
1
|
import pandas as pd
|
2
|
-
import
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
df
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
class TestFaultOnInt(object):
|
131
|
-
|
132
|
-
def fault_df_on_output_int(self) -> pd.DataFrame:
|
133
|
-
data = []
|
134
|
-
for i in range(TEST_DATASET_ROWS):
|
135
|
-
if i % 2 == 0:
|
136
|
-
data.append(econ_plus_mech_clg_row_int())
|
137
|
-
else:
|
138
|
-
data.append(mech_clg_row())
|
139
|
-
return pd.DataFrame(data)
|
140
|
-
|
141
|
-
def test_fault_on_int(self):
|
142
|
-
fault_df_on_output_int = self.fault_df_on_output_int().set_index(
|
143
|
-
generate_timestamp()
|
144
|
-
)
|
145
|
-
with pytest.raises(
|
146
|
-
TypeError,
|
147
|
-
match=HelperUtils().float_int_check_err(TEST_SUPPLY_VFD_SPEED_COL),
|
148
|
-
):
|
149
|
-
fc4.apply(fault_df_on_output_int)
|
150
|
-
|
151
|
-
|
152
|
-
class TestFaultOnFloatGreaterThanOne(object):
|
153
|
-
|
154
|
-
def fault_df_on_output_greater_than_one(self) -> pd.DataFrame:
|
155
|
-
data = []
|
156
|
-
for i in range(TEST_DATASET_ROWS):
|
157
|
-
if i % 2 == 0:
|
158
|
-
data.append(econ_plus_mech_clg_row_float_greater_than_one())
|
159
|
-
else:
|
160
|
-
data.append(mech_clg_row())
|
161
|
-
return pd.DataFrame(data)
|
162
|
-
|
163
|
-
def test_fault_on_float_greater_than_one(self):
|
164
|
-
fault_df_on_output_greater_than_one = (
|
165
|
-
self.fault_df_on_output_greater_than_one().set_index(generate_timestamp())
|
166
|
-
)
|
167
|
-
with pytest.raises(
|
168
|
-
TypeError,
|
169
|
-
match=HelperUtils().float_max_check_err(TEST_SUPPLY_VFD_SPEED_COL),
|
170
|
-
):
|
171
|
-
fc4.apply(fault_df_on_output_greater_than_one)
|
172
|
-
|
173
|
-
|
174
|
-
class TestFaultOnMixedTypes(object):
|
175
|
-
|
176
|
-
def fault_df_on_mixed_types(self) -> pd.DataFrame:
|
177
|
-
data = []
|
178
|
-
for i in range(TEST_DATASET_ROWS):
|
179
|
-
if i % 2 == 0:
|
180
|
-
data.append(
|
181
|
-
{
|
182
|
-
TEST_MIX_AIR_DAMPER_COL: 0.6,
|
183
|
-
TEST_HEATING_COIL_SIG_COL: 0.0,
|
184
|
-
TEST_COOLING_COIL_SIG_COL: 0.6,
|
185
|
-
TEST_SUPPLY_VFD_SPEED_COL: 1.1,
|
186
|
-
}
|
187
|
-
)
|
188
|
-
else:
|
189
|
-
data.append(mech_clg_row())
|
190
|
-
return pd.DataFrame(data)
|
191
|
-
|
192
|
-
def test_fault_on_mixed_types(self):
|
193
|
-
fault_df_on_mixed_types = self.fault_df_on_mixed_types().set_index(
|
194
|
-
generate_timestamp()
|
195
|
-
)
|
196
|
-
with pytest.raises(
|
197
|
-
TypeError,
|
198
|
-
match=HelperUtils().float_max_check_err(TEST_SUPPLY_VFD_SPEED_COL),
|
199
|
-
):
|
200
|
-
fc4.apply(fault_df_on_mixed_types)
|
2
|
+
from open_fdd.air_handling_unit.faults.fault_condition import (
|
3
|
+
FaultCondition,
|
4
|
+
MissingColumnError,
|
5
|
+
)
|
6
|
+
import sys
|
7
|
+
|
8
|
+
|
9
|
+
class FaultConditionFour(FaultCondition):
|
10
|
+
"""Class provides the definitions for Fault Condition 4.
|
11
|
+
|
12
|
+
This fault flags excessive operating states on the AHU
|
13
|
+
if it's hunting between heating, econ, econ+mech, and
|
14
|
+
a mech clg modes. The code counts how many operating
|
15
|
+
changes in an hour and will throw a fault if there is
|
16
|
+
excessive OS changes to flag control sys hunting.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, dict_):
|
20
|
+
super().__init__()
|
21
|
+
self.delta_os_max = float
|
22
|
+
self.ahu_min_oa_dpr = float
|
23
|
+
self.economizer_sig_col = str
|
24
|
+
self.heating_sig_col = str
|
25
|
+
self.cooling_sig_col = str
|
26
|
+
self.supply_vfd_speed_col = str
|
27
|
+
self.troubleshoot_mode = bool # default to False
|
28
|
+
|
29
|
+
self.set_attributes(dict_)
|
30
|
+
|
31
|
+
# Set required columns, making heating and cooling optional
|
32
|
+
self.required_columns = [
|
33
|
+
self.economizer_sig_col,
|
34
|
+
self.supply_vfd_speed_col,
|
35
|
+
]
|
36
|
+
|
37
|
+
# If heating or cooling columns are provided, add them to the required columns
|
38
|
+
if self.heating_sig_col:
|
39
|
+
self.required_columns.append(self.heating_sig_col)
|
40
|
+
if self.cooling_sig_col:
|
41
|
+
self.required_columns.append(self.cooling_sig_col)
|
42
|
+
|
43
|
+
def get_required_columns(self) -> str:
|
44
|
+
"""Returns a string representation of the required columns."""
|
45
|
+
return f"Required columns for FaultConditionFour: {', '.join(self.required_columns)}"
|
46
|
+
|
47
|
+
def apply(self, df: pd.DataFrame) -> pd.DataFrame:
|
48
|
+
try:
|
49
|
+
# Ensure all required columns are present
|
50
|
+
self.check_required_columns(df)
|
51
|
+
|
52
|
+
# If the optional columns are not present, create them with all values set to 0.0
|
53
|
+
if self.heating_sig_col not in df.columns:
|
54
|
+
df[self.heating_sig_col] = 0.0
|
55
|
+
if self.cooling_sig_col not in df.columns:
|
56
|
+
df[self.cooling_sig_col] = 0.0
|
57
|
+
|
58
|
+
if self.troubleshoot_mode:
|
59
|
+
self.troubleshoot_cols(df)
|
60
|
+
|
61
|
+
# Check analog outputs [data with units of %] are floats only
|
62
|
+
columns_to_check = [
|
63
|
+
self.economizer_sig_col,
|
64
|
+
self.heating_sig_col,
|
65
|
+
self.cooling_sig_col,
|
66
|
+
self.supply_vfd_speed_col,
|
67
|
+
]
|
68
|
+
|
69
|
+
for col in columns_to_check:
|
70
|
+
self.check_analog_pct(df, [col])
|
71
|
+
|
72
|
+
print("=" * 50)
|
73
|
+
print("Warning: The program is in FC4 and resampling the data")
|
74
|
+
print("to compute AHU OS state changes per hour")
|
75
|
+
print("to flag any hunting issue")
|
76
|
+
print("and this usually takes a while to run...")
|
77
|
+
print("=" * 50)
|
78
|
+
|
79
|
+
sys.stdout.flush()
|
80
|
+
|
81
|
+
# AHU htg only mode based on OA damper @ min oa and only htg pid/vlv modulating
|
82
|
+
df["heating_mode"] = (
|
83
|
+
(df[self.heating_sig_col] > 0)
|
84
|
+
& (df[self.cooling_sig_col] == 0)
|
85
|
+
& (df[self.supply_vfd_speed_col] > 0)
|
86
|
+
& (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
|
87
|
+
)
|
88
|
+
|
89
|
+
# AHU econ only mode based on OA damper modulating and clg htg = zero
|
90
|
+
df["econ_only_cooling_mode"] = (
|
91
|
+
(df[self.heating_sig_col] == 0)
|
92
|
+
& (df[self.cooling_sig_col] == 0)
|
93
|
+
& (df[self.supply_vfd_speed_col] > 0)
|
94
|
+
& (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
|
95
|
+
)
|
96
|
+
|
97
|
+
# AHU econ+mech clg mode based on OA damper modulating for cooling and clg pid/vlv modulating
|
98
|
+
df["econ_plus_mech_cooling_mode"] = (
|
99
|
+
(df[self.heating_sig_col] == 0)
|
100
|
+
& (df[self.cooling_sig_col] > 0)
|
101
|
+
& (df[self.supply_vfd_speed_col] > 0)
|
102
|
+
& (df[self.economizer_sig_col] > self.ahu_min_oa_dpr)
|
103
|
+
)
|
104
|
+
|
105
|
+
# AHU mech mode based on OA damper @ min OA and clg pid/vlv modulating
|
106
|
+
df["mech_cooling_only_mode"] = (
|
107
|
+
(df[self.heating_sig_col] == 0)
|
108
|
+
& (df[self.cooling_sig_col] > 0)
|
109
|
+
& (df[self.supply_vfd_speed_col] > 0)
|
110
|
+
& (df[self.economizer_sig_col] == self.ahu_min_oa_dpr)
|
111
|
+
)
|
112
|
+
|
113
|
+
# Fill non-finite values with zero or drop them
|
114
|
+
df = df.fillna(0)
|
115
|
+
|
116
|
+
df = df.astype(int)
|
117
|
+
df = df.resample("60min").apply(lambda x: (x.eq(1) & x.shift().ne(1)).sum())
|
118
|
+
|
119
|
+
df["fc4_flag"] = (
|
120
|
+
df[df.columns].gt(self.delta_os_max).any(axis=1).astype(int)
|
121
|
+
)
|
122
|
+
|
123
|
+
return df
|
124
|
+
|
125
|
+
except MissingColumnError as e:
|
126
|
+
print(f"Error: {e.message}")
|
127
|
+
sys.stdout.flush()
|
128
|
+
raise e # Re-raise the exception so it can be caught by pytest
|