From dd0b15fa17c5b57182e064bab97e44c394c73d78 Mon Sep 17 00:00:00 2001 From: Taoning Wang Date: Thu, 19 Oct 2023 14:50:25 -0700 Subject: [PATCH 1/9] add _request_variable_from_callback() to EnergyPlusSetup to automatically request variables --- frads/eplus.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/frads/eplus.py b/frads/eplus.py index f4adcf7..a5ec8e8 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -4,10 +4,10 @@ from datetime import datetime, timedelta import json -import os from pathlib import Path from typing import List, Optional, Callable, Union -from tempfile import TemporaryDirectory +import inspect +import ast from epmodel import epmodel as epm import epmodel @@ -593,9 +593,27 @@ def set_callback(self, method_name: str, func: Callable): raise AttributeError( f"Method {method_name} not found in EnergyPlus runtime API." ) + + self._request_variable_from_callback(func) + # method(self.state, partial(func, self)) method(self.state, func) + def _request_variable_from_callback(self, func: Callable) -> None: + source_code = inspect.getsource(func) + tree = ast.parse(source_code) + key_value_pairs = [] + + for node in ast.walk(tree): + if isinstance(node, ast.Call) and hasattr(node.func, 'attr'): + if node.func.attr == 'get_variable_value': + key_value = (ast.literal_eval(node.args[0]), ast.literal_eval(node.args[1])) + key_value_pairs.append(key_value) + for key_value in key_value_pairs: + self.request_variable(key_value[0], key_value[1]) + + + def load_idf(fpath: Union[str, Path]) -> dict: """Load IDF file as JSON object. From d3cb0cc2b95043db290e03008ee774839776c2be Mon Sep 17 00:00:00 2001 From: Taoning Wang Date: Thu, 19 Oct 2023 14:50:25 -0700 Subject: [PATCH 2/9] add _request_variable_from_callback() to EnergyPlusSetup to automatically request variables --- frads/eplus.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/frads/eplus.py b/frads/eplus.py index f4adcf7..a5ec8e8 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -4,10 +4,10 @@ from datetime import datetime, timedelta import json -import os from pathlib import Path from typing import List, Optional, Callable, Union -from tempfile import TemporaryDirectory +import inspect +import ast from epmodel import epmodel as epm import epmodel @@ -593,9 +593,27 @@ def set_callback(self, method_name: str, func: Callable): raise AttributeError( f"Method {method_name} not found in EnergyPlus runtime API." ) + + self._request_variable_from_callback(func) + # method(self.state, partial(func, self)) method(self.state, func) + def _request_variable_from_callback(self, func: Callable) -> None: + source_code = inspect.getsource(func) + tree = ast.parse(source_code) + key_value_pairs = [] + + for node in ast.walk(tree): + if isinstance(node, ast.Call) and hasattr(node.func, 'attr'): + if node.func.attr == 'get_variable_value': + key_value = (ast.literal_eval(node.args[0]), ast.literal_eval(node.args[1])) + key_value_pairs.append(key_value) + for key_value in key_value_pairs: + self.request_variable(key_value[0], key_value[1]) + + + def load_idf(fpath: Union[str, Path]) -> dict: """Load IDF file as JSON object. From 1f93452350fcfb9822bf380e5093c476ff8c7012 Mon Sep 17 00:00:00 2001 From: Taoning Wang Date: Thu, 19 Oct 2023 14:50:25 -0700 Subject: [PATCH 3/9] add _request_variable_from_callback() to EnergyPlusSetup to automatically request variables --- frads/eplus.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/frads/eplus.py b/frads/eplus.py index f4adcf7..a5ec8e8 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -4,10 +4,10 @@ from datetime import datetime, timedelta import json -import os from pathlib import Path from typing import List, Optional, Callable, Union -from tempfile import TemporaryDirectory +import inspect +import ast from epmodel import epmodel as epm import epmodel @@ -593,9 +593,27 @@ def set_callback(self, method_name: str, func: Callable): raise AttributeError( f"Method {method_name} not found in EnergyPlus runtime API." ) + + self._request_variable_from_callback(func) + # method(self.state, partial(func, self)) method(self.state, func) + def _request_variable_from_callback(self, func: Callable) -> None: + source_code = inspect.getsource(func) + tree = ast.parse(source_code) + key_value_pairs = [] + + for node in ast.walk(tree): + if isinstance(node, ast.Call) and hasattr(node.func, 'attr'): + if node.func.attr == 'get_variable_value': + key_value = (ast.literal_eval(node.args[0]), ast.literal_eval(node.args[1])) + key_value_pairs.append(key_value) + for key_value in key_value_pairs: + self.request_variable(key_value[0], key_value[1]) + + + def load_idf(fpath: Union[str, Path]) -> dict: """Load IDF file as JSON object. From 0ecea6bbd9520fd0b6b98d42be78ab22cf0f8605 Mon Sep 17 00:00:00 2001 From: Taoning Wang Date: Thu, 19 Oct 2023 16:55:14 -0700 Subject: [PATCH 4/9] fix bug when get_variable_value is called with keyword arguments --- frads/eplus.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frads/eplus.py b/frads/eplus.py index a5ec8e8..5b71738 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -600,6 +600,14 @@ def set_callback(self, method_name: str, func: Callable): method(self.state, func) def _request_variable_from_callback(self, func: Callable) -> None: + """Request variables from callback function. + + Args: + func: Callback function. + + Example: + >>> epsetup._request_variable_from_callback(func) + """ source_code = inspect.getsource(func) tree = ast.parse(source_code) key_value_pairs = [] @@ -607,7 +615,18 @@ def _request_variable_from_callback(self, func: Callable) -> None: for node in ast.walk(tree): if isinstance(node, ast.Call) and hasattr(node.func, 'attr'): if node.func.attr == 'get_variable_value': - key_value = (ast.literal_eval(node.args[0]), ast.literal_eval(node.args[1])) + if len(node.args) == 2: + key_value = { + "name": ast.literal_eval(node.args[0]), + "key": ast.literal_eval(node.args[1]), + } + elif len(node.keywords) == 2: + key_value = { + node.keywords[0].arg: node.keywords[0].value.value, + node.keywords[1].arg: node.keywords[1].value.value, + } + else: + raise ValueError(f"Invalid number of arguments in {func}.") key_value_pairs.append(key_value) for key_value in key_value_pairs: self.request_variable(key_value[0], key_value[1]) From 41b6f47f78ec84141039f0f439810fb9cb9a54b4 Mon Sep 17 00:00:00 2001 From: Taoning Wang Date: Thu, 19 Oct 2023 16:59:03 -0700 Subject: [PATCH 5/9] fix request variable --- frads/eplus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frads/eplus.py b/frads/eplus.py index 5b71738..daaf1c2 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -629,7 +629,7 @@ def _request_variable_from_callback(self, func: Callable) -> None: raise ValueError(f"Invalid number of arguments in {func}.") key_value_pairs.append(key_value) for key_value in key_value_pairs: - self.request_variable(key_value[0], key_value[1]) + self.request_variable(**key_value) From 7f92c57e7ea5269202aa8c06deea63c3fa9a12a0 Mon Sep 17 00:00:00 2001 From: Taoning Wang Date: Thu, 19 Oct 2023 17:28:07 -0700 Subject: [PATCH 6/9] add check for actuators in callback function --- frads/eplus.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/frads/eplus.py b/frads/eplus.py index daaf1c2..baa8e30 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -313,7 +313,7 @@ def __init__(self, epmodel: EnergyPlusModel, weather_file: Optional[str] = None) ) self.api.runtime.callback_begin_new_environment(self.state, self._get_handles()) - self.actuators = None + self.actuators = [] self._get_list_of_actuators() def __enter__(self): @@ -323,14 +323,12 @@ def __exit__(self, exc_type, exc_value, exc_tb): self.api.state_manager.delete_state(self.state) def _actuator_func(self, state): - actuators_list = [] - if self.actuators is None: - list = self.api.api.listAllAPIDataCSV(state).decode("utf-8") - for line in list.split("\n"): + if len(self.actuators) == 0: + api_data: List[str] = self.api.api.listAllAPIDataCSV(state).decode("utf-8").splitlines() + for line in api_data: if line.startswith("Actuator"): line = line.replace(";", "") - actuators_list.append(line.split(",", 1)[1]) - self.actuators = actuators_list + self.actuators.append(line.split(",", 1)[1].split(",")) else: self.api.api.stopSimulation(state) @@ -340,10 +338,7 @@ def _get_list_of_actuators(self): actuator_state = self.api.state_manager.new_state() self.api.runtime.set_console_output_status(actuator_state, False) - method = getattr( - self.api.runtime, "callback_begin_system_timestep_before_predictor" - ) - method(actuator_state, self._actuator_func) + self.api.runtime.callback_begin_system_timestep_before_predictor(actuator_state, self._actuator_func) if self.epw is not None: self.api.runtime.run_energyplus( @@ -496,7 +491,7 @@ def run( silent: bool = False, annual: bool = False, design_day: bool = False, - ) -> EnergyPlusResult: + ) -> None: """Run EnergyPlus simulation. Args: @@ -599,7 +594,7 @@ def set_callback(self, method_name: str, func: Callable): # method(self.state, partial(func, self)) method(self.state, func) - def _request_variable_from_callback(self, func: Callable) -> None: + def _analyze_callback(self, func: Callable) -> None: """Request variables from callback function. Args: @@ -616,20 +611,32 @@ def _request_variable_from_callback(self, func: Callable) -> None: if isinstance(node, ast.Call) and hasattr(node.func, 'attr'): if node.func.attr == 'get_variable_value': if len(node.args) == 2: - key_value = { + key_value_dict = { "name": ast.literal_eval(node.args[0]), "key": ast.literal_eval(node.args[1]), } elif len(node.keywords) == 2: - key_value = { + key_value_dict = { node.keywords[0].arg: node.keywords[0].value.value, node.keywords[1].arg: node.keywords[1].value.value, } else: raise ValueError(f"Invalid number of arguments in {func}.") - key_value_pairs.append(key_value) - for key_value in key_value_pairs: - self.request_variable(**key_value) + key_value_pairs.append(key_value_dict) + + if node.func.attr == 'actuate': + if len(node.args) == 4: + key_value = [ast.literal_eval(node.args[i]) for i in range(4)] + elif len(node.keywords) == 4: + key_value_dict = {node.keywords[i].arg: node.keywords[i].value.value for i in range(4)} + key_value= [key_value_dict['component_type'], key_value_dict['name'], key_value_dict['key'], key_value_dict['value']] + else: + raise ValueError(f"Invalid number of arguments in {func}.") + if key_value not in self.actuators: + raise ValueError(f"Actuator {key_value} not found in model.") + + for key_value_dict in key_value_pairs: + self.request_variable(**key_value_dict) From 2b0e16ce9e4511c74f2b406930e15020695a30b2 Mon Sep 17 00:00:00 2001 From: Taoning Wang Date: Thu, 19 Oct 2023 17:42:02 -0700 Subject: [PATCH 7/9] fix acuator check bug --- frads/eplus.py | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/frads/eplus.py b/frads/eplus.py index baa8e30..d420948 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -324,7 +324,9 @@ def __exit__(self, exc_type, exc_value, exc_tb): def _actuator_func(self, state): if len(self.actuators) == 0: - api_data: List[str] = self.api.api.listAllAPIDataCSV(state).decode("utf-8").splitlines() + api_data: List[str] = ( + self.api.api.listAllAPIDataCSV(state).decode("utf-8").splitlines() + ) for line in api_data: if line.startswith("Actuator"): line = line.replace(";", "") @@ -338,7 +340,9 @@ def _get_list_of_actuators(self): actuator_state = self.api.state_manager.new_state() self.api.runtime.set_console_output_status(actuator_state, False) - self.api.runtime.callback_begin_system_timestep_before_predictor(actuator_state, self._actuator_func) + self.api.runtime.callback_begin_system_timestep_before_predictor( + actuator_state, self._actuator_func + ) if self.epw is not None: self.api.runtime.run_energyplus( @@ -555,19 +559,6 @@ def run( self.api.runtime.set_console_output_status(self.state, not silent) self.api.runtime.run_energyplus(self.state, [*opt, f"{output_prefix}.json"]) - # TODO: Setup temp directory for EnergyPlus output - # with TemporaryDirectory() as temp_dir: - # with open(os.path.join(temp_dir, "input.json"), "w") as wtr: - # wtr.write(self.model.model_dump_json(by_alias=True, exclude_none=True)) - # opt.extend(["-d", temp_dir, "input.json"]) - # self.api.runtime.set_console_output_status(self.state, not silent) - # self.api.runtime.run_energyplus(self.state, opt) - # # load everything except input.json and sqlite.err - # for ofile in os.listdir(temp_dir): - # if ofile not in ["input.json", "sqlite.err"]: - # with open(os.path.join(temp_dir, ofile)) as f: - # setattr(self.result, ofile, f.read()) - # return self.result def set_callback(self, method_name: str, func: Callable): """Set callback function for EnergyPlus runtime API. @@ -608,8 +599,8 @@ def _analyze_callback(self, func: Callable) -> None: key_value_pairs = [] for node in ast.walk(tree): - if isinstance(node, ast.Call) and hasattr(node.func, 'attr'): - if node.func.attr == 'get_variable_value': + if isinstance(node, ast.Call) and hasattr(node.func, "attr"): + if node.func.attr == "get_variable_value": if len(node.args) == 2: key_value_dict = { "name": ast.literal_eval(node.args[0]), @@ -624,12 +615,19 @@ def _analyze_callback(self, func: Callable) -> None: raise ValueError(f"Invalid number of arguments in {func}.") key_value_pairs.append(key_value_dict) - if node.func.attr == 'actuate': + elif node.func.attr == "actuate": if len(node.args) == 4: - key_value = [ast.literal_eval(node.args[i]) for i in range(4)] + key_value = [ast.literal_eval(node.args[i]) for i in range(3)] elif len(node.keywords) == 4: - key_value_dict = {node.keywords[i].arg: node.keywords[i].value.value for i in range(4)} - key_value= [key_value_dict['component_type'], key_value_dict['name'], key_value_dict['key'], key_value_dict['value']] + key_value_dict = { + node.keywords[i].arg: node.keywords[i].value.value + for i in range(4) + } + key_value = [ + key_value_dict["component_type"], + key_value_dict["name"], + key_value_dict["key"], + ] else: raise ValueError(f"Invalid number of arguments in {func}.") if key_value not in self.actuators: @@ -639,8 +637,6 @@ def _analyze_callback(self, func: Callable) -> None: self.request_variable(**key_value_dict) - - def load_idf(fpath: Union[str, Path]) -> dict: """Load IDF file as JSON object. From 5dbd16a2c47ca8989f11ae148584a4a381cb4303 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Fri, 20 Oct 2023 11:06:50 -0700 Subject: [PATCH 8/9] fix(eplus):fix get list of actuator bug --- frads/eplus.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frads/eplus.py b/frads/eplus.py index d420948..ff9a2aa 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -340,7 +340,7 @@ def _get_list_of_actuators(self): actuator_state = self.api.state_manager.new_state() self.api.runtime.set_console_output_status(actuator_state, False) - self.api.runtime.callback_begin_system_timestep_before_predictor( + self.api.runtime.callback_end_zone_timestep_after_zone_reporting( actuator_state, self._actuator_func ) @@ -559,7 +559,6 @@ def run( self.api.runtime.set_console_output_status(self.state, not silent) self.api.runtime.run_energyplus(self.state, [*opt, f"{output_prefix}.json"]) - def set_callback(self, method_name: str, func: Callable): """Set callback function for EnergyPlus runtime API. @@ -580,7 +579,7 @@ def set_callback(self, method_name: str, func: Callable): f"Method {method_name} not found in EnergyPlus runtime API." ) - self._request_variable_from_callback(func) + self._analyze_callback(func) # method(self.state, partial(func, self)) method(self.state, func) @@ -592,7 +591,7 @@ def _analyze_callback(self, func: Callable) -> None: func: Callback function. Example: - >>> epsetup._request_variable_from_callback(func) + >>> epsetup._analyze_callback(func) """ source_code = inspect.getsource(func) tree = ast.parse(source_code) From ba367337684ace5fa7ab086abd6605704404b0e4 Mon Sep 17 00:00:00 2001 From: Tammie Yu Date: Fri, 20 Oct 2023 13:02:30 -0700 Subject: [PATCH 9/9] feat(eplus): specific actuate function --- frads/eplus.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/frads/eplus.py b/frads/eplus.py index ff9a2aa..1264185 100644 --- a/frads/eplus.py +++ b/frads/eplus.py @@ -396,6 +396,130 @@ def actuate(self, component_type: str, name: str, key: str, value: float): self.state, self.actuator_handles[key][name], value ) + def actuate_cooling_setpoint(self, zone: str, value: float): + """Set cooling setpoint for a zone. + + Args: + zone: The name of the zone to set the cooling setpoint for. + value: The value to set the cooling setpoint to. + + Raises: + ValueError: If the zone is not found. + ValueError: If the zone does not have a thermostat. + + Example: + >>> epsetup.actuate_cooling_setpoint("zone1", 24) + """ + zone_stat = set( + [ + self.model.zone_control_thermostat[stat].zone_or_zonelist_name + for stat in self.model.zone_control_thermostat + ] + ) + if zone not in self.model.zone: + raise ValueError(f"Zone {zone} not found in model.") + elif zone not in zone_stat: + raise ValueError(f"Zone {zone} does not have a thermostat.") + + self.actuate( + component_type="Zone Temperature Control", + name="Cooling Setpoint", + key=zone, + value=value, + ) + + def actuate_heating_setpoint(self, zone: str, value: float): + """Set heating setpoint for a zone. + + Args: + zone: The name of the zone to set the heating setpoint for. + value: The value to set the heating setpoint to. + + Raises: + ValueError: If the zone is not found. + ValueError: If the zone does not have a thermostat. + + Example: + >>> epsetup.actuate_cooling_setpoint("zone1", 20) + """ + zone_stat = set( + [ + self.model.zone_control_thermostat[stat].zone_or_zonelist_name + for stat in self.model.zone_control_thermostat + ] + ) + if zone not in self.model.zone: + raise ValueError(f"Zone {zone} not found in model.") + elif zone not in zone_stat: + raise ValueError(f"Zone {zone} does not have a thermostat.") + + self.actuate( + component_type="Zone Temperature Control", + name="Heating Setpoint", + key=zone, + value=value, + ) + + def actuate_lighting_power(self, zone: str, value: float): + """Set lighting power for a zone. + + Args: + zone: The name of the zone to set the lighting power for. + value: The value to set the lighting power to. + + Raises: + ValueError: If the zone is not found. + ValueError: If the zone does not have lighting. + + Example: + >>> epsetup.actuate_lighting_power("zone1", 1000) + """ + zone_lgt = set( + [ + self.model.lights[lgt].zone_or_zonelist_or_space_or_spacelist_name + for lgt in self.model.lights + ] + ) + if zone not in self.model.zone: + raise ValueError(f"Zone {zone} not found in model.") + elif zone not in zone_lgt: + raise ValueError(f"Zone {zone} does not have lighting.") + + self.actuate( + component_type="Lights", + name="Lighting Level", + key=zone, + value=value, + ) + + def actuate_cfs_state(self, surface: str, construction_state: str): + """Set construction state for a surface. + + Args: + surface: The name of the surface to set the cfs state for. + construction_state: The name of the cfs state to set the surface to. + + Raises: + ValueError: If the surface is not found. + ValueError: If the cfs state is not found. + + Example: + >>> epsetup.actuate_construction_state("window1", "cfs1") + """ + if surface not in self.model.fenestration_surface_detailed: + raise ValueError(f"Surface {surface} not found in model.") + elif ( + construction_state not in self.model.construction_complex_fenestration_state + ): + raise ValueError(f"CFS state {construction_state} not found in model.") + + self.actuate( + component_type="Surface", + name="Construction State", + key=surface, + value=self.construction_handles[construction_state], + ) + def get_variable_value(self, name: str, key: str) -> float: """Get the value of a variable in the EnergyPlus model during runtime.