Skip to content

Commit

Permalink
ASHRAE like chiller plant fault 2 done and test
Browse files Browse the repository at this point in the history
  • Loading branch information
bbartling committed Sep 5, 2024
1 parent d1d9caf commit fff6b69
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 39 deletions.
70 changes: 31 additions & 39 deletions open_fdd/chiller_plant/faults/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,58 +137,56 @@ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
raise e


'''
class FaultConditionTwo(FaultCondition):
"""Class provides the definitions for Fault Condition 2.
Mix temperature too low; should be between outside and return air.
"""
Class provides the definitions for Fault Condition 2.
Primary chilled water flow is too high with the chilled water pump running at high speed.
py -3.12 -m pytest open_fdd/tests/chiller/test_chiller_fc2.py -rP -s
"""

def __init__(self, dict_):
super().__init__()

# Threshold parameters
self.mix_degf_err_thres = dict_.get("MIX_DEGF_ERR_THRES", None)
self.return_degf_err_thres = dict_.get("RETURN_DEGF_ERR_THRES", None)
self.outdoor_degf_err_thres = dict_.get("OUTDOOR_DEGF_ERR_THRES", None)
self.flow_error_threshold = dict_.get("FLOW_ERROR_THRESHOLD", None)
self.pump_speed_percent_max = dict_.get("PUMP_SPEED_PERCENT_MAX", None)
self.pump_speed_percent_err_thres = dict_.get(
"PUMP_SPEED_PERCENT_ERR_THRES", None
)

# Validate that threshold parameters are floats
for param, value in [
("mix_degf_err_thres", self.mix_degf_err_thres),
("return_degf_err_thres", self.return_degf_err_thres),
("outdoor_degf_err_thres", self.outdoor_degf_err_thres),
("flow_error_threshold", self.flow_error_threshold),
("pump_speed_percent_max", self.pump_speed_percent_max),
("pump_speed_percent_err_thres", self.pump_speed_percent_err_thres),
]:
if not isinstance(value, float):
raise InvalidParameterError(
f"The parameter '{param}' should be a float, but got {type(value).__name__}."
)

# Other attributes
self.mat_col = dict_.get("MAT_COL", None)
self.rat_col = dict_.get("RAT_COL", None)
self.oat_col = dict_.get("OAT_COL", None)
self.supply_vfd_speed_col = dict_.get("SUPPLY_VFD_SPEED_COL", None)
self.troubleshoot_mode = dict_.get("TROUBLESHOOT_MODE", False)
self.flow_col = dict_.get("FLOW_COL", None)
self.pump_speed_col = dict_.get("PUMP_SPEED_COL", None)
self.rolling_window_size = dict_.get("ROLLING_WINDOW_SIZE", None)

self.equation_string = (
"fc2_flag = 1 if (MAT + εMAT < min(RAT - εRAT, OAT - εOAT)) and (VFDSPD > 0) "
"fc2_flag = 1 if (FLOW > εFM) and (PUMPSPD >= PUMPSPD_max - εPUMPSPD) "
"for N consecutive values else 0 \n"
)
self.description_string = "Fault Condition 2: Mix temperature too low; should be between outside and return air \n"
self.description_string = "Fault Condition 2: Primary chilled water flow is too high with the pump running at high speed \n"
self.required_column_description = (
"Required inputs are the mix air temperature, return air temperature, outside air temperature, "
"and supply fan VFD speed \n"
"Required inputs are the flow meter readings and pump VFD speed \n"
)
self.error_string = "One or more required columns are missing or None \n"
self.error_string = f"One or more required columns are missing or None \n"

self.set_attributes(dict_)

# Set required columns specific to this fault condition
self.required_columns = [
self.mat_col,
self.rat_col,
self.oat_col,
self.supply_vfd_speed_col,
self.flow_col,
self.pump_speed_col,
]

# Check if any of the required columns are None
Expand All @@ -209,7 +207,7 @@ def __init__(self, dict_):
)

def get_required_columns(self) -> str:
"""Returns a string representation of the required columns."""
"""Called from IPython to print out."""
return (
f"{self.equation_string}"
f"{self.description_string}"
Expand All @@ -222,29 +220,21 @@ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
# Ensure all required columns are present
self.check_required_columns(df)

if self.troubleshoot_mode:
self.troubleshoot_cols(df)
# Check analog outputs [data with units of %] are floats only
columns_to_check = [self.supply_vfd_speed_col]
self.check_analog_pct(df, columns_to_check)
# Perform checks
mat_check = df[self.mat_col] + self.mix_degf_err_thres
temp_min_check = np.minimum(
df[self.rat_col] - self.return_degf_err_thres,
df[self.oat_col] - self.outdoor_degf_err_thres,
flow_check = df[self.flow_col] < self.flow_error_threshold
pump_check = (
df[self.pump_speed_col]
>= self.pump_speed_percent_max - self.pump_speed_percent_err_thres
)

combined_check = (mat_check < temp_min_check) & (
df[self.supply_vfd_speed_col] > 0.01
)
# Combined condition check
combined_check = flow_check & pump_check

# Rolling sum to count consecutive trues
rolling_sum = combined_check.rolling(window=self.rolling_window_size).sum()

# Set flag to 1 if rolling sum equals the window size
df["fc2_flag"] = (rolling_sum >= self.rolling_window_size).astype(int)
df["fc2_flag"] = (rolling_sum == self.rolling_window_size).astype(int)

return df

Expand All @@ -258,6 +248,8 @@ def apply(self, df: pd.DataFrame) -> pd.DataFrame:
raise e


'''
class FaultConditionThree(FaultCondition):
"""Class provides the definitions for Fault Condition 3.
Mix temperature too high; should be between outside and return air.
Expand Down
95 changes: 95 additions & 0 deletions open_fdd/tests/chiller/test_chiller_fc2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import pandas as pd
import pytest
from open_fdd.chiller_plant.faults import FaultConditionTwo
from open_fdd.air_handling_unit.faults.fault_condition import MissingColumnError


# Constants for test cases
TEST_FLOW_ERR_THRESHOLD = 10.0 # Error threshold for flow in GPM
TEST_PUMP_SPEED_MAX = 0.9 # Maximum pump speed percentage
TEST_PUMP_SPEED_ERR_THRESHOLD = 0.05 # Error threshold for pump speed percentage
TEST_FLOW_COL = "flow_gpm"
TEST_PUMP_SPEED_COL = "pump_speed"

ROLLING_WINDOW_SIZE = 5

# Initialize FaultConditionTwo with a dictionary
fault_condition_params = {
"FLOW_ERROR_THRESHOLD": TEST_FLOW_ERR_THRESHOLD,
"PUMP_SPEED_PERCENT_MAX": TEST_PUMP_SPEED_MAX,
"PUMP_SPEED_PERCENT_ERR_THRES": TEST_PUMP_SPEED_ERR_THRESHOLD,
"FLOW_COL": TEST_FLOW_COL,
"PUMP_SPEED_COL": TEST_PUMP_SPEED_COL,
"ROLLING_WINDOW_SIZE": ROLLING_WINDOW_SIZE,
}

fc_flow = FaultConditionTwo(fault_condition_params)


class TestMissingColumn:
def missing_col_df(self) -> pd.DataFrame:
data = {
TEST_FLOW_COL: [5.0, 8.0, 9.0, 15.0, 7.0],
# Missing pump speed column
}
return pd.DataFrame(data)

def test_missing_column(self):
with pytest.raises(MissingColumnError):
fc_flow.apply(self.missing_col_df())


class TestNoFault:
def no_fault_df(self) -> pd.DataFrame:
data = {
TEST_FLOW_COL: [11.0, 11.0, 12.0, 12.0, 12.5], # Flow above threshold
TEST_PUMP_SPEED_COL: [0.5, 0.45, 0.55, 0.45, 0.55], # Above threshold
}
return pd.DataFrame(data)

def test_no_fault(self):
results = fc_flow.apply(self.no_fault_df())
actual = results["fc2_flag"].sum()
expected = 0
message = f"FC2 no_fault_df actual is {actual} and expected is {expected}"
assert actual == expected, message


class TestFault:
def fault_df(self) -> pd.DataFrame:
data = {
TEST_FLOW_COL: [
6.0,
6.1,
6.2,
6.3,
6.4,
12.0,
6.0,
6.1,
6.2,
6.3,
6.4,
], # 6th row interrupts the fault condition
TEST_PUMP_SPEED_COL: [
0.97,
0.98,
0.98,
0.97,
0.99,
0.95,
0.97,
0.98,
0.98,
0.97,
0.99,
], # All above threshold
}
return pd.DataFrame(data)

def test_fault(self):
results = fc_flow.apply(self.fault_df())
actual = results["fc2_flag"].sum() # Fault flags counted
expected = 2 # 5 faults, interruption, then 5 more faults = 2 total flags
message = f"FC2 fault_df actual is {actual} and expected is {expected}"
assert actual == expected, message

0 comments on commit fff6b69

Please sign in to comment.