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 new file mode 100644 index 00000000..f3c40e43 --- /dev/null +++ b/constrain/library/trim_respond_logic.py @@ -0,0 +1,431 @@ +""" +### 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 +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 _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], + ignored_requests: int, + SP0: Union[float, int], + SPtrim: Union[float, int], + SPres: Union[float, int], + SPmin: Union[float, int], + SPmax: Union[float, int], + SPres_max: Union[float, int], + controller_type: str, + variable_type: str, + variable_subtype: str, +) -> Union[None, pd.DataFrame]: + """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), + 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. + 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. + 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 calculated setpoint calculation results in each timestep. + """ + + # 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 + 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", "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 + 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)}." + ) + return None + + if not isinstance(ignored_requests, int): + logging.error( + f"The type of the `ignored_requests` arg must be an int. It cannot be {type(ignored_requests)}." + ) + 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(SP0)}." + ) + 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)}." + ) + 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, 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 `SPmax` arg must be a float or int. It cannot be {type(SPmax)}." + ) + return None + + if not isinstance(SPres_max, (float, int)): + logging.error( + f"The type of the `SPres_max` arg must be a float or int. It cannot be {type(SPres_max)}." + ) + return None + + # 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}`." + ) + return None + + # 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}`." + ) + return None + + if variable_subtype not in VARIABLE_SUBTYPE_SET.get(variable_type): + logging.error( + f"The `variable_subtype` arg doesn't have a right subtype. Please check the ./constrain/tolerances.json file." + ) + 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] + + # Create "result" dataframe + result = pd.DataFrame( + columns=["verification", "calculated_setpoint"], + index=df.index, + ) + + # 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(): + 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() + ): + 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 + ) + + elif controller_type == "reverse_acting": + + # Check if the setpoint increased by SPtrim + result.loc[current_timestamp, "verification"] = False + 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 + ) + + else: + # When requests > ignored requests + trim_amount = (num_requests - ignored_requests) * SPres + delta = ( + SPres_max if abs(trim_amount) > SPres_max else trim_amount + ) + + 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 + 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 + ) + + elif controller_type == "reverse_acting": + + # Check if setpoint increased by correct amount + result.loc[current_timestamp, "verification"] = False + 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 + ) + 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 + _assgin_value( + result, + current_timestamp, + {"verification": "Untested", "calculated_setpoint": SP0}, + ) + + TR_logic_start = False + + else: + # When device is off + # When the associated device is OFF, the setpoint shall be SP0 + _assgin_value( + result, + current_timestamp, + {"verification": "Untested", "calculated_setpoint": SP0}, + ) + + loop_count += 1 + + return result diff --git a/tests/test_trim_respond_logic.py b/tests/test_trim_respond_logic.py new file mode 100644 index 00000000..f9dbc729 --- /dev/null +++ b/tests/test_trim_respond_logic.py @@ -0,0 +1,625 @@ +import sys +import unittest + +import numpy as np +import pandas as pd + +sys.path.append("./constrain") +from lib_unit_test_runner import * +from library import * + +# data preparation +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"), +) +# 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, +] + +# 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["flag_device"] = [1 for _ in range(30)] + + +class TestTrimRespond(unittest.TestCase): + def test_check_args_type(self): + """Test if argument types are correct.""" + + # check `data` (type) + with self.assertLogs() as logobs: + TrimRespondLogic( + dict(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 `df` arg must be a dataframe. It cannot be .", + 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, + controller_type="direct_acting", + variable_type="pressure", + variable_subtype="static", + ) + 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=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`.", + logobs.output[0], + ) + + # check `Td` + 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 `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", + 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 .", + logobs.output[0], + ) + + # check `SP0` + 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 `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=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 .", + logobs.output[0], + ) + + # check `SPres` + 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 `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, + 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 .", + logobs.output[0], + ) + + # check `SPmax` + 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 `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, + 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 `controller_type` + 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="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 variable_type has one of the ("temperature", "airflow", "waterflow", "pressure") values + 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="wrong_var", + variable_subtype="static", + ) + self.assertEqual( + "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 variable_subtype has a right subtype + 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="temperature", + variable_subtype="no_subtype", + ) + self.assertEqual( + "ERROR:root:The `variable_subtype` arg doesn't have a right subtype. Please check the ./constrain/tolerances.json file.", + logobs.output[0], + ) + + def test_TR_logic_verification(self): + """Test if the T&R logic was implemented correctly.""" + + # verify the verification was implemented correctly + tr_obj = 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", + ) + + # check if all verification passed + self.assertTrue(all(tr_obj["verification"])) + + def test_TR_winter_day_reset_singlesample(self): + """Test the winter day reset dataset from Modelica""" + + modelica_winter = pd.DataFrame( + columns=["setpoint", "number_of_requests", "flag_device"], + index=pd.date_range(start="2023-12-21 12:00:00", periods=1441, freq="min"), + ) + + modelica_winter["setpoint"] = ( + [120] * 294 + + [ + 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] * 792 + + [120] * 301 + ) + + modelica_winter["number_of_requests"] = ( + [0] * 300 + + [3] * 1 + + [9] * 3 + + [10, 12] + + [11] * 2 + + [9] * 2 + + [6] * 2 + + [3] * 2 + + [0] * 1127 + ) + modelica_winter["flag_device"] = [0] * 283 + [1] * 857 + [0] * 301 + + # 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 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__": + unittest.main()