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.
Files changed (49) hide show
  1. open_fdd/air_handling_unit/faults/fault_condition.py +26 -8
  2. open_fdd/air_handling_unit/faults/fault_condition_eight.py +61 -37
  3. open_fdd/air_handling_unit/faults/fault_condition_eleven.py +59 -37
  4. open_fdd/air_handling_unit/faults/fault_condition_fifteen.py +77 -51
  5. open_fdd/air_handling_unit/faults/fault_condition_five.py +60 -41
  6. open_fdd/air_handling_unit/faults/fault_condition_four.py +108 -65
  7. open_fdd/air_handling_unit/faults/fault_condition_fourteen.py +71 -44
  8. open_fdd/air_handling_unit/faults/fault_condition_nine.py +60 -36
  9. open_fdd/air_handling_unit/faults/fault_condition_one.py +58 -37
  10. open_fdd/air_handling_unit/faults/fault_condition_seven.py +55 -32
  11. open_fdd/air_handling_unit/faults/fault_condition_six.py +100 -76
  12. open_fdd/air_handling_unit/faults/fault_condition_ten.py +62 -37
  13. open_fdd/air_handling_unit/faults/fault_condition_thirteen.py +61 -36
  14. open_fdd/air_handling_unit/faults/fault_condition_three.py +58 -33
  15. open_fdd/air_handling_unit/faults/fault_condition_twelve.py +63 -39
  16. open_fdd/air_handling_unit/faults/fault_condition_two.py +58 -36
  17. open_fdd/air_handling_unit/faults/helper_utils.py +294 -64
  18. open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_1.jpg +0 -0
  19. open_fdd/air_handling_unit/images/ahu1_fc1_2024-06_2.jpg +0 -0
  20. open_fdd/air_handling_unit/images/example1.jpg +0 -0
  21. open_fdd/air_handling_unit/images/example2.jpg +0 -0
  22. open_fdd/air_handling_unit/images/fc10_definition.png +0 -0
  23. open_fdd/air_handling_unit/images/fc11_definition.png +0 -0
  24. open_fdd/air_handling_unit/images/fc12_definition.png +0 -0
  25. open_fdd/air_handling_unit/images/fc13_definition.png +0 -0
  26. open_fdd/air_handling_unit/images/fc1_definition.png +0 -0
  27. open_fdd/air_handling_unit/images/fc1_report_screenshot_all.png +0 -0
  28. open_fdd/air_handling_unit/images/fc2_definition.png +0 -0
  29. open_fdd/air_handling_unit/images/fc3_definition.png +0 -0
  30. open_fdd/air_handling_unit/images/fc4_definition.png +0 -0
  31. open_fdd/air_handling_unit/images/fc5_definition.png +0 -0
  32. open_fdd/air_handling_unit/images/fc6_definition.png +0 -0
  33. open_fdd/air_handling_unit/images/fc7_definition.png +0 -0
  34. open_fdd/air_handling_unit/images/fc8_definition.png +0 -0
  35. open_fdd/air_handling_unit/images/fc9_definition.png +0 -0
  36. open_fdd/air_handling_unit/images/latex_generator.py +175 -0
  37. open_fdd/air_handling_unit/images/params.docx +0 -0
  38. open_fdd/air_handling_unit/images/params.pdf +0 -0
  39. open_fdd/air_handling_unit/images/plot_for_repo.png +0 -0
  40. open_fdd/air_handling_unit/reports/base_report.py +47 -0
  41. open_fdd/air_handling_unit/reports/report_fc7.py +3 -1
  42. open_fdd/tests/ahu/test_ahu_fc1.py +17 -0
  43. open_fdd/tests/ahu/test_ahu_fc4.py +127 -199
  44. open_fdd-0.1.3.dist-info/METADATA +87 -0
  45. {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/RECORD +48 -25
  46. open_fdd-0.1.0.dist-info/METADATA +0 -65
  47. {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/LICENSE +0 -0
  48. {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/WHEEL +0 -0
  49. {open_fdd-0.1.0.dist-info → open_fdd-0.1.3.dist-info}/top_level.txt +0 -0
@@ -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
+
@@ -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("Fault Condition 7: Supply air temperature too low in full heating mode")
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 pytest
3
- from open_fdd.air_handling_unit.faults.fault_condition_four import FaultConditionFour
4
- from open_fdd.air_handling_unit.faults.helper_utils import HelperUtils
5
- from datetime import datetime, timezone
6
-
7
- """
8
- To see print statements in pytest run with:
9
- $ py -3.12 -m pytest open_fdd/tests/ahu/test_ahu_fc4.py -rP -s
10
-
11
- Too much hunting in control system
12
- OS state changes greater than 7 in an hour
13
- """
14
-
15
- # Constants
16
- DELTA_OS_MAX = 7
17
- AHU_MIN_OA = 0.20
18
- TEST_MIX_AIR_DAMPER_COL = "economizer_sig_col"
19
- TEST_HEATING_COIL_SIG_COL = "heating_sig_col"
20
- TEST_COOLING_COIL_SIG_COL = "cooling_sig_col"
21
- TEST_SUPPLY_VFD_SPEED_COL = "fan_vfd_speed_col"
22
- TEST_DATASET_ROWS = 60
23
-
24
- # Initialize FaultConditionFour with a dictionary
25
- fault_condition_params = {
26
- "DELTA_OS_MAX": DELTA_OS_MAX,
27
- "AHU_MIN_OA_DPR": AHU_MIN_OA,
28
- "ECONOMIZER_SIG_COL": TEST_MIX_AIR_DAMPER_COL,
29
- "HEATING_SIG_COL": TEST_HEATING_COIL_SIG_COL,
30
- "COOLING_SIG_COL": TEST_COOLING_COIL_SIG_COL,
31
- "SUPPLY_VFD_SPEED_COL": TEST_SUPPLY_VFD_SPEED_COL,
32
- "TROUBLESHOOT_MODE": False, # default value
33
- }
34
-
35
- fc4 = FaultConditionFour(fault_condition_params)
36
-
37
-
38
- def generate_timestamp() -> pd.Series:
39
- df = pd.DataFrame()
40
- date_range = pd.period_range(
41
- # make a time stamp starting at top of
42
- # the hour with one min intervals
43
- start=datetime(2022, 6, 6, 14, 30, 0, 0, tzinfo=timezone.utc),
44
- periods=TEST_DATASET_ROWS,
45
- freq="min",
46
- )
47
- df["Date"] = [x.to_timestamp() for x in date_range]
48
- return df["Date"]
49
-
50
-
51
- def econ_plus_mech_clg_row() -> dict:
52
- data = {
53
- TEST_MIX_AIR_DAMPER_COL: 0.6,
54
- TEST_HEATING_COIL_SIG_COL: 0.0,
55
- TEST_COOLING_COIL_SIG_COL: 0.6,
56
- TEST_SUPPLY_VFD_SPEED_COL: 0.8,
57
- }
58
- return data
59
-
60
-
61
- def mech_clg_row() -> dict:
62
- data = {
63
- TEST_MIX_AIR_DAMPER_COL: 0.0,
64
- TEST_HEATING_COIL_SIG_COL: 0.0,
65
- TEST_COOLING_COIL_SIG_COL: 0.6,
66
- TEST_SUPPLY_VFD_SPEED_COL: 0.8,
67
- }
68
- return data
69
-
70
-
71
- def econ_plus_mech_clg_row_int() -> dict:
72
- data = {
73
- TEST_MIX_AIR_DAMPER_COL: 0.6,
74
- TEST_HEATING_COIL_SIG_COL: 0.0,
75
- TEST_COOLING_COIL_SIG_COL: 0.6,
76
- TEST_SUPPLY_VFD_SPEED_COL: 88,
77
- }
78
- return data
79
-
80
-
81
- def econ_plus_mech_clg_row_float_greater_than_one() -> dict:
82
- data = {
83
- TEST_MIX_AIR_DAMPER_COL: 0.6,
84
- TEST_HEATING_COIL_SIG_COL: 0.0,
85
- TEST_COOLING_COIL_SIG_COL: 0.6,
86
- TEST_SUPPLY_VFD_SPEED_COL: 88.8,
87
- }
88
- return data
89
-
90
-
91
- class TestFault(object):
92
-
93
- def fault_df(self) -> pd.DataFrame:
94
- data = []
95
- counter = 0
96
- for i in range(TEST_DATASET_ROWS):
97
- if i % 2 == 0 and counter < 11:
98
- data.append(econ_plus_mech_clg_row())
99
- counter += 1 # only simulate 10 OS changes
100
- else:
101
- data.append(mech_clg_row())
102
- return pd.DataFrame(data)
103
-
104
- def test_fault(self):
105
- fault_df = self.fault_df().set_index(generate_timestamp())
106
- results = fc4.apply(fault_df)
107
- actual = results["fc4_flag"].sum()
108
- expected = 1
109
- message = f"FC4 fault_df actual is {actual} and expected is {expected}"
110
- assert actual == expected, message
111
-
112
-
113
- class TestNoFault(object):
114
-
115
- def no_fault_df(self) -> pd.DataFrame:
116
- data = []
117
- for i in range(TEST_DATASET_ROWS):
118
- data.append(mech_clg_row())
119
- return pd.DataFrame(data)
120
-
121
- def test_no_fault(self):
122
- no_fault_df = self.no_fault_df().set_index(generate_timestamp())
123
- results = fc4.apply(no_fault_df)
124
- actual = results["fc4_flag"].sum()
125
- expected = 0
126
- message = f"FC4 no_fault_df actual is {actual} and expected is {expected}"
127
- assert actual == expected, message
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