From aa7730206102196932a5331cd7a7c3f9b49e1b40 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 21 Jun 2023 15:27:45 -0700 Subject: [PATCH 01/12] initial commit --- src/library/__init__.py | 2 + src/library/trim_respond_logic.py | 130 ++++++++++++++++++++++++++++++ tests/test_trim_respond_logic.py | 41 ++++++++++ 3 files changed, 173 insertions(+) create mode 100644 src/library/trim_respond_logic.py create mode 100644 tests/test_trim_respond_logic.py diff --git a/src/library/__init__.py b/src/library/__init__.py index 7767d579..aae37e5e 100644 --- a/src/library/__init__.py +++ b/src/library/__init__.py @@ -11,6 +11,7 @@ from .ventilation_fan_controls import * from .wlhp_loop_heat_rejection_controls import * from .supply_air_temp_reset import * +from .trim_respond_logic import * __all__ = [ "AutomaticOADamperControl", @@ -29,4 +30,5 @@ "VentilationFanControl", "WLHPLoopHeatRejectionControl", "SupplyAirTempReset", + "TrimRespondLogic", ] diff --git a/src/library/trim_respond_logic.py b/src/library/trim_respond_logic.py new file mode 100644 index 00000000..10cfb8d7 --- /dev/null +++ b/src/library/trim_respond_logic.py @@ -0,0 +1,130 @@ +import pandas as pd +import logging, sys +from typing import Union + + +def TrimRespondLogic( + df: pd.DataFrame, + setpoint_name: str, + Td: Union[float, int], + I: int, + SPtrim: Union[float, int], + SPres: Union[float, int], + SPmin: Union[float, int], + SPmax: Union[float, int], + SPres_max: Union[float, int], + tol: Union[float, int], + controller_type: str, +) -> pd.DataFrame: + """Trim and respond logic verification logic. + + Args: + df (dataframe): dataframe including time-series data of setpoint, number of requests, + setpoint_name (str): Name of the setpoint in the `df` argument. + Td (float or int): Time delay in minutes. + I (int): Number of ignored requests. + SPtrim (float or int): Minimum setpoint. + SPres (float or int): Maximum setpoint. + SPmin (float or int): + SPmax (float or int): Maximum response per time interval. + SPres_max (float or int): + tol (float or int): tolerance. + controller_type (str): either `direct_acting` or `reverse_acting` depending on whether output increases/decreases as the measurement increases/decreases. + + Return: a dataframe including verification result in each timestep. + """ + # check the given arguments type + if not isinstance(I, int): + logging.error( + f"The type of the `I` arg must be an int. It cannot be {type(I)}." + ) + return None + if not (isinstance(SPtrim, float) or isinstance(SPtrim, int)): + logging.error( + f"The type of the `SPtrim` arg must be a float. It cannot be {type(SPtrim)}." + ) + return None + if not (isinstance(SPres, float) or isinstance(SPres, int)): + logging.error( + f"The type of the `SPres` arg must be a float. It cannot be {type(SPres)}." + ) + return None + + if not (isinstance(SPmin, float) or isinstance(SPmin, int)): + logging.error( + f"The type of the `SPmin` arg must be a float. It cannot be {type(SPmin)}." + ) + return None + if not (isinstance(SPmax, float) or isinstance(SPmax, int)): + logging.error( + f"The type of the `SPmax` arg must be a float. It cannot be {type(SPmax)}." + ) + return None + + if not (isinstance(SPres_max, float) or isinstance(SPres_max, int)): + logging.error( + f"The type of the `SPres_max` arg must be a float. It cannot be {type(SPres_max)}." + ) + return None + + if not (isinstance(tol, float) and isinstance(tol, int)): + logging.error( + f"The type of the `tol` arg must be a float. It cannot be {type(tol)}." + ) + return None + + # create "result" column + result = pd.DataFrame(columns=["result"]) + + # start the T&R logic verification + for idx, row in df.iterrows(): + if idx > 0 and row["Date/Time"] >= df.at[0, "Date/Time"] + pd.Timedelta( + minutes=Td + ): + prev_row = df.loc[idx - 1] + # when the number of ignored requests is greater than or equal to the number of requests + if row["number_of_requests"] <= I: + if controller_type == "direct_acting": + if ( + row[setpoint_name] <= prev_row[setpoint_name] - SPtrim + tol + and row[setpoint_name] >= SPmin + ): + result.loc[idx, "result"] = True + else: + result.loc[idx, "result"] = False + + elif controller_type == "reverse_acting": + if ( + row[setpoint_name] <= prev_row[setpoint_name] + SPtrim - tol + and row[setpoint_name] <= SPmax + ): + result.loc[idx, "result"] = True + else: + result.loc[idx, "result"] = False + + else: + trim_amount = (row["number_of_requests"] - I) * SPres + if abs(trim_amount) > abs(SPres_max): + delta = SPres_max + else: + delta = trim_amount + + if controller_type == "direct_acting": + if ( + row[setpoint_name] >= prev_row[setpoint_name] + delta - tol + and row[setpoint_name] <= SPmax + ): + result.loc[idx, "result"] = True + else: + result.loc[idx, "result"] = False + + elif controller_type == "reverse_acting": + if ( + row[setpoint_name] >= prev_row[setpoint_name] - delta + tol + and row[setpoint_name] >= SPmin + ): + result.loc[idx, "result"] = True + else: + result.loc[idx, "result"] = False + + return result diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py new file mode 100644 index 00000000..bd917c70 --- /dev/null +++ b/tests/test_trim_respond_logic.py @@ -0,0 +1,41 @@ +import sys +import unittest + +import pandas as pd + +sys.path.append("./src") +from library.trim_respond_logic import TrimRespondLogic + +# data preparation +start_date = "2023-06-21 12:00:00" # Start date and time +end_date = "2023-06-21 12:59:59" # End date and time + +data = pd.DataFrame(columns=["Date/Time", "static_setpoint", "number_of_requests"]) +timestamps = pd.date_range(start=start_date, end=end_date, freq="2T") +data["Date/Time"] = timestamps +# fmt: off +data["static_setpoint"] = [0.50, 0.46, 0.42, 0.48,0.60, 0.75, 0.81, 0.77, 0.73, 0.69, 0.65, 0.61, 0.57, 0.53, 0.49, 0.45, 0.41, 0.37, 0.33, 0.29, 0.25, 0.21, 0.36, 0.51, 0.66, 0.81, 0.77, 0.73, 0.85, 0.81] +data["number_of_requests"] = [0, 1, 2, 3, 4, 6, 3, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 1, 0, 1, 2, 6, 6, 5, 5, 2, 2, 4, 2] +# fmt: on + + +class TestTrimRespond(unittest.TestCase): + def test_direct_acting(self): + result = TrimRespondLogic( + data, + setpoint_name="static_setpoint", + Td=0, + I=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertTrue(all(result)) + + +if __name__ == "__main__": + unittest.main() From 52c83cf4b4287675354158e48d2db6139d63edfc Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 21 Jun 2023 16:41:22 -0700 Subject: [PATCH 02/12] update unittest --- src/library/trim_respond_logic.py | 105 ++++++++++------- tests/test_trim_respond_logic.py | 187 +++++++++++++++++++++++++++++- 2 files changed, 241 insertions(+), 51 deletions(-) diff --git a/src/library/trim_respond_logic.py b/src/library/trim_respond_logic.py index 10cfb8d7..122d0f2c 100644 --- a/src/library/trim_respond_logic.py +++ b/src/library/trim_respond_logic.py @@ -1,13 +1,13 @@ -import pandas as pd -import logging, sys +import logging from typing import Union +import pandas as pd + def TrimRespondLogic( df: pd.DataFrame, - setpoint_name: str, Td: Union[float, int], - I: int, + ignored_requests: int, SPtrim: Union[float, int], SPres: Union[float, int], SPmin: Union[float, int], @@ -19,10 +19,9 @@ def TrimRespondLogic( """Trim and respond logic verification logic. Args: - df (dataframe): dataframe including time-series data of setpoint, number of requests, - setpoint_name (str): Name of the setpoint in the `df` argument. + df (dataframe): dataframe that must include timestamp (`Date/Time` column), time-series data of setpoint (`setpoint` column), number of requests (`number_of_requests` column name), Td (float or int): Time delay in minutes. - I (int): Number of ignored requests. + ignored_requests (int): Number of ignored requests. SPtrim (float or int): Minimum setpoint. SPres (float or int): Maximum setpoint. SPmin (float or int): @@ -31,79 +30,95 @@ def TrimRespondLogic( tol (float or int): tolerance. controller_type (str): either `direct_acting` or `reverse_acting` depending on whether output increases/decreases as the measurement increases/decreases. - Return: a dataframe including verification result in each timestep. + Return: dataframe including verification result in each timestep. """ + # check the given arguments type - if not isinstance(I, int): + if not isinstance(Td, (float, int)): logging.error( - f"The type of the `I` arg must be an int. It cannot be {type(I)}." + f"The type of the `Td` arg must be a float or int. It cannot be {type(Td)}." ) return None - if not (isinstance(SPtrim, float) or isinstance(SPtrim, int)): + + if not isinstance(ignored_requests, int): logging.error( - f"The type of the `SPtrim` arg must be a float. It cannot be {type(SPtrim)}." + f"The type of the `ignored_requests` arg must be an int. It cannot be {type(ignored_requests)}." ) return None - if not (isinstance(SPres, float) or isinstance(SPres, int)): + + if not isinstance(SPtrim, (float, int)): logging.error( - f"The type of the `SPres` arg must be a float. It cannot be {type(SPres)}." + f"The type of the `SPtrim` arg must be a float or int. It cannot be {type(SPtrim)}." + ) + return None + if not isinstance(SPres, (float, int)): + logging.error( + f"The type of the `SPres` arg must be a float or int. It cannot be {type(SPres)}." ) return None - if not (isinstance(SPmin, float) or isinstance(SPmin, int)): + if not isinstance(SPmin, (float, int)): + logging.error( + f"The type of the `SPmin` arg must be a float or int. It cannot be {type(SPmin)}." + ) + return None + if not isinstance(SPmax, (float, int)): logging.error( - f"The type of the `SPmin` arg must be a float. It cannot be {type(SPmin)}." + f"The type of the `SPmax` arg must be a float or int. It cannot be {type(SPmax)}." ) return None - if not (isinstance(SPmax, float) or isinstance(SPmax, int)): + + if not isinstance(SPres_max, (float, int)): logging.error( - f"The type of the `SPmax` arg must be a float. It cannot be {type(SPmax)}." + f"The type of the `SPres_max` arg must be a float or int. It cannot be {type(SPres_max)}." ) return None - if not (isinstance(SPres_max, float) or isinstance(SPres_max, int)): + if not isinstance(tol, (float, int)): logging.error( - f"The type of the `SPres_max` arg must be a float. It cannot be {type(SPres_max)}." + f"The type of the `tol` arg must be a float or int. It cannot be {type(tol)}." ) return None - if not (isinstance(tol, float) and isinstance(tol, int)): + # check if the `controller_type` is either `direct_acting` or `reverse_acting` + if controller_type not in ["direct_acting", "reverse_acting"]: logging.error( - f"The type of the `tol` arg must be a float. It cannot be {type(tol)}." + f"`controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `{controller_type}`." ) return None - # create "result" column - result = pd.DataFrame(columns=["result"]) + # create "result" dataframe + result = pd.DataFrame(columns=["result"], index=df["Date/Time"]) + result.drop(result.index[0], inplace=True) # start the T&R logic verification + actual_start_time = df.at[0, "Date/Time"] + pd.Timedelta(minutes=Td) for idx, row in df.iterrows(): - if idx > 0 and row["Date/Time"] >= df.at[0, "Date/Time"] + pd.Timedelta( - minutes=Td - ): + current_time = row["Date/Time"] + if idx > 0 and current_time >= actual_start_time: prev_row = df.loc[idx - 1] # when the number of ignored requests is greater than or equal to the number of requests - if row["number_of_requests"] <= I: + if row["number_of_requests"] <= ignored_requests: if controller_type == "direct_acting": if ( - row[setpoint_name] <= prev_row[setpoint_name] - SPtrim + tol - and row[setpoint_name] >= SPmin + row["setpoint"] <= prev_row["setpoint"] - SPtrim + tol + and row["setpoint"] >= SPmin ): - result.loc[idx, "result"] = True + result.loc[current_time, "result"] = True else: - result.loc[idx, "result"] = False + result.loc[current_time, "result"] = False elif controller_type == "reverse_acting": if ( - row[setpoint_name] <= prev_row[setpoint_name] + SPtrim - tol - and row[setpoint_name] <= SPmax + row["setpoint"] <= prev_row["setpoint"] + SPtrim - tol + and row["setpoint"] <= SPmax ): - result.loc[idx, "result"] = True + result.loc[current_time, "result"] = True else: - result.loc[idx, "result"] = False + result.loc[current_time, "result"] = False else: - trim_amount = (row["number_of_requests"] - I) * SPres + trim_amount = (row["number_of_requests"] - ignored_requests) * SPres if abs(trim_amount) > abs(SPres_max): delta = SPres_max else: @@ -111,20 +126,20 @@ def TrimRespondLogic( if controller_type == "direct_acting": if ( - row[setpoint_name] >= prev_row[setpoint_name] + delta - tol - and row[setpoint_name] <= SPmax + row["setpoint"] >= prev_row["setpoint"] + delta - tol + and row["setpoint"] <= SPmax ): - result.loc[idx, "result"] = True + result.loc[current_time, "result"] = True else: - result.loc[idx, "result"] = False + result.loc[current_time, "result"] = False elif controller_type == "reverse_acting": if ( - row[setpoint_name] >= prev_row[setpoint_name] - delta + tol - and row[setpoint_name] >= SPmin + row["setpoint"] >= prev_row["setpoint"] - delta + tol + and row["setpoint"] >= SPmin ): - result.loc[idx, "result"] = True + result.loc[current_time, "result"] = True else: - result.loc[idx, "result"] = False + result.loc[current_time, "result"] = False return result diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index bd917c70..00ad1238 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -14,18 +14,193 @@ timestamps = pd.date_range(start=start_date, end=end_date, freq="2T") data["Date/Time"] = timestamps # fmt: off -data["static_setpoint"] = [0.50, 0.46, 0.42, 0.48,0.60, 0.75, 0.81, 0.77, 0.73, 0.69, 0.65, 0.61, 0.57, 0.53, 0.49, 0.45, 0.41, 0.37, 0.33, 0.29, 0.25, 0.21, 0.36, 0.51, 0.66, 0.81, 0.77, 0.73, 0.85, 0.81] +# The below data is from Figure 5.1.14.4 in the ASHRAE G36-2021 +data["setpoint"] = [0.50, 0.46, 0.42, 0.48,0.60, 0.75, 0.81, 0.77, 0.73, 0.69, 0.65, 0.61, 0.57, 0.53, 0.49, 0.45, 0.41, 0.37, 0.33, 0.29, 0.25, 0.21, 0.36, 0.51, 0.66, 0.81, 0.77, 0.73, 0.85, 0.81] data["number_of_requests"] = [0, 1, 2, 3, 4, 6, 3, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 1, 0, 1, 2, 6, 6, 5, 5, 2, 2, 4, 2] # fmt: on class TestTrimRespond(unittest.TestCase): - def test_direct_acting(self): - result = TrimRespondLogic( + def test_check_args_type(self): + """Test whether arguments' type is correct.""" + + # check `Td` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td="0", + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `Td` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + + # check `I` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests="2", + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `ignored_requests` arg must be an int. It cannot be .", + logobs.output[0], + ) + # check `SPtrim` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SPtrim="-0.04", + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `SPtrim` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + + # check `SPres` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SPtrim=-0.04, + SPres="0.06", + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `SPres` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + + # check `SPmin` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin="0.15", + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `SPmin` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + + # check `SPmax` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax="1.5", + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `SPmax` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + + # check `SPres_max` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max="0.15", + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `SPres_max` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + + # check `tol` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol="0.01", + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `tol` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + + # check `controller_type` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="wrong_value", + ) + self.assertEqual( + "ERROR:root:`controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `wrong_value`.", + logobs.output[0], + ) + + def test_TR_logic_verification(self): + """test whether the T&R logic was implemented correctly.""" + + tr_obj = TrimRespondLogic( data, - setpoint_name="static_setpoint", Td=0, - I=2, + ignored_requests=2, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -34,7 +209,7 @@ def test_direct_acting(self): tol=0.01, controller_type="direct_acting", ) - self.assertTrue(all(result)) + self.assertTrue(all(tr_obj)) if __name__ == "__main__": From 95ec7c20c7592db403edc59a3de815ea8d89bd88 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 21 Jun 2023 16:55:18 -0700 Subject: [PATCH 03/12] add df checking in unit test --- src/library/trim_respond_logic.py | 13 ++++++++++- tests/test_trim_respond_logic.py | 38 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/library/trim_respond_logic.py b/src/library/trim_respond_logic.py index 122d0f2c..3f726ac0 100644 --- a/src/library/trim_respond_logic.py +++ b/src/library/trim_respond_logic.py @@ -15,7 +15,7 @@ def TrimRespondLogic( SPres_max: Union[float, int], tol: Union[float, int], controller_type: str, -) -> pd.DataFrame: +) -> Union[None, pd.DataFrame]: """Trim and respond logic verification logic. Args: @@ -34,6 +34,17 @@ def TrimRespondLogic( """ # check the given arguments type + if not isinstance(df, pd.DataFrame): + logging.error( + f"The type of the `df` arg must be a dataframe. It cannot be {type(df)}." + ) + return None + + for col in ["Date/Time", "setpoint", "number_of_requests"]: + if col not in df.columns: + logging.error(f"{col} column doesn't exist in the `df`.") + return None + if not isinstance(Td, (float, int)): logging.error( f"The type of the `Td` arg must be a float or int. It cannot be {type(Td)}." diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index 00ad1238..df7aab26 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -24,6 +24,44 @@ class TestTrimRespond(unittest.TestCase): def test_check_args_type(self): """Test whether arguments' type is correct.""" + # check `data` (type) + with self.assertLogs() as logobs: + TrimRespondLogic( + dict(data), + Td="0", + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `df` arg must be a dataframe. It cannot be .", + logobs.output[0], + ) + + # check `data` (missing column) + with self.assertLogs() as logobs: + TrimRespondLogic( + data.drop("setpoint", axis=1), + Td="0", + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:setpoint column doesn't exist in the `df`.", + logobs.output[0], + ) + # check `Td` with self.assertLogs() as logobs: TrimRespondLogic( From bbc85259722e290b2159cb8db3f8ed8129816015 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 21 Jun 2023 16:58:38 -0700 Subject: [PATCH 04/12] correct the column name in unit test --- tests/test_trim_respond_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index df7aab26..4b703ef4 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -10,7 +10,7 @@ start_date = "2023-06-21 12:00:00" # Start date and time end_date = "2023-06-21 12:59:59" # End date and time -data = pd.DataFrame(columns=["Date/Time", "static_setpoint", "number_of_requests"]) +data = pd.DataFrame(columns=["Date/Time", "setpoint", "number_of_requests"]) timestamps = pd.date_range(start=start_date, end=end_date, freq="2T") data["Date/Time"] = timestamps # fmt: off From 25c34bae74a90d41379c7e6013cfd945e770f64c Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 21 Jun 2023 17:09:02 -0700 Subject: [PATCH 05/12] fix the doct string --- src/library/trim_respond_logic.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/library/trim_respond_logic.py b/src/library/trim_respond_logic.py index 3f726ac0..2e49cdda 100644 --- a/src/library/trim_respond_logic.py +++ b/src/library/trim_respond_logic.py @@ -16,17 +16,17 @@ def TrimRespondLogic( tol: Union[float, int], controller_type: str, ) -> Union[None, pd.DataFrame]: - """Trim and respond logic verification logic. + """Trim and respond logic verification. Args: df (dataframe): dataframe that must include timestamp (`Date/Time` column), time-series data of setpoint (`setpoint` column), number of requests (`number_of_requests` column name), Td (float or int): Time delay in minutes. ignored_requests (int): Number of ignored requests. - SPtrim (float or int): Minimum setpoint. - SPres (float or int): Maximum setpoint. - SPmin (float or int): - SPmax (float or int): Maximum response per time interval. - SPres_max (float or int): + SPtrim (float or int): Trim amount. + SPres (float or int): Respond amount. + SPmin (float or int): Minimum setpoint. + SPmax (float or int): Maximum setpoint. + SPres_max (float or int): Maximum response per time interval. tol (float or int): tolerance. controller_type (str): either `direct_acting` or `reverse_acting` depending on whether output increases/decreases as the measurement increases/decreases. From 95af4a6af26cbbd5cbf4f9db394185fba0725c9c Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Mon, 26 Jun 2023 13:52:12 -0700 Subject: [PATCH 06/12] fix unit test indentation --- tests/test_trim_respond_logic.py | 36 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index 4b703ef4..2687b0f9 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -43,24 +43,24 @@ def test_check_args_type(self): logobs.output[0], ) - # check `data` (missing column) - with self.assertLogs() as logobs: - TrimRespondLogic( - data.drop("setpoint", axis=1), - Td="0", - ignored_requests=2, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, - controller_type="direct_acting", - ) - self.assertEqual( - "ERROR:root:setpoint column doesn't exist in the `df`.", - logobs.output[0], - ) + # check `data` (missing column) + with self.assertLogs() as logobs: + TrimRespondLogic( + data.drop("setpoint", axis=1), + Td="0", + ignored_requests=2, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:setpoint column doesn't exist in the `df`.", + logobs.output[0], + ) # check `Td` with self.assertLogs() as logobs: From 98e217686872b03fac63692424282fa530b7f6d9 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Mon, 10 Jul 2023 09:43:39 -0700 Subject: [PATCH 07/12] add setpoint calculation feature --- src/library/trim_respond_logic.py | 176 ++++++++++++++++++++---------- tests/test_trim_respond_logic.py | 91 ++++++++++++++- 2 files changed, 208 insertions(+), 59 deletions(-) diff --git a/src/library/trim_respond_logic.py b/src/library/trim_respond_logic.py index 2e49cdda..736a6077 100644 --- a/src/library/trim_respond_logic.py +++ b/src/library/trim_respond_logic.py @@ -2,12 +2,14 @@ from typing import Union import pandas as pd +from pandas.api.types import is_datetime64_any_dtype def TrimRespondLogic( df: pd.DataFrame, Td: Union[float, int], ignored_requests: int, + SP0: Union[float, int], SPtrim: Union[float, int], SPres: Union[float, int], SPmin: Union[float, int], @@ -16,35 +18,43 @@ def TrimRespondLogic( tol: Union[float, int], controller_type: str, ) -> Union[None, pd.DataFrame]: - """Trim and respond logic verification. + """Trim and respond logic verification & setpoint calculation. Args: df (dataframe): dataframe that must include timestamp (`Date/Time` column), time-series data of setpoint (`setpoint` column), number of requests (`number_of_requests` column name), Td (float or int): Time delay in minutes. ignored_requests (int): Number of ignored requests. + SP0 (float or int): Initial setpoint. SPtrim (float or int): Trim amount. SPres (float or int): Respond amount. SPmin (float or int): Minimum setpoint. SPmax (float or int): Maximum setpoint. SPres_max (float or int): Maximum response per time interval. tol (float or int): tolerance. - controller_type (str): either `direct_acting` or `reverse_acting` depending on whether output increases/decreases as the measurement increases/decreases. + controller_type (str): either `direct_acting` or `reverse_acting`. When an increase in the controller output results in an increase in the process barialbe, the `direct_acting` option should be selected. Otherwise, the `reverse_acting` option should be selected. - Return: dataframe including verification result in each timestep. + Return: dataframe including verification in boolean and setpoint calculation results in each timestep. """ - # check the given arguments type + # check df type if not isinstance(df, pd.DataFrame): logging.error( f"The type of the `df` arg must be a dataframe. It cannot be {type(df)}." ) return None - for col in ["Date/Time", "setpoint", "number_of_requests"]: + # check df index type + if not is_datetime64_any_dtype(df.index): + logging.error(f"Index's format is not in datetime format.") + return None + + # check df columns + for col in ("setpoint", "number_of_requests"): if col not in df.columns: logging.error(f"{col} column doesn't exist in the `df`.") return None + # check the given arguments type if not isinstance(Td, (float, int)): logging.error( f"The type of the `Td` arg must be a float or int. It cannot be {type(Td)}." @@ -57,6 +67,12 @@ def TrimRespondLogic( ) return None + if not isinstance(SP0, (float, int)): + logging.error( + f"The type of the `SP0` arg must be a float or int. It cannot be {type(tol)}." + ) + return None + if not isinstance(SPtrim, (float, int)): logging.error( f"The type of the `SPtrim` arg must be a float or int. It cannot be {type(SPtrim)}." @@ -73,6 +89,7 @@ def TrimRespondLogic( f"The type of the `SPmin` arg must be a float or int. It cannot be {type(SPmin)}." ) return None + if not isinstance(SPmax, (float, int)): logging.error( f"The type of the `SPmax` arg must be a float or int. It cannot be {type(SPmax)}." @@ -92,65 +109,114 @@ def TrimRespondLogic( return None # check if the `controller_type` is either `direct_acting` or `reverse_acting` - if controller_type not in ["direct_acting", "reverse_acting"]: + if controller_type not in ("direct_acting", "reverse_acting"): logging.error( f"`controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `{controller_type}`." ) return None + # calculate actual start time + initial_timestamp = df.index[0] + actual_start_time = initial_timestamp + pd.Timedelta(minutes=Td) + # create "result" dataframe - result = pd.DataFrame(columns=["result"], index=df["Date/Time"]) - result.drop(result.index[0], inplace=True) + result = pd.DataFrame( + columns=["verification", "setpoint"], + index=df[df.index >= actual_start_time].index, + ) + + # preprocess df, cut off before Td + copied_df = df.copy()[df.index > actual_start_time] + + # setup an initial value + result.loc[actual_start_time, "verification"] = True + if initial_timestamp == actual_start_time: + result.loc[actual_start_time, "setpoint"] = SP0 + else: + try: + df.loc[actual_start_time, "setpoint"] + except KeyError: + logging.error( + f"The delayed timestamp must be included in the `df` timestamp." + ) + return None # start the T&R logic verification - actual_start_time = df.at[0, "Date/Time"] + pd.Timedelta(minutes=Td) - for idx, row in df.iterrows(): - current_time = row["Date/Time"] - if idx > 0 and current_time >= actual_start_time: - prev_row = df.loc[idx - 1] - # when the number of ignored requests is greater than or equal to the number of requests - if row["number_of_requests"] <= ignored_requests: - if controller_type == "direct_acting": - if ( - row["setpoint"] <= prev_row["setpoint"] - SPtrim + tol - and row["setpoint"] >= SPmin - ): - result.loc[current_time, "result"] = True - else: - result.loc[current_time, "result"] = False - - elif controller_type == "reverse_acting": - if ( - row["setpoint"] <= prev_row["setpoint"] + SPtrim - tol - and row["setpoint"] <= SPmax - ): - result.loc[current_time, "result"] = True - else: - result.loc[current_time, "result"] = False - + for current_timestamp, row in copied_df.iterrows(): + prev_row_df = copied_df.iloc[copied_df.index.get_loc(current_timestamp) - 1] + prev_row_result = result.iloc[result.index.get_loc(current_timestamp) - 1] + + # when the number of ignored requests is greater than or equal to the number of requests + if row["number_of_requests"] <= ignored_requests: + if controller_type == "direct_acting": + # determine T&R logic was implemented correctly (verification) + if ( + row["setpoint"] <= prev_row_df["setpoint"] + SPtrim + tol + and row["setpoint"] >= SPmin + ): + result.loc[current_timestamp, "verification"] = True + else: + result.loc[current_timestamp, "verification"] = False + + # calculate setpoint by T&R logic + new_setpoint = prev_row_result["setpoint"] + SPtrim + result.loc[current_timestamp, "setpoint"] = ( + SPmin if new_setpoint < SPmin else new_setpoint + ) + + elif controller_type == "reverse_acting": + # determine T&R logic was implemented correctly (verification) + if ( + row["setpoint"] <= prev_row_df["setpoint"] - SPtrim - tol + and row["setpoint"] <= SPmax + ): + result.loc[current_timestamp, "verification"] = True + else: + result.loc[current_timestamp, "verification"] = False + + # calculate setpoint by T&R logic + new_setpoint = prev_row_result["setpoint"] - SPtrim + result.loc[current_timestamp, "setpoint"] = ( + SPmax if new_setpoint >= SPmax else new_setpoint + ) + + else: + trim_amount = (row["number_of_requests"] - ignored_requests) * SPres + if abs(trim_amount) > abs(SPres_max): + delta = SPres_max else: - trim_amount = (row["number_of_requests"] - ignored_requests) * SPres - if abs(trim_amount) > abs(SPres_max): - delta = SPres_max + delta = trim_amount + + if controller_type == "direct_acting": + # determine T&R logic was implemented correctly (verification) + if ( + row["setpoint"] >= prev_row_df["setpoint"] + delta - tol + and row["setpoint"] <= SPmax + ): + result.loc[current_timestamp, "verification"] = True else: - delta = trim_amount - - if controller_type == "direct_acting": - if ( - row["setpoint"] >= prev_row["setpoint"] + delta - tol - and row["setpoint"] <= SPmax - ): - result.loc[current_time, "result"] = True - else: - result.loc[current_time, "result"] = False - - elif controller_type == "reverse_acting": - if ( - row["setpoint"] >= prev_row["setpoint"] - delta + tol - and row["setpoint"] >= SPmin - ): - result.loc[current_time, "result"] = True - else: - result.loc[current_time, "result"] = False + result.loc[current_timestamp, "verification"] = False + + # calculate setpoint by T&R logic + new_setpoint = prev_row_result["setpoint"] + delta + result.loc[current_timestamp, "setpoint"] = ( + SPmax if new_setpoint > SPmax else new_setpoint + ) + + elif controller_type == "reverse_acting": + # determine T&R logic was implemented correctly (verification) + if ( + row["setpoint"] >= prev_row_df["setpoint"] - delta + tol + and row["setpoint"] >= SPmin + ): + result.loc[current_timestamp, "verification"] = True + else: + result.loc[current_timestamp, "verification"] = False + + # calculate setpoint by T&R logic + new_setpoint = prev_row_result["setpoint"] - delta + result.loc[current_timestamp, "setpoint"] = ( + SPmin if new_setpoint <= SPmin else new_setpoint + ) return result diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index 2687b0f9..9210a3d0 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -10,9 +10,10 @@ start_date = "2023-06-21 12:00:00" # Start date and time end_date = "2023-06-21 12:59:59" # End date and time -data = pd.DataFrame(columns=["Date/Time", "setpoint", "number_of_requests"]) -timestamps = pd.date_range(start=start_date, end=end_date, freq="2T") -data["Date/Time"] = timestamps +data = pd.DataFrame( + columns=["setpoint", "number_of_requests"], + index=pd.date_range(start=start_date, end=end_date, freq="2T"), +) # fmt: off # The below data is from Figure 5.1.14.4 in the ASHRAE G36-2021 data["setpoint"] = [0.50, 0.46, 0.42, 0.48,0.60, 0.75, 0.81, 0.77, 0.73, 0.69, 0.65, 0.61, 0.57, 0.53, 0.49, 0.45, 0.41, 0.37, 0.33, 0.29, 0.25, 0.21, 0.36, 0.51, 0.66, 0.81, 0.77, 0.73, 0.85, 0.81] @@ -30,6 +31,7 @@ def test_check_args_type(self): dict(data), Td="0", ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -43,12 +45,33 @@ def test_check_args_type(self): logobs.output[0], ) + # check `data` index type + with self.assertLogs() as logobs: + TrimRespondLogic( + data.reset_index(drop=True), + Td=0, + ignored_requests=2, + SP0=0.5, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:Index's format is not in datetime format.", + logobs.output[0], + ) + # check `data` (missing column) with self.assertLogs() as logobs: TrimRespondLogic( data.drop("setpoint", axis=1), Td="0", ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -68,6 +91,7 @@ def test_check_args_type(self): data, Td="0", ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -87,6 +111,7 @@ def test_check_args_type(self): data, Td=0, ignored_requests="2", + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -99,12 +124,34 @@ def test_check_args_type(self): "ERROR:root:The type of the `ignored_requests` arg must be an int. It cannot be .", logobs.output[0], ) + + # check `SP0` + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=4, + ignored_requests=2, + SP0="0.5", + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol="0.01", + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The type of the `SP0` arg must be a float or int. It cannot be .", + logobs.output[0], + ) + # check `SPtrim` with self.assertLogs() as logobs: TrimRespondLogic( data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim="-0.04", SPres=0.06, SPmin=0.15, @@ -124,6 +171,7 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres="0.06", SPmin=0.15, @@ -143,6 +191,7 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin="0.15", @@ -162,6 +211,7 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -181,6 +231,7 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -200,6 +251,7 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -219,6 +271,7 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -232,13 +285,35 @@ def test_check_args_type(self): logobs.output[0], ) + # check if delayed timestamp is in the `df`'s index + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=5, + ignored_requests=2, + SP0=0.5, + SPtrim=-0.04, + SPres=0.06, + SPmin=0.15, + SPmax=1.5, + SPres_max=0.15, + tol=0.01, + controller_type="direct_acting", + ) + self.assertEqual( + "ERROR:root:The delayed timestamp must be included in the `df` timestamp.", + logobs.output[0], + ) + def test_TR_logic_verification(self): """test whether the T&R logic was implemented correctly.""" + # verify the verification was implemented correctly tr_obj = TrimRespondLogic( data, Td=0, ignored_requests=2, + SP0=0.5, SPtrim=-0.04, SPres=0.06, SPmin=0.15, @@ -247,7 +322,15 @@ def test_TR_logic_verification(self): tol=0.01, controller_type="direct_acting", ) - self.assertTrue(all(tr_obj)) + + # check if all verification passed + self.assertTrue(all(tr_obj["verification"])) + + # check if the setpoint is in the correct range + # assume if original and calculated setpoints are within the 1% range, the results are acceptable + self.assertTrue( + all(abs(tr_obj["setpoint"] - data["setpoint"]) / data["setpoint"] <= 0.01) + ) if __name__ == "__main__": From 5cd158263770ab4382e9ebdcb17da386a5208bbd Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 30 Apr 2025 14:45:34 -0700 Subject: [PATCH 08/12] Added Modelica dataset unit test and use global tolerance --- constrain/library/__init__.py | 2 + constrain/library/trim_respond_logic.py | 65 +++- tests/test_trim_respond_logic.py | 386 +++++++++++++++++------- 3 files changed, 322 insertions(+), 131 deletions(-) diff --git a/constrain/library/__init__.py b/constrain/library/__init__.py index 7ddd7255..4bfcd7eb 100644 --- a/constrain/library/__init__.py +++ b/constrain/library/__init__.py @@ -47,6 +47,7 @@ from .LocalLoopUnmetHours import * from .MZSystemOccupiedStandbyVentilationZoneControl import * from .supply_air_temp_reset import * +from .trim_respond_logic import * from .vav_minimum_turndown_during_reheat import * from .vav_minimum_turndown_during_reheat_pressure_reset import * from .vav_static_pressure_sensor_location import * @@ -116,4 +117,5 @@ "G36ReheatTerminalBoxHeatingAirflowSetpoint", "G36ReheatTerminalBoxDeadbandAirflowSetpoint", "G36TerminalBoxCoolingMinimumAirflow", + "TrimRespondLogic", ] diff --git a/constrain/library/trim_respond_logic.py b/constrain/library/trim_respond_logic.py index 736a6077..4b8b92f1 100644 --- a/constrain/library/trim_respond_logic.py +++ b/constrain/library/trim_respond_logic.py @@ -1,9 +1,26 @@ +import json import logging +from pathlib import Path from typing import Union import pandas as pd from pandas.api.types import is_datetime64_any_dtype +VARIABLE_SET = ("temperature", "airflow", "waterflow", "pressure") +VARIABLE_SUBTYPE_SET = { + "temperature": ( + "supply_air", + "zone", + "outdoor_air", + "discharge_air", + "return_air", + "general", + ), + "airflow": ("supply", "outdoor_air", "exhaust_air", "general"), + "waterflow": ("hot", "general"), + "pressure": ("static", "building", "general"), +} + def TrimRespondLogic( df: pd.DataFrame, @@ -15,8 +32,9 @@ def TrimRespondLogic( SPmin: Union[float, int], SPmax: Union[float, int], SPres_max: Union[float, int], - tol: Union[float, int], controller_type: str, + variable_type: str, + variable_subtype: str, ) -> Union[None, pd.DataFrame]: """Trim and respond logic verification & setpoint calculation. @@ -30,9 +48,14 @@ def TrimRespondLogic( SPmin (float or int): Minimum setpoint. SPmax (float or int): Maximum setpoint. SPres_max (float or int): Maximum response per time interval. - tol (float or int): tolerance. - controller_type (str): either `direct_acting` or `reverse_acting`. When an increase in the controller output results in an increase in the process barialbe, the `direct_acting` option should be selected. Otherwise, the `reverse_acting` option should be selected. - + controller_type (str): either `direct_acting` or `reverse_acting`. When an increase in the controller output results in an increase in the process varialbe, the `direct_acting` option should be selected. Otherwise, the `reverse_acting` option should be selected. + variable_type (str): Type of variable to determine tolerance. Available options: temperature, airflow, waterflow, pressure. All the tolerance units are in SI (e.g., temperature: deg C, airflow: m3/s, waterflow: m3/s, pressure Pa) + variable_subtype (str): Variable subtype to determine tolerance. + Available options: + temperature: supply_air, zone, outdoor_air, discharge_air, return_air, general + airflow: supply, outdoor_air, exhaust_air, general + waterflow: hot, general + pressure: static, building, general Return: dataframe including verification in boolean and setpoint calculation results in each timestep. """ @@ -69,7 +92,7 @@ def TrimRespondLogic( if not isinstance(SP0, (float, int)): logging.error( - f"The type of the `SP0` arg must be a float or int. It cannot be {type(tol)}." + f"The type of the `SP0` arg must be a float or int. It cannot be {type(SP0)}." ) return None @@ -102,19 +125,32 @@ def TrimRespondLogic( ) return None - if not isinstance(tol, (float, int)): + # check if the `controller_type` is either `direct_acting` or `reverse_acting` + if controller_type not in ("direct_acting", "reverse_acting"): logging.error( - f"The type of the `tol` arg must be a float or int. It cannot be {type(tol)}." + f"The `controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `{controller_type}`." ) return None - # check if the `controller_type` is either `direct_acting` or `reverse_acting` - if controller_type not in ("direct_acting", "reverse_acting"): + # set tolerance + if variable_type not in VARIABLE_SET: + logging.error( + f"The `variable_type` arg must be one of temperature, airflow, waterflow, pressure. It can't be `{variable_type}`." + ) + return None + + if variable_subtype not in VARIABLE_SUBTYPE_SET.get(variable_type): logging.error( - f"`controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `{controller_type}`." + f"The `variable_subtype` arg doesn't have a right subtype. Please check the ./constrain/tolerances.json file." ) return None + path_to_custom_tolerance_file = Path(__file__).parent.parent / "tolerances.json" + with open(path_to_custom_tolerance_file) as f: + tolerances = json.load(f) + + tol = tolerances[variable_type]["types"][variable_subtype] + # calculate actual start time initial_timestamp = df.index[0] actual_start_time = initial_timestamp + pd.Timedelta(minutes=Td) @@ -142,6 +178,7 @@ def TrimRespondLogic( return None # start the T&R logic verification + SPtrim = abs(SPtrim) for current_timestamp, row in copied_df.iterrows(): prev_row_df = copied_df.iloc[copied_df.index.get_loc(current_timestamp) - 1] prev_row_result = result.iloc[result.index.get_loc(current_timestamp) - 1] @@ -151,7 +188,7 @@ def TrimRespondLogic( if controller_type == "direct_acting": # determine T&R logic was implemented correctly (verification) if ( - row["setpoint"] <= prev_row_df["setpoint"] + SPtrim + tol + row["setpoint"] <= prev_row_df["setpoint"] - SPtrim + tol and row["setpoint"] >= SPmin ): result.loc[current_timestamp, "verification"] = True @@ -159,7 +196,7 @@ def TrimRespondLogic( result.loc[current_timestamp, "verification"] = False # calculate setpoint by T&R logic - new_setpoint = prev_row_result["setpoint"] + SPtrim + new_setpoint = prev_row_result["setpoint"] - SPtrim result.loc[current_timestamp, "setpoint"] = ( SPmin if new_setpoint < SPmin else new_setpoint ) @@ -167,7 +204,7 @@ def TrimRespondLogic( elif controller_type == "reverse_acting": # determine T&R logic was implemented correctly (verification) if ( - row["setpoint"] <= prev_row_df["setpoint"] - SPtrim - tol + row["setpoint"] <= prev_row_df["setpoint"] + SPtrim - tol and row["setpoint"] <= SPmax ): result.loc[current_timestamp, "verification"] = True @@ -175,7 +212,7 @@ def TrimRespondLogic( result.loc[current_timestamp, "verification"] = False # calculate setpoint by T&R logic - new_setpoint = prev_row_result["setpoint"] - SPtrim + new_setpoint = prev_row_result["setpoint"] + SPtrim result.loc[current_timestamp, "setpoint"] = ( SPmax if new_setpoint >= SPmax else new_setpoint ) diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index 9210a3d0..7e2df3ac 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -3,8 +3,9 @@ import pandas as pd -sys.path.append("./src") -from library.trim_respond_logic import TrimRespondLogic +sys.path.append("./constrain") +from lib_unit_test_runner import * +from library import * # data preparation start_date = "2023-06-21 12:00:00" # Start date and time @@ -16,7 +17,14 @@ ) # fmt: off # The below data is from Figure 5.1.14.4 in the ASHRAE G36-2021 -data["setpoint"] = [0.50, 0.46, 0.42, 0.48,0.60, 0.75, 0.81, 0.77, 0.73, 0.69, 0.65, 0.61, 0.57, 0.53, 0.49, 0.45, 0.41, 0.37, 0.33, 0.29, 0.25, 0.21, 0.36, 0.51, 0.66, 0.81, 0.77, 0.73, 0.85, 0.81] +values_in_inches = [0.50, 0.46, 0.42, 0.48,0.60, 0.75, 0.81, 0.77, 0.73, 0.69, 0.65, 0.61, 0.57, 0.53, 0.49, 0.45, 0.41, 0.37, 0.33, 0.29, 0.25, 0.21, 0.36, 0.51, 0.66, 0.81, 0.77, 0.73, 0.85, 0.81] + +# Define the conversion factor +in_to_pa = 248.84 + +# Convert to Pascals +data["setpoint"] = [value * in_to_pa for value in values_in_inches] + data["number_of_requests"] = [0, 1, 2, 3, 4, 6, 3, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 1, 0, 1, 2, 6, 6, 5, 5, 2, 2, 4, 2] # fmt: on @@ -29,16 +37,17 @@ def test_check_args_type(self): with self.assertLogs() as logobs: TrimRespondLogic( dict(data), - Td="0", + Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `df` arg must be a dataframe. It cannot be .", @@ -57,8 +66,9 @@ def test_check_args_type(self): SPmin=0.15, SPmax=1.5, SPres_max=0.15, - tol=0.01, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:Index's format is not in datetime format.", @@ -69,16 +79,17 @@ def test_check_args_type(self): with self.assertLogs() as logobs: TrimRespondLogic( data.drop("setpoint", axis=1), - Td="0", + Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:setpoint column doesn't exist in the `df`.", @@ -91,14 +102,15 @@ def test_check_args_type(self): data, Td="0", ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `Td` arg must be a float or int. It cannot be .", @@ -111,14 +123,15 @@ def test_check_args_type(self): data, Td=0, ignored_requests="2", - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `ignored_requests` arg must be an int. It cannot be .", @@ -129,16 +142,17 @@ def test_check_args_type(self): with self.assertLogs() as logobs: TrimRespondLogic( data, - Td=4, + Td=0, ignored_requests=2, - SP0="0.5", - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol="0.01", + SP0="120", + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `SP0` arg must be a float or int. It cannot be .", @@ -151,14 +165,15 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim="-0.04", - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim="-10", + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `SPtrim` arg must be a float or int. It cannot be .", @@ -171,14 +186,15 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres="0.06", - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres="15", + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `SPres` arg must be a float or int. It cannot be .", @@ -191,14 +207,15 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin="0.15", - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin="37", + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `SPmin` arg must be a float or int. It cannot be .", @@ -211,14 +228,15 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax="1.5", - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax="370", + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `SPmax` arg must be a float or int. It cannot be .", @@ -231,77 +249,102 @@ def test_check_args_type(self): data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max="0.15", - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max="37", controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( "ERROR:root:The type of the `SPres_max` arg must be a float or int. It cannot be .", logobs.output[0], ) - # check `tol` + # check `controller_type` with self.assertLogs() as logobs: TrimRespondLogic( data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol="0.01", + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, + controller_type="wrong_value", + variable_type="pressure", + variable_subtype="static", + ) + self.assertEqual( + "ERROR:root:The `controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `wrong_value`.", + logobs.output[0], + ) + + # check if delayed timestamp is in the `df`'s index + with self.assertLogs() as logobs: + TrimRespondLogic( + data, + Td=0, + ignored_requests=2, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) self.assertEqual( - "ERROR:root:The type of the `tol` arg must be a float or int. It cannot be .", + "ERROR:root:The delayed timestamp must be included in the `df` timestamp.", logobs.output[0], ) - # check `controller_type` + # Check if variable_type has one of the ("temperature", "airflow", "waterflow", "pressure") values with self.assertLogs() as logobs: TrimRespondLogic( data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, - controller_type="wrong_value", + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, + controller_type="direct_acting", + variable_type="wrong_var", + variable_subtype="static", ) self.assertEqual( - "ERROR:root:`controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `wrong_value`.", + "ERROR:root:The `variable_type` arg must be one of temperature, airflow, waterflow, pressure. It can't be `wrong_var`.", logobs.output[0], ) - # check if delayed timestamp is in the `df`'s index + # Check if variable_subtype has a right subtype with self.assertLogs() as logobs: TrimRespondLogic( data, - Td=5, + Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, controller_type="direct_acting", + variable_type="temperature", + variable_subtype="no_subtype", ) self.assertEqual( - "ERROR:root:The delayed timestamp must be included in the `df` timestamp.", + "ERROR:root:The `variable_subtype` arg doesn't have a right subtype. Please check the ./constrain/tolerances.json file.", logobs.output[0], ) @@ -313,25 +356,134 @@ def test_TR_logic_verification(self): data, Td=0, ignored_requests=2, - SP0=0.5, - SPtrim=-0.04, - SPres=0.06, - SPmin=0.15, - SPmax=1.5, - SPres_max=0.15, - tol=0.01, + SP0=120, + SPtrim=-10, + SPres=15, + SPmin=37, + SPmax=370, + SPres_max=37, + controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", + ) + + # check if all verification passed + self.assertTrue(all(tr_obj["verification"])) + + def test_TR_winter_day_reset(self): + """Test winter day reset dataset from Modelica""" + + start_date = "2023-12-21 12:00:00" + + modelica_winter = pd.DataFrame( + columns=["setpoint", "number_of_requests"], + index=pd.date_range(start=start_date, periods=1441, freq="T"), + ) + + modelica_winter["setpoint"] = ( + [120] * 295 + + [108] * 3 + + [96] * 2 + + [99] * 2 + + [119] * 2 + + [139] * 2 + + [159] * 2 + + [179] * 2 + + [199] * 2 + + [219] * 2 + + [222] * 2 + + [210] * 2 + + [198] * 2 + + [186] * 2 + + [174] * 2 + + [162] * 2 + + [150] * 2 + + [138] * 2 + + [126] * 2 + + [114] * 2 + + [102] * 2 + + [90] * 2 + + [78] * 2 + + [66] * 2 + + [54] * 2 + + [42] * 2 + + [30] * 2 + + [25] * 792 + + [120] * 301 + ) + modelica_winter["number_of_requests"] = ( + [8] * 300 + + [3] * 1 + + [9] * 9 + + [8] * 2 + + [6] * 2 + + [3] * 2 + + [1] * 2 + + [0] * 1123 + ) + + # verify the verification was implemented correctly + tr_obj = TrimRespondLogic( + modelica_winter, + Td=10, # 10 mins + ignored_requests=2, + SP0=120, + SPtrim=-12, + SPres=15, + SPmin=25, + SPmax=1000, + SPres_max=32, controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) # check if all verification passed self.assertTrue(all(tr_obj["verification"])) - # check if the setpoint is in the correct range - # assume if original and calculated setpoints are within the 1% range, the results are acceptable - self.assertTrue( - all(abs(tr_obj["setpoint"] - data["setpoint"]) / data["setpoint"] <= 0.01) + def test_TR_summer_day_reset(self): + """Test summer day reset dataset from Modelica""" + + start_date = "2023-06-21 12:00:00" + + modelica_summer = pd.DataFrame( + columns=["setpoint", "number_of_requests"], + index=pd.date_range(start=start_date, periods=1441, freq="T"), + ) + + modelica_summer["setpoint"] = ( + [120] * 372 + + [108] * 2 + + [96] * 2 + + [84] * 2 + + [72] * 2 + + [60] * 2 + + [48] * 2 + + [36] * 2 + + [25] * 754 + + [120] * 301 + ) + modelica_summer["number_of_requests"] = [0] * 1441 + + # verify the verification was implemented correctly + tr_obj = TrimRespondLogic( + modelica_summer, + Td=10, # 10 mins + ignored_requests=2, + SP0=120, + SPtrim=-12, + SPres=15, + SPmin=25, + SPmax=1000, + SPres_max=32, + controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", ) + # check if all verification passed + self.assertTrue(all(tr_obj["verification"])) + if __name__ == "__main__": unittest.main() From 168f4a27e4724064d689f96d8591d31bb5dc2ac5 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Fri, 2 May 2025 14:29:31 -0700 Subject: [PATCH 09/12] Updated the logic and unit tests --- constrain/library/trim_respond_logic.py | 228 +++++++++++++----------- tests/test_trim_respond_logic.py | 39 +--- 2 files changed, 137 insertions(+), 130 deletions(-) diff --git a/constrain/library/trim_respond_logic.py b/constrain/library/trim_respond_logic.py index 4b8b92f1..17c1ec6f 100644 --- a/constrain/library/trim_respond_logic.py +++ b/constrain/library/trim_respond_logic.py @@ -39,7 +39,9 @@ def TrimRespondLogic( """Trim and respond logic verification & setpoint calculation. Args: - df (dataframe): dataframe that must include timestamp (`Date/Time` column), time-series data of setpoint (`setpoint` column), number of requests (`number_of_requests` column name), + df (dataframe): dataframe that must include timestamp (`Date/Time` column), + time-series data of setpoint (`setpoint` column), number of requests (`number_of_requests` column name), + flag_device (int or bool): flag to indicate device status (e.g., AHU). This can be integer (0: off, 1: on) or boolean (True, False), Td (float or int): Time delay in minutes. ignored_requests (int): Number of ignored requests. SP0 (float or int): Initial setpoint. @@ -56,7 +58,8 @@ def TrimRespondLogic( airflow: supply, outdoor_air, exhaust_air, general waterflow: hot, general pressure: static, building, general - Return: dataframe including verification in boolean and setpoint calculation results in each timestep. + + Return: dataframe including verification in boolean and calculated setpoint calculation results in each timestep. """ # check df type @@ -72,7 +75,7 @@ def TrimRespondLogic( return None # check df columns - for col in ("setpoint", "number_of_requests"): + for col in ("setpoint", "number_of_requests", "flag_device"): if col not in df.columns: logging.error(f"{col} column doesn't exist in the `df`.") return None @@ -101,6 +104,7 @@ def TrimRespondLogic( f"The type of the `SPtrim` arg must be a float or int. It cannot be {type(SPtrim)}." ) return None + if not isinstance(SPres, (float, int)): logging.error( f"The type of the `SPres` arg must be a float or int. It cannot be {type(SPres)}." @@ -132,7 +136,7 @@ def TrimRespondLogic( ) return None - # set tolerance + # Check tolerance input arguments if variable_type not in VARIABLE_SET: logging.error( f"The `variable_type` arg must be one of temperature, airflow, waterflow, pressure. It can't be `{variable_type}`." @@ -145,115 +149,139 @@ def TrimRespondLogic( ) return None + # Read the tolerance JSON file path_to_custom_tolerance_file = Path(__file__).parent.parent / "tolerances.json" with open(path_to_custom_tolerance_file) as f: tolerances = json.load(f) + # Define tolerance tol = tolerances[variable_type]["types"][variable_subtype] - # calculate actual start time - initial_timestamp = df.index[0] - actual_start_time = initial_timestamp + pd.Timedelta(minutes=Td) - # create "result" dataframe result = pd.DataFrame( - columns=["verification", "setpoint"], - index=df[df.index >= actual_start_time].index, + columns=["verification", "calculated_setpoint"], + index=df.index, ) - # preprocess df, cut off before Td - copied_df = df.copy()[df.index > actual_start_time] - - # setup an initial value - result.loc[actual_start_time, "verification"] = True - if initial_timestamp == actual_start_time: - result.loc[actual_start_time, "setpoint"] = SP0 - else: - try: - df.loc[actual_start_time, "setpoint"] - except KeyError: - logging.error( - f"The delayed timestamp must be included in the `df` timestamp." - ) - return None - # start the T&R logic verification SPtrim = abs(SPtrim) - for current_timestamp, row in copied_df.iterrows(): - prev_row_df = copied_df.iloc[copied_df.index.get_loc(current_timestamp) - 1] - prev_row_result = result.iloc[result.index.get_loc(current_timestamp) - 1] - - # when the number of ignored requests is greater than or equal to the number of requests - if row["number_of_requests"] <= ignored_requests: - if controller_type == "direct_acting": - # determine T&R logic was implemented correctly (verification) - if ( - row["setpoint"] <= prev_row_df["setpoint"] - SPtrim + tol - and row["setpoint"] >= SPmin - ): - result.loc[current_timestamp, "verification"] = True - else: - result.loc[current_timestamp, "verification"] = False - - # calculate setpoint by T&R logic - new_setpoint = prev_row_result["setpoint"] - SPtrim - result.loc[current_timestamp, "setpoint"] = ( - SPmin if new_setpoint < SPmin else new_setpoint - ) - - elif controller_type == "reverse_acting": - # determine T&R logic was implemented correctly (verification) - if ( - row["setpoint"] <= prev_row_df["setpoint"] + SPtrim - tol - and row["setpoint"] <= SPmax - ): - result.loc[current_timestamp, "verification"] = True - else: - result.loc[current_timestamp, "verification"] = False + loop_count = 0 + for current_timestamp, row in df.iterrows(): + if row["flag_device"] in (1, True) and loop_count != 0: + # when device is on + # Check if the device is within the last Td minute(s). If so, proceed with the trim and response logic; otherwise, skip it. + if ( + df[ + (df.index >= current_timestamp - pd.Timedelta(minutes=Td)) + & (df.index <= current_timestamp) + ]["flag_device"] + .apply(lambda x: x in [1, True]) + .all() + ): + prev_timestamp_no = df.index.get_loc(current_timestamp) - 1 + prev_row_df = df.iloc[prev_timestamp_no] + prev_row_result = result.iloc[prev_timestamp_no] + + # Extracted common variables + num_requests = row["number_of_requests"] + setpoint = row["setpoint"] + prev_setpoint = prev_row_df["setpoint"] + prev_calculated_setpoint = prev_row_result["calculated_setpoint"] + + # When the number of ignored requests is greater than or equal to the number of requests + if num_requests <= ignored_requests: + if controller_type == "direct_acting": + + # Check if the setpoint was lowered by SPtrim + result.loc[current_timestamp, "verification"] = ( + True + if ( + setpoint <= prev_setpoint - SPtrim + tol + and setpoint >= SPmin + ) + else False + ) + + # If new_setpoint is lower than SPmin, set SPmin + new_setpoint = prev_calculated_setpoint - SPtrim + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmin if new_setpoint < SPmin else new_setpoint + ) + + elif controller_type == "reverse_acting": + + # Check if the setpoint was increased by SPtrim + result.loc[current_timestamp, "verification"] = ( + True + if ( + setpoint <= prev_setpoint + SPtrim - tol + and setpoint <= SPmax + ) + else False + ) + + # If new_setpoint is greater than SPmax, set SPmax + new_setpoint = prev_calculated_setpoint + SPtrim + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmax if new_setpoint >= SPmax else new_setpoint + ) - # calculate setpoint by T&R logic - new_setpoint = prev_row_result["setpoint"] + SPtrim - result.loc[current_timestamp, "setpoint"] = ( - SPmax if new_setpoint >= SPmax else new_setpoint - ) - - else: - trim_amount = (row["number_of_requests"] - ignored_requests) * SPres - if abs(trim_amount) > abs(SPres_max): - delta = SPres_max - else: - delta = trim_amount - - if controller_type == "direct_acting": - # determine T&R logic was implemented correctly (verification) - if ( - row["setpoint"] >= prev_row_df["setpoint"] + delta - tol - and row["setpoint"] <= SPmax - ): - result.loc[current_timestamp, "verification"] = True - else: - result.loc[current_timestamp, "verification"] = False - - # calculate setpoint by T&R logic - new_setpoint = prev_row_result["setpoint"] + delta - result.loc[current_timestamp, "setpoint"] = ( - SPmax if new_setpoint > SPmax else new_setpoint - ) - - elif controller_type == "reverse_acting": - # determine T&R logic was implemented correctly (verification) - if ( - row["setpoint"] >= prev_row_df["setpoint"] - delta + tol - and row["setpoint"] >= SPmin - ): - result.loc[current_timestamp, "verification"] = True else: - result.loc[current_timestamp, "verification"] = False - - # calculate setpoint by T&R logic - new_setpoint = prev_row_result["setpoint"] - delta - result.loc[current_timestamp, "setpoint"] = ( - SPmin if new_setpoint <= SPmin else new_setpoint - ) + # When requests > ignored requests + trim_amount = (num_requests - ignored_requests) * SPres + delta = ( + SPres_max if abs(trim_amount) > abs(SPres_max) else trim_amount + ) + + if controller_type == "direct_acting": + # Check if setpoint was increased by correct amount + result.loc[current_timestamp, "verification"] = ( + True + if ( + setpoint >= prev_row_df["setpoint"] + delta - tol + and setpoint <= SPmax + ) + else False + ) + + # Calculate setpoint + new_setpoint = prev_row_result["calculated_setpoint"] + delta + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmax if new_setpoint > SPmax else new_setpoint + ) + + elif controller_type == "reverse_acting": + # Check if setpoint was increased by correct amount + result.loc[current_timestamp, "verification"] = ( + True + if ( + row["setpoint"] >= prev_setpoint - delta + tol + and row["setpoint"] >= SPmin + ) + else False + ) + + # Calculate setpoint + new_setpoint = prev_calculated_setpoint - delta + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmin if new_setpoint <= SPmin else new_setpoint + ) + else: + # when device is on, but it hasn't been on for the Td period + result.loc[ + current_timestamp, ["verification", "calculated_setpoint"] + ] = [ + "Untested", + SP0, + ] + else: + # when device is off + # When the associated device is OFF, the setpoint shall be SP0 + result.loc[current_timestamp, ["verification", "calculated_setpoint"]] = [ + "Untested", + SP0, + ] + + loop_count += 1 return result diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index 7e2df3ac..24eb04ea 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -13,7 +13,7 @@ data = pd.DataFrame( columns=["setpoint", "number_of_requests"], - index=pd.date_range(start=start_date, end=end_date, freq="2T"), + index=pd.date_range(start=start_date, end=end_date, freq="2min"), ) # fmt: off # The below data is from Figure 5.1.14.4 in the ASHRAE G36-2021 @@ -24,8 +24,8 @@ # Convert to Pascals data["setpoint"] = [value * in_to_pa for value in values_in_inches] - data["number_of_requests"] = [0, 1, 2, 3, 4, 6, 3, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 1, 0, 1, 2, 6, 6, 5, 5, 2, 2, 4, 2] +data["flag_device"] = [1 for _ in range(30)] # fmt: on @@ -285,27 +285,6 @@ def test_check_args_type(self): logobs.output[0], ) - # check if delayed timestamp is in the `df`'s index - with self.assertLogs() as logobs: - TrimRespondLogic( - data, - Td=0, - ignored_requests=2, - SP0=120, - SPtrim=-10, - SPres=15, - SPmin=37, - SPmax=370, - SPres_max=37, - controller_type="direct_acting", - variable_type="pressure", - variable_subtype="static", - ) - self.assertEqual( - "ERROR:root:The delayed timestamp must be included in the `df` timestamp.", - logobs.output[0], - ) - # Check if variable_type has one of the ("temperature", "airflow", "waterflow", "pressure") values with self.assertLogs() as logobs: TrimRespondLogic( @@ -374,10 +353,9 @@ def test_TR_winter_day_reset(self): """Test winter day reset dataset from Modelica""" start_date = "2023-12-21 12:00:00" - modelica_winter = pd.DataFrame( columns=["setpoint", "number_of_requests"], - index=pd.date_range(start=start_date, periods=1441, freq="T"), + index=pd.date_range(start=start_date, periods=1441, freq="min"), ) modelica_winter["setpoint"] = ( @@ -412,7 +390,7 @@ def test_TR_winter_day_reset(self): + [120] * 301 ) modelica_winter["number_of_requests"] = ( - [8] * 300 + [0] * 300 + [3] * 1 + [9] * 9 + [8] * 2 @@ -421,6 +399,7 @@ def test_TR_winter_day_reset(self): + [1] * 2 + [0] * 1123 ) + modelica_winter["flag_device"] = [0] * 283 + [1] * 857 + [0] * 301 # verify the verification was implemented correctly tr_obj = TrimRespondLogic( @@ -445,10 +424,9 @@ def test_TR_summer_day_reset(self): """Test summer day reset dataset from Modelica""" start_date = "2023-06-21 12:00:00" - modelica_summer = pd.DataFrame( - columns=["setpoint", "number_of_requests"], - index=pd.date_range(start=start_date, periods=1441, freq="T"), + columns=["setpoint", "number_of_requests", "flag_device"], + index=pd.date_range(start=start_date, periods=1441, freq="min"), ) modelica_summer["setpoint"] = ( @@ -464,11 +442,12 @@ def test_TR_summer_day_reset(self): + [120] * 301 ) modelica_summer["number_of_requests"] = [0] * 1441 + modelica_summer["flag_device"] = [0] * 360 + [1] * 780 + [0] * 301 # verify the verification was implemented correctly tr_obj = TrimRespondLogic( modelica_summer, - Td=10, # 10 mins + Td=10, ignored_requests=2, SP0=120, SPtrim=-12, From 346ad856c5f345fd530450292df521448338c53b Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Tue, 6 May 2025 08:46:26 -0700 Subject: [PATCH 10/12] Updated the logic --- constrain/library/trim_respond_logic.py | 237 ++++++++++++++---------- tests/test_trim_respond_logic.py | 102 ++-------- 2 files changed, 155 insertions(+), 184 deletions(-) diff --git a/constrain/library/trim_respond_logic.py b/constrain/library/trim_respond_logic.py index 17c1ec6f..09b5cd47 100644 --- a/constrain/library/trim_respond_logic.py +++ b/constrain/library/trim_respond_logic.py @@ -22,6 +22,30 @@ } +def _assgin_value( + result_df: pd.DataFrame, + current_timestamp: pd.Timestamp, + col_value_pair: dict[str, str], +) -> pd.DataFrame: + """ + Assigns values to specified columns in a DataFrame at a given timestamp. + + Args: + result_df (pd.DataFrame): The DataFrame where the values will be assigned. + current_timestamp (pd.Timestamp): The timestamp at which the values should be set. + col_value_pair (dict[str, str]): A dictionary where keys are column names and values are the corresponding values to be assigned. + + Returns: + pd.DataFrame: The updated DataFrame with assigned values at the specified timestamp. + + """ + + for col_name, value in col_value_pair.items(): + result_df.loc[current_timestamp, col_name] = value + + return result_df + + def TrimRespondLogic( df: pd.DataFrame, Td: Union[float, int], @@ -62,25 +86,25 @@ def TrimRespondLogic( Return: dataframe including verification in boolean and calculated setpoint calculation results in each timestep. """ - # check df type + # Check df type if not isinstance(df, pd.DataFrame): logging.error( f"The type of the `df` arg must be a dataframe. It cannot be {type(df)}." ) return None - # check df index type + # Check df index type if not is_datetime64_any_dtype(df.index): logging.error(f"Index's format is not in datetime format.") return None - # check df columns + # Check df columns for col in ("setpoint", "number_of_requests", "flag_device"): if col not in df.columns: logging.error(f"{col} column doesn't exist in the `df`.") return None - # check the given arguments type + # Check the given arguments type if not isinstance(Td, (float, int)): logging.error( f"The type of the `Td` arg must be a float or int. It cannot be {type(Td)}." @@ -129,7 +153,7 @@ def TrimRespondLogic( ) return None - # check if the `controller_type` is either `direct_acting` or `reverse_acting` + # Check if the `controller_type` is either `direct_acting` or `reverse_acting` if controller_type not in ("direct_acting", "reverse_acting"): logging.error( f"The `controller_type` arg must be either `direct_acting` or `reverse_acting`. It can't be `{controller_type}`." @@ -157,130 +181,143 @@ def TrimRespondLogic( # Define tolerance tol = tolerances[variable_type]["types"][variable_subtype] - # create "result" dataframe + # Create "result" dataframe result = pd.DataFrame( columns=["verification", "calculated_setpoint"], index=df.index, ) - # start the T&R logic verification + # Start the T&R logic verification SPtrim = abs(SPtrim) loop_count = 0 + TR_logic_start = False for current_timestamp, row in df.iterrows(): if row["flag_device"] in (1, True) and loop_count != 0: - # when device is on + # When device is on # Check if the device is within the last Td minute(s). If so, proceed with the trim and response logic; otherwise, skip it. if ( df[ (df.index >= current_timestamp - pd.Timedelta(minutes=Td)) & (df.index <= current_timestamp) ]["flag_device"] - .apply(lambda x: x in [1, True]) + .apply(lambda x: x in (1, True)) .all() ): - prev_timestamp_no = df.index.get_loc(current_timestamp) - 1 - prev_row_df = df.iloc[prev_timestamp_no] - prev_row_result = result.iloc[prev_timestamp_no] - - # Extracted common variables - num_requests = row["number_of_requests"] - setpoint = row["setpoint"] - prev_setpoint = prev_row_df["setpoint"] - prev_calculated_setpoint = prev_row_result["calculated_setpoint"] - - # When the number of ignored requests is greater than or equal to the number of requests - if num_requests <= ignored_requests: - if controller_type == "direct_acting": - - # Check if the setpoint was lowered by SPtrim - result.loc[current_timestamp, "verification"] = ( - True - if ( - setpoint <= prev_setpoint - SPtrim + tol - and setpoint >= SPmin + if TR_logic_start: + prev_timestamp_no = df.index.get_loc(current_timestamp) - 1 + prev_row_df = df.iloc[prev_timestamp_no] + prev_row_result = result.iloc[prev_timestamp_no] + + # Extracted common variables + num_requests = row["number_of_requests"] + setpoint = row["setpoint"] + prev_setpoint = prev_row_df["setpoint"] + prev_calculated_setpoint = prev_row_result["calculated_setpoint"] + + # When the number of ignored requests is greater than or equal to the number of requests + if num_requests <= ignored_requests: + if controller_type == "direct_acting": + # Check if the setpoint was lowered by SPtrim + result.loc[current_timestamp, "verification"] = False + expected_setpoint = prev_setpoint - SPtrim + tol + if expected_setpoint > SPmin: + if setpoint <= expected_setpoint and setpoint >= SPmin: + result.loc[current_timestamp, "verification"] = True + elif setpoint == SPmin: + result.loc[current_timestamp, "verification"] = True + + # If new_setpoint is lower than SPmin, set SPmin + new_setpoint = prev_calculated_setpoint - SPtrim + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmin if new_setpoint < SPmin else new_setpoint ) - else False - ) - # If new_setpoint is lower than SPmin, set SPmin - new_setpoint = prev_calculated_setpoint - SPtrim - result.loc[current_timestamp, "calculated_setpoint"] = ( - SPmin if new_setpoint < SPmin else new_setpoint - ) - - elif controller_type == "reverse_acting": - - # Check if the setpoint was increased by SPtrim - result.loc[current_timestamp, "verification"] = ( - True - if ( - setpoint <= prev_setpoint + SPtrim - tol - and setpoint <= SPmax + elif controller_type == "reverse_acting": + # Check if the setpoint was increased by SPtrim + result.loc[current_timestamp, "verification"] = False + expected_setpoint = prev_setpoint + SPtrim + tol + if expected_setpoint > setpoint: + if setpoint <= expected_setpoint and setpoint <= SPmax: + result.loc[current_timestamp, "verification"] = True + else: + if setpoint == SPmax: + result.loc[current_timestamp, "verification"] = True + + # If new_setpoint is greater than SPmax, set SPmax + new_setpoint = prev_calculated_setpoint + SPtrim + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmax if new_setpoint >= SPmax else new_setpoint ) - else False - ) - # If new_setpoint is greater than SPmax, set SPmax - new_setpoint = prev_calculated_setpoint + SPtrim - result.loc[current_timestamp, "calculated_setpoint"] = ( - SPmax if new_setpoint >= SPmax else new_setpoint + else: + # When requests > ignored requests + trim_amount = (num_requests - ignored_requests) * SPres + delta = ( + SPres_max + if abs(trim_amount) > abs(SPres_max) + else trim_amount ) - else: - # When requests > ignored requests - trim_amount = (num_requests - ignored_requests) * SPres - delta = ( - SPres_max if abs(trim_amount) > abs(SPres_max) else trim_amount - ) - - if controller_type == "direct_acting": - # Check if setpoint was increased by correct amount - result.loc[current_timestamp, "verification"] = ( - True - if ( - setpoint >= prev_row_df["setpoint"] + delta - tol - and setpoint <= SPmax + if controller_type == "direct_acting": + result.loc[current_timestamp, "verification"] = False + expected_setpoint = prev_row_df["setpoint"] + delta - tol + if expected_setpoint <= SPmax: + if setpoint >= expected_setpoint and setpoint <= SPmax: + result.loc[current_timestamp, "verification"] = True + else: + if setpoint == SPmin: + result.loc[current_timestamp, "verification"] = True + + # Calculate setpoint + new_setpoint = ( + prev_row_result["calculated_setpoint"] + delta ) - else False - ) - - # Calculate setpoint - new_setpoint = prev_row_result["calculated_setpoint"] + delta - result.loc[current_timestamp, "calculated_setpoint"] = ( - SPmax if new_setpoint > SPmax else new_setpoint - ) - - elif controller_type == "reverse_acting": - # Check if setpoint was increased by correct amount - result.loc[current_timestamp, "verification"] = ( - True - if ( - row["setpoint"] >= prev_setpoint - delta + tol - and row["setpoint"] >= SPmin + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmax if new_setpoint >= SPmax else new_setpoint ) - else False - ) - # Calculate setpoint - new_setpoint = prev_calculated_setpoint - delta - result.loc[current_timestamp, "calculated_setpoint"] = ( - SPmin if new_setpoint <= SPmin else new_setpoint - ) + elif controller_type == "reverse_acting": + # Check if setpoint was increased by correct amount + + result.loc[current_timestamp, "verification"] = False + expected_setpoint = prev_setpoint - delta - tol + if expected_setpoint > setpoint: + if setpoint >= expected_setpoint and setpoint >= SPmin: + result.loc[current_timestamp, "verification"] = True + else: + if setpoint == SPmin: + result.loc[current_timestamp, "verification"] = True + + # Calculate setpoint + new_setpoint = prev_calculated_setpoint - delta + result.loc[current_timestamp, "calculated_setpoint"] = ( + SPmin if new_setpoint <= SPmin else new_setpoint + ) + else: + TR_logic_start = True + _assgin_value( + result, + current_timestamp, + {"verification": "Untested", "calculated_setpoint": SP0}, + ) else: - # when device is on, but it hasn't been on for the Td period - result.loc[ - current_timestamp, ["verification", "calculated_setpoint"] - ] = [ - "Untested", - SP0, - ] + # When device is on, but it hasn't been on for the Td period + _assgin_value( + result, + current_timestamp, + {"verification": "Untested", "calculated_setpoint": SP0}, + ) + + TR_logic_start = False + else: - # when device is off + # When device is off # When the associated device is OFF, the setpoint shall be SP0 - result.loc[current_timestamp, ["verification", "calculated_setpoint"]] = [ - "Untested", - SP0, - ] + _assgin_value( + result, + current_timestamp, + {"verification": "Untested", "calculated_setpoint": SP0}, + ) loop_count += 1 diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index 24eb04ea..0b693ef0 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -31,7 +31,7 @@ class TestTrimRespond(unittest.TestCase): def test_check_args_type(self): - """Test whether arguments' type is correct.""" + """Test if argument types are correct.""" # check `data` (type) with self.assertLogs() as logobs: @@ -328,7 +328,7 @@ def test_check_args_type(self): ) def test_TR_logic_verification(self): - """test whether the T&R logic was implemented correctly.""" + """Test if the T&R logic was implemented correctly.""" # verify the verification was implemented correctly tr_obj = TrimRespondLogic( @@ -349,54 +349,31 @@ def test_TR_logic_verification(self): # check if all verification passed self.assertTrue(all(tr_obj["verification"])) - def test_TR_winter_day_reset(self): - """Test winter day reset dataset from Modelica""" + def test_TR_winter_day_reset_singlesample(self): + """Test the winter day reset dataset from Modelica""" start_date = "2023-12-21 12:00:00" modelica_winter = pd.DataFrame( - columns=["setpoint", "number_of_requests"], + columns=["setpoint", "number_of_requests", "flag_device"], index=pd.date_range(start=start_date, periods=1441, freq="min"), ) - + # fmt: off modelica_winter["setpoint"] = ( - [120] * 295 - + [108] * 3 - + [96] * 2 - + [99] * 2 - + [119] * 2 - + [139] * 2 - + [159] * 2 - + [179] * 2 - + [199] * 2 - + [219] * 2 - + [222] * 2 - + [210] * 2 - + [198] * 2 - + [186] * 2 - + [174] * 2 - + [162] * 2 - + [150] * 2 - + [138] * 2 - + [126] * 2 - + [114] * 2 - + [102] * 2 - + [90] * 2 - + [78] * 2 - + [66] * 2 - + [54] * 2 - + [42] * 2 - + [30] * 2 - + [25] * 792 + [120] * 294 + + [ + 108, 96, 84, 72, 60, 48, 51, 71, 91, 111, 131, 151, 171, 191, 211, 231, + 251, 271, 291, 311, 314, 317, 305, 293, 281, 269, 257, 245, 233, 221, + 209, 197, 185, 173, 161, 149, 137, 125, 113, 101, 89, 29, 77, 65, 53, 41, + ] + + [25] * 800 + [120] * 301 ) + # fmt: on modelica_winter["number_of_requests"] = ( [0] * 300 + [3] * 1 - + [9] * 9 - + [8] * 2 - + [6] * 2 - + [3] * 2 - + [1] * 2 + + [9] * 3 + + [10, 12, 11, 11, 11, 11, 10, 10, 7, 7, 3, 3, 2, 2] + [0] * 1123 ) modelica_winter["flag_device"] = [0] * 283 + [1] * 857 + [0] * 301 @@ -417,51 +394,8 @@ def test_TR_winter_day_reset(self): variable_subtype="static", ) - # check if all verification passed - self.assertTrue(all(tr_obj["verification"])) - - def test_TR_summer_day_reset(self): - """Test summer day reset dataset from Modelica""" - - start_date = "2023-06-21 12:00:00" - modelica_summer = pd.DataFrame( - columns=["setpoint", "number_of_requests", "flag_device"], - index=pd.date_range(start=start_date, periods=1441, freq="min"), - ) - - modelica_summer["setpoint"] = ( - [120] * 372 - + [108] * 2 - + [96] * 2 - + [84] * 2 - + [72] * 2 - + [60] * 2 - + [48] * 2 - + [36] * 2 - + [25] * 754 - + [120] * 301 - ) - modelica_summer["number_of_requests"] = [0] * 1441 - modelica_summer["flag_device"] = [0] * 360 + [1] * 780 + [0] * 301 - - # verify the verification was implemented correctly - tr_obj = TrimRespondLogic( - modelica_summer, - Td=10, - ignored_requests=2, - SP0=120, - SPtrim=-12, - SPres=15, - SPmin=25, - SPmax=1000, - SPres_max=32, - controller_type="direct_acting", - variable_type="pressure", - variable_subtype="static", - ) - - # check if all verification passed - self.assertTrue(all(tr_obj["verification"])) + # check if all verification result + # self.assertTrue(all(tr_obj["verification"])) if __name__ == "__main__": From 5c26467152cd6c0c7a5b178dd47b2f36354f3506 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 4 Jun 2025 12:17:18 -0700 Subject: [PATCH 11/12] Added supply air temp unit tests --- constrain/library/trim_respond_logic.py | 49 ++--- tests/test_trim_respond_logic.py | 261 ++++++++++++++++++++++-- 2 files changed, 267 insertions(+), 43 deletions(-) diff --git a/constrain/library/trim_respond_logic.py b/constrain/library/trim_respond_logic.py index 09b5cd47..fa24226e 100644 --- a/constrain/library/trim_respond_logic.py +++ b/constrain/library/trim_respond_logic.py @@ -187,8 +187,12 @@ def TrimRespondLogic( index=df.index, ) - # Start the T&R logic verification + # make sure the sign is consistent SPtrim = abs(SPtrim) + SPres = abs(SPres) + SPres_max = abs(SPres_max) + + # Start the T&R logic verification loop_count = 0 TR_logic_start = False for current_timestamp, row in df.iterrows(): @@ -217,6 +221,7 @@ def TrimRespondLogic( # When the number of ignored requests is greater than or equal to the number of requests if num_requests <= ignored_requests: if controller_type == "direct_acting": + # Check if the setpoint was lowered by SPtrim result.loc[current_timestamp, "verification"] = False expected_setpoint = prev_setpoint - SPtrim + tol @@ -233,29 +238,27 @@ def TrimRespondLogic( ) elif controller_type == "reverse_acting": - # Check if the setpoint was increased by SPtrim + + # Check if the setpoint increased by SPtrim result.loc[current_timestamp, "verification"] = False - expected_setpoint = prev_setpoint + SPtrim + tol - if expected_setpoint > setpoint: - if setpoint <= expected_setpoint and setpoint <= SPmax: - result.loc[current_timestamp, "verification"] = True - else: - if setpoint == SPmax: + expected_setpoint = prev_setpoint + SPtrim - tol + if expected_setpoint < SPmax: + if setpoint > expected_setpoint and setpoint <= SPmax: result.loc[current_timestamp, "verification"] = True + elif setpoint == SPmax: + result.loc[current_timestamp, "verification"] = True # If new_setpoint is greater than SPmax, set SPmax new_setpoint = prev_calculated_setpoint + SPtrim result.loc[current_timestamp, "calculated_setpoint"] = ( - SPmax if new_setpoint >= SPmax else new_setpoint + SPmax if new_setpoint > SPmax else new_setpoint ) else: # When requests > ignored requests trim_amount = (num_requests - ignored_requests) * SPres delta = ( - SPres_max - if abs(trim_amount) > abs(SPres_max) - else trim_amount + SPres_max if abs(trim_amount) > SPres_max else trim_amount ) if controller_type == "direct_acting": @@ -264,34 +267,32 @@ def TrimRespondLogic( if expected_setpoint <= SPmax: if setpoint >= expected_setpoint and setpoint <= SPmax: result.loc[current_timestamp, "verification"] = True - else: - if setpoint == SPmin: - result.loc[current_timestamp, "verification"] = True + elif setpoint == SPmin: + result.loc[current_timestamp, "verification"] = True # Calculate setpoint new_setpoint = ( prev_row_result["calculated_setpoint"] + delta ) result.loc[current_timestamp, "calculated_setpoint"] = ( - SPmax if new_setpoint >= SPmax else new_setpoint + SPmax if new_setpoint > SPmax else new_setpoint ) elif controller_type == "reverse_acting": - # Check if setpoint was increased by correct amount + # Check if setpoint increased by correct amount result.loc[current_timestamp, "verification"] = False - expected_setpoint = prev_setpoint - delta - tol - if expected_setpoint > setpoint: - if setpoint >= expected_setpoint and setpoint >= SPmin: - result.loc[current_timestamp, "verification"] = True - else: - if setpoint == SPmin: + expected_setpoint = prev_setpoint - delta + tol + if expected_setpoint > SPmin: + if setpoint <= expected_setpoint and setpoint >= SPmin: result.loc[current_timestamp, "verification"] = True + elif setpoint == SPmin: + result.loc[current_timestamp, "verification"] = True # Calculate setpoint new_setpoint = prev_calculated_setpoint - delta result.loc[current_timestamp, "calculated_setpoint"] = ( - SPmin if new_setpoint <= SPmin else new_setpoint + SPmin if new_setpoint < SPmin else new_setpoint ) else: TR_logic_start = True diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py index 0b693ef0..f9dbc729 100644 --- a/tests/test_trim_respond_logic.py +++ b/tests/test_trim_respond_logic.py @@ -1,6 +1,7 @@ import sys import unittest +import numpy as np import pandas as pd sys.path.append("./constrain") @@ -8,25 +9,85 @@ from library import * # data preparation -start_date = "2023-06-21 12:00:00" # Start date and time -end_date = "2023-06-21 12:59:59" # End date and time +start_date = "2023-06-21 12:00:00" +end_date = "2023-06-21 12:59:59" data = pd.DataFrame( columns=["setpoint", "number_of_requests"], index=pd.date_range(start=start_date, end=end_date, freq="2min"), ) -# fmt: off # The below data is from Figure 5.1.14.4 in the ASHRAE G36-2021 -values_in_inches = [0.50, 0.46, 0.42, 0.48,0.60, 0.75, 0.81, 0.77, 0.73, 0.69, 0.65, 0.61, 0.57, 0.53, 0.49, 0.45, 0.41, 0.37, 0.33, 0.29, 0.25, 0.21, 0.36, 0.51, 0.66, 0.81, 0.77, 0.73, 0.85, 0.81] +values_in_inches = [ + 0.50, + 0.46, + 0.42, + 0.48, + 0.60, + 0.75, + 0.81, + 0.77, + 0.73, + 0.69, + 0.65, + 0.61, + 0.57, + 0.53, + 0.49, + 0.45, + 0.41, + 0.37, + 0.33, + 0.29, + 0.25, + 0.21, + 0.36, + 0.51, + 0.66, + 0.81, + 0.77, + 0.73, + 0.85, + 0.81, +] # Define the conversion factor in_to_pa = 248.84 # Convert to Pascals -data["setpoint"] = [value * in_to_pa for value in values_in_inches] -data["number_of_requests"] = [0, 1, 2, 3, 4, 6, 3, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 1, 0, 1, 2, 6, 6, 5, 5, 2, 2, 4, 2] +data["setpoint"] = [value * in_to_pa for value in values_in_inches] +data["number_of_requests"] = [ + 0, + 1, + 2, + 3, + 4, + 6, + 3, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 1, + 0, + 1, + 2, + 6, + 6, + 5, + 5, + 2, + 2, + 4, + 2, +] data["flag_device"] = [1 for _ in range(30)] -# fmt: on class TestTrimRespond(unittest.TestCase): @@ -352,29 +413,83 @@ def test_TR_logic_verification(self): def test_TR_winter_day_reset_singlesample(self): """Test the winter day reset dataset from Modelica""" - start_date = "2023-12-21 12:00:00" modelica_winter = pd.DataFrame( columns=["setpoint", "number_of_requests", "flag_device"], - index=pd.date_range(start=start_date, periods=1441, freq="min"), + index=pd.date_range(start="2023-12-21 12:00:00", periods=1441, freq="min"), ) - # fmt: off + modelica_winter["setpoint"] = ( [120] * 294 + [ - 108, 96, 84, 72, 60, 48, 51, 71, 91, 111, 131, 151, 171, 191, 211, 231, - 251, 271, 291, 311, 314, 317, 305, 293, 281, 269, 257, 245, 233, 221, - 209, 197, 185, 173, 161, 149, 137, 125, 113, 101, 89, 29, 77, 65, 53, 41, + 108, + 96, + 84, + 72, + 60, + 48, + 63, + 95, + 127, + 159, + 191, + 223, + 255, + 287, + 319, + 351, + 383, + 415, + 430, + 445, + 433, + 421, + 409, + 397, + 385, + 373, + 361, + 349, + 337, + 325, + 313, + 301, + 289, + 277, + 265, + 253, + 241, + 229, + 217, + 205, + 193, + 181, + 169, + 157, + 145, + 133, + 121, + 109, + 97, + 85, + 73, + 61, + 49, + 37, ] - + [25] * 800 + + [25] * 792 + [120] * 301 ) - # fmt: on + modelica_winter["number_of_requests"] = ( [0] * 300 + [3] * 1 + [9] * 3 - + [10, 12, 11, 11, 11, 11, 10, 10, 7, 7, 3, 3, 2, 2] - + [0] * 1123 + + [10, 12] + + [11] * 2 + + [9] * 2 + + [6] * 2 + + [3] * 2 + + [0] * 1127 ) modelica_winter["flag_device"] = [0] * 283 + [1] * 857 + [0] * 301 @@ -394,8 +509,116 @@ def test_TR_winter_day_reset_singlesample(self): variable_subtype="static", ) - # check if all verification result - # self.assertTrue(all(tr_obj["verification"])) + # check if there are only "Untested" or True in the `verification` column + self.assertTrue( + set(tr_obj["verification"].unique()).issubset({"Untested", True}) + ) + + # check if the given and calculated setpoints are within 1% diff + percent_diff = ( + np.abs(modelica_winter["setpoint"] - tr_obj["calculated_setpoint"]) + <= 0.01 * modelica_winter["setpoint"] + ).all() + self.assertTrue(percent_diff) + + def test_TR_summer_day_reset_singlesample(self): + """Test the summer day reset dataset from Modelica""" + + modelica_summer = pd.DataFrame( + columns=["setpoint", "number_of_requests", "flag_device"], + index=pd.date_range(start="2023-06-21 12:00:00", periods=2161, freq="min"), + ) + modelica_summer["setpoint"] = ( + [291.15] * 1845 + + [ + 290.95, + 290.75, + 290.55, + 290.35, + 290.15, + 289.95, + 289.75, + 289.55, + 289.35, + 289.15, + 288.95, + 288.55, + 288.15, + 287.75, + 287.35, + 286.95, + 286.55, + 286.15, + 285.75, + 285.35, + ] + + [285.15] * 132 + + [ + 285.25, + 285.35, + 285.45, + 285.55, + 285.65, + 285.75, + 285.85, + 285.95, + 286.05, + 286.15, + 286.25, + 286.35, + 286.45, + ] + + [291.15] * 151 + ) + modelica_summer["number_of_requests"] = ( + [0] * 1177 + + [1] * 3 + + [0] * 35 + + [1] * 72 + + [0] * 422 + + [1] * 91 + + [2] * 45 + + [3] * 11 + + [4] * 36 + + [5] * 79 + + [4] * 26 + + [2] * 3 + + [1] * 7 + + [0] * 154 + ) + modelica_summer["flag_device"] = ( + [0] * 172 + + [1] * 398 + + [0] * 330 + + [1] * 390 + + [0] * 304 + + [1] * 416 + + [0] * 151 + ) + tr_obj = TrimRespondLogic( + modelica_summer, + Td=10, # 10 mins + ignored_requests=2, + SP0=291.15, + SPtrim=0.1, + SPres=-0.2, + SPmin=285.15, + SPmax=291.15, + SPres_max=-0.6, + controller_type="reverse_acting", + variable_type="temperature", + variable_subtype="general", + ) + + # check if there are only "Untested" or True in the `verification` column + self.assertTrue(set(tr_obj["verification"]).issubset({"Untested", True})) + + # check if the given and calculated setpoints are within 1% diff + percent_diff = ( + np.abs(modelica_summer["setpoint"] - tr_obj["calculated_setpoint"]) + <= 0.01 * modelica_summer["setpoint"] + ).all() + self.assertTrue(percent_diff) if __name__ == "__main__": From a2a9e55a5b209fdecc1fe26f9b6be1fbbb6fa3b2 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Wed, 4 Jun 2025 12:36:30 -0700 Subject: [PATCH 12/12] Added a docstring --- constrain/library/trim_respond_logic.py | 106 ++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/constrain/library/trim_respond_logic.py b/constrain/library/trim_respond_logic.py index fa24226e..f3c40e43 100644 --- a/constrain/library/trim_respond_logic.py +++ b/constrain/library/trim_respond_logic.py @@ -1,3 +1,109 @@ +""" +### Description + +Trim & Respond (T&R) logic resets a setpoint for pressure, temperature, or other variables at an air handler or plant. +It reduces the setpoint at a fixed rate until a downstream zone is no longer satisfied and generates a request. + +### Code requirement + +- Code Name: ASHRAE Guideline 36 +- Code Year: 2021 +- Code Section: 5.1.14 Trim & Respond Set-Point Reset Logic + +### Verification Approach + +The verification checks that the setpoint adjustments follow the trim and respond logic rules. When the number of requests is less than or equal to the ignored requests, the setpoint should be trimmed. +When the number of requests exceeds the ignored requests, the setpoint should be adjusted in response to the requests, with the adjustment proportional to the number of requests. + +### Verification Applicability + +- Building Type(s): any +- Space Type(s): any +- System(s): HVAC systems with trim and respond control logic +- Climate Zone(s): any +- Component(s): air handling units, VAV boxes, static pressure control, supply air temperature control + +### Verification Algorithm Pseudo Code + +``` +if device_on and has_been_on_for_time_delay: + if number_of_requests <= ignored_requests: + if controller_type == "direct_acting": + verify setpoint <= previous_setpoint - trim_amount + tolerance + else: # reverse_acting + verify setpoint >= previous_setpoint + trim_amount - tolerance + else: # number_of_requests > ignored_requests + response_amount = (number_of_requests - ignored_requests) * respond_amount + if response_amount > max_response: + response_amount = max_response + + if controller_type == "direct_acting": + verify setpoint >= previous_setpoint + response_amount - tolerance + else: # reverse_acting + verify setpoint <= previous_setpoint - response_amount + tolerance +else: + return "Untested" +``` + +### Data requirements + +- setpoint: control setpoint + - Data Value Unit: varies (temperature, pressure, flow rate) + - Data Point Affiliation: System operation + +- number_of_requests: Number of requests + - Data Value Unit: count + - Data Point Affiliation: System operation + +- ignored_requests: Number of ignored requests + - Data Value Unit: count + - Data Point Affiliation: System operation + +- flag_device: Device operational status + - Data Value Unit: binary + - Data Point Affiliation: System operation + +- Td: Time delay in minutes + - Data Value Unit: minute + - Data Point Affiliation: System operation + +- SP0: Initial setpoint + - Data Value Unit: varies (temperature, pressure, flow rate) + - Data Point Affiliation: System operation + +- SPtrim: Trim amount + - Data Value Unit: varies (temperature, pressure, flow rate) + - Data Point Affiliation: System operation + +- SPres: Respond amount + - Data Value Unit: varies (temperature, pressure, flow rate) + - Data Point Affiliation: System operation + +- SPmin: Minimum setpoint + - Data Value Unit: varies (temperature, pressure, flow rate) + - Data Point Affiliation: System operation + +- SPmax: Maximum setpoint + - Data Value Unit: varies (temperature, pressure, flow rate) + - Data Point Affiliation: System operation + +- SPres_max: Maximum response per time interval + - Data Value Unit: varies (temperature, pressure, flow rate) + - Data Point Affiliation: System operation + +- controller_type: Type of controller (either `direct_acting` or `reverse_acting`) + - Data Value Unit: None + - Data Point Affiliation: System operation + +- variable_type: Type of variable to determine tolerance + - Data Value Unit: None + - Data Point Affiliation: System operation + +- variable_subtype: Variable subtype to determine tolerance + - Data Value Unit: None + - Data Point Affiliation: System operation +""" + import json import logging from pathlib import Path