diff --git a/alfalfa_worker/jobs/openstudio/lib/alfalfa_point.py b/alfalfa_worker/jobs/openstudio/lib/alfalfa_point.py deleted file mode 100644 index 6ebde009..00000000 --- a/alfalfa_worker/jobs/openstudio/lib/alfalfa_point.py +++ /dev/null @@ -1,11 +0,0 @@ -from dataclasses import dataclass -from typing import Callable - -from alfalfa_worker.lib.models import Point - - -@dataclass -class AlfalfaPoint: - point: Point - handle: int - converter: Callable[[float], float] = lambda x: x diff --git a/alfalfa_worker/jobs/openstudio/lib/openstudio_component.py b/alfalfa_worker/jobs/openstudio/lib/openstudio_component.py new file mode 100644 index 00000000..9f50cbb6 --- /dev/null +++ b/alfalfa_worker/jobs/openstudio/lib/openstudio_component.py @@ -0,0 +1,89 @@ + +from dataclasses import dataclass +from enum import Enum +from typing import Callable + +from alfalfa_worker.lib.job_exception import JobException + +# import pyenergyplus + + +class OpenStudioComponentType(Enum): + ACTUATOR = "Actuator" + CONSTANT = "Constant" + OUTPUT_VARIABLE = "OutputVariable" + GLOBAL_VARIABLE = "GlobalVariable" + METER = "Meter" + + +@dataclass +class OpenStudioComponent: + type: OpenStudioComponentType + parameters: dict + handle: int = None + + def __init__(self, type: str, parameters: dict, handle: int = None, converter: Callable[[float], float] = None): + self.type = OpenStudioComponentType(type) + self.parameters = parameters + self.handle = handle + + if converter: + self.converter = converter + else: + self.converter = lambda x: x + + if self.type == OpenStudioComponentType.ACTUATOR: + self.reset = False + + def pre_initialize(self, api, state): + if self.type == OpenStudioComponentType.OUTPUT_VARIABLE: + api.exchange.request_variable(state, **self.parameters) + + def initialize(self, api, state): + if self.type == OpenStudioComponentType.GLOBAL_VARIABLE: + self.handle = api.exchange.get_ems_global_handle(state, var_name=self.parameters["variable_name"]) + elif self.type == OpenStudioComponentType.OUTPUT_VARIABLE: + self.handle = api.exchange.get_variable_handle(state, **self.parameters) + elif self.type == OpenStudioComponentType.METER: + self.handle = api.exchange.get_meter_handle(state, **self.parameters) + elif self.type == OpenStudioComponentType.ACTUATOR: + self.handle = api.exchange.get_actuator_handle(state, + component_type=self.parameters["component_type"], + control_type=self.parameters["control_type"], + actuator_key=self.parameters["component_name"]) + elif self.type == OpenStudioComponentType.CONSTANT: + return + else: + raise JobException(f"Unknown point type: {self.type}") + + if self.handle == -1: + raise JobException(f"Handle not found for point of type: {self.type} and parameters: {self.parameters}") + + def read_value(self, api, state): + if self.handle == -1 or self.handle is None: + return None + if self.type == OpenStudioComponentType.OUTPUT_VARIABLE: + value = api.exchange.get_variable_value(state, self.handle) + elif self.type == OpenStudioComponentType.GLOBAL_VARIABLE: + value = api.exchange.get_ems_global_value(state, self.handle) + elif self.type == OpenStudioComponentType.METER: + value = api.exchange.get_meter_value(state, self.handle) + elif self.type == OpenStudioComponentType.ACTUATOR: + value = api.exchange.get_actuator_value(state, self.handle) + elif self.type == OpenStudioComponentType.CONSTANT: + value = self.parameters["value"] + + return self.converter(value) + + def write_value(self, api, state, value): + if self.handle == -1 or self.handle is None: + return None + if self.type == OpenStudioComponentType.GLOBAL_VARIABLE and value is not None: + api.exchange.set_ems_global_value(state, self.handle, value) + if self.type == OpenStudioComponentType.ACTUATOR: + if value is not None: + api.exchange.set_actuator_value(state, self.handle, value) + self.reset = False + elif self.reset is False: + api.exchange.reset_actuator(state, self.handle) + self.reset = True diff --git a/alfalfa_worker/jobs/openstudio/lib/openstudio_point.py b/alfalfa_worker/jobs/openstudio/lib/openstudio_point.py new file mode 100644 index 00000000..2970b366 --- /dev/null +++ b/alfalfa_worker/jobs/openstudio/lib/openstudio_point.py @@ -0,0 +1,91 @@ +from dataclasses import dataclass + +from alfalfa_worker.jobs.openstudio.lib.openstudio_component import ( + OpenStudioComponent +) +from alfalfa_worker.lib.enums import PointType +from alfalfa_worker.lib.job_exception import JobException +from alfalfa_worker.lib.models import Point, Run + + +@dataclass +class OpenStudioPoint: + id: str + name: str + optional: bool = False + units: str = None + input: OpenStudioComponent = None + output: OpenStudioComponent = None + point: Point = None + + def __init__(self, id: str, name: str, optional: bool = False, units: str = None, input: dict = None, output: dict = None): + self.id = id + self.name = name + self.optional = optional + self.units = units + self.input = OpenStudioComponent(**input) if input else None + self.output = OpenStudioComponent(**output) if output else None + + def create_point(self): + point_type = PointType.OUTPUT + if self.input is not None: + if self.output is not None: + point_type = PointType.BIDIRECTIONAL + else: + point_type = PointType.INPUT + + self.point = Point(ref_id=self.id, point_type=point_type, name=self.name) + if self.units is not None: + self.point.units = self.units + + def attach_run(self, run: Run): + if self.point is None: + self.create_point() + + conflicting_points = Point.objects(ref_id=self.point.ref_id, run=run) + needs_rename = len(conflicting_points) > 0 + if len(conflicting_points) == 1: + needs_rename = not (conflicting_points[0] == self.point) + + if needs_rename: + self.point.ref_id = self.point.ref_id + "_1" + return self.attach_run(run) + else: + run.add_point(self.point) + + def pre_initialize(self, api, state): + if self.input: + self.input.pre_initialize(api, state) + + if self.output: + self.output.pre_initialize(api, state) + + def initialize(self, api, state): + try: + if self.input: + self.input.initialize(api, state) + + if self.output: + self.output.initialize(api, state) + except JobException as e: + if self.optional: + self.disable() + else: + raise e + + def disable(self): + if self.point: + if self.point.run: + self.point.run = None + + def update_input(self, api, state): + if self.input: + self.input.write_value(api, state, self.point.value) + + def update_output(self, api, state) -> float: + if self.output: + value = self.output.read_value(api, state) + if self.point: + self.point.value = value + return value + return None diff --git a/alfalfa_worker/jobs/openstudio/lib/variables.py b/alfalfa_worker/jobs/openstudio/lib/variables.py deleted file mode 100644 index 583c2223..00000000 --- a/alfalfa_worker/jobs/openstudio/lib/variables.py +++ /dev/null @@ -1,55 +0,0 @@ -import json -from pathlib import Path - -from alfalfa_worker.lib.enums import PointType -from alfalfa_worker.lib.models import Point, Run - - -class Variables: - def __init__(self, run: Run) -> None: - self.run = run - self.points = {} - self.points_path = run.dir / 'simulation' / 'points.json' - if self.points_path.exists(): - with open(self.points_path, 'r') as fp: - self.points = json.load(fp) - - def generate_points(self, alfalfa_json: Path) -> None: - with open(alfalfa_json, 'r') as fp: - points = json.load(fp) - point_id_count = {} - for point in points: - id = point["id"] - - # Make IDs unique - if id in point_id_count.keys(): - point_id_count[id] = point_id_count[id] + 1 - id = f"{id}_{point_id_count[id]}" - - # Set point type - point_type = PointType.OUTPUT - if "input" in point and "output" in point: - point_type = PointType.BIDIRECTIONAL - elif "input" in point: - point_type = PointType.INPUT - - # Create point and add to Run - run_point = Point(ref_id=id, point_type=point_type, name=point["name"]) - self.run.add_point(run_point) - - # Set value for "Constant" type points - if "output" in point and point["output"]["type"] == "Constant": - run_point.value = point["output"]["parameters"]["value"] - - if "units" in point: - run_point.units = point["units"] - - self.points[id] = point - if point["name"] == "Whole Building Electricity": - self.points[id]["output"]["multiplier"] = 1 / 60 - del self.points[id]["id"] - - self.run.save() - - with open(self.points_path, 'w') as fp: - json.dump(self.points, fp) diff --git a/alfalfa_worker/jobs/openstudio/step_run.py b/alfalfa_worker/jobs/openstudio/step_run.py index ef4aa9ef..45efd3d0 100644 --- a/alfalfa_worker/jobs/openstudio/step_run.py +++ b/alfalfa_worker/jobs/openstudio/step_run.py @@ -1,3 +1,4 @@ +import json import os from datetime import datetime, timedelta from typing import Callable @@ -5,16 +6,15 @@ import openstudio from pyenergyplus.api import EnergyPlusAPI -from alfalfa_worker.jobs.openstudio.lib.alfalfa_point import AlfalfaPoint -from alfalfa_worker.jobs.openstudio.lib.variables import Variables +from alfalfa_worker.jobs.openstudio.lib.openstudio_component import ( + OpenStudioComponent +) +from alfalfa_worker.jobs.openstudio.lib.openstudio_point import OpenStudioPoint from alfalfa_worker.jobs.step_run_process import StepRunProcess -from alfalfa_worker.lib.enums import PointType from alfalfa_worker.lib.job_exception import ( JobException, - JobExceptionExternalProcess, - JobExceptionSimulation + JobExceptionExternalProcess ) -from alfalfa_worker.lib.models import Point def callback_wrapper(func): @@ -44,17 +44,25 @@ def __init__(self, run_id, realtime, timescale, external_clock, start_datetime, self.weather_file = os.path.realpath(self.dir / 'simulation' / 'sim.epw') - self.variables = Variables(self.run) - self.logger.info('Generating variables from Openstudio output') + self.ep_points: list[OpenStudioPoint] = [] for alfalfa_json in self.run.dir.glob('**/run/alfalfa.json'): - self.variables.generate_points(alfalfa_json) + self.ep_points += [OpenStudioPoint(**point) for point in json.load(alfalfa_json.open())] + + def add_additional_meter(fuel: str, units: str, converter: Callable[[float], float]): + meter_component = OpenStudioComponent("Meter", {"meter_name": f"{fuel}:Building"}, converter) + meter_point = OpenStudioPoint(id=f"whole_building_{fuel.lower()}", name=f"Whole Building {fuel}", units=units) + meter_point.output = meter_component + self.ep_points.append(meter_point) + + add_additional_meter("Electricity", "W", lambda x: x / self.options.timesteps_per_hour) + add_additional_meter("NaturalGas", "W", lambda x: x / self.options.timesteps_per_hour) + + [point.attach_run(self.run) for point in self.ep_points] self.ep_api: EnergyPlusAPI = None self.ep_state = None - self.additional_points: list[AlfalfaPoint] = [] - def simulation_process_entrypoint(self): """ Initialize and start EnergyPlus co-simulation. @@ -72,11 +80,8 @@ def simulation_process_entrypoint(self): self.ep_api.runtime.callback_message(self.ep_state, self.callback_message) self.ep_api.functional.callback_error(self.ep_state, self.callback_error) - # Request output variables - for point in self.variables.points.values(): - if "output" in point: - if point["output"]["type"] == "OutputVariable": - self.ep_api.exchange.request_variable(self.ep_state, **point["output"]["parameters"]) + for point in self.ep_points: + point.pre_initialize(self.ep_api, self.ep_state) return_code = self.ep_api.runtime.run_energyplus(state=self.ep_state, command_line_args=['-w', str(self.weather_file), '-d', str(self.dir / 'simulation'), '-r', str(self.idf_file)]) self.logger.info(f"Exited simulation with code: {return_code}") @@ -105,54 +110,12 @@ def initialize_handles(self, state): them with handles that can be used to transact data with energyplus.""" exceptions = [] - def get_handle(type, parameters): - handle = None - if type == "GlobalVariable": - handle = self.ep_api.exchange.get_ems_global_handle(state, var_name=parameters["variable_name"]) - elif type == "OutputVariable": - handle = self.ep_api.exchange.get_variable_handle(state, **parameters) - self.logger.info(f"Got handle: {handle} for {parameters}") - elif type == "Meter": - handle = self.ep_api.exchange.get_meter_handle(state, **parameters) - elif type == "Actuator": - handle = self.ep_api.exchange.get_actuator_handle(state, - component_type=parameters["component_type"], - control_type=parameters["control_type"], - actuator_key=parameters["component_name"]) - else: - raise JobException(f"Unknown point type: {type}") - if handle == -1: - raise JobException(f"Handle not found for point of type: {type} and parameters: {parameters}") - return handle - - def add_additional_meter(fuel: str, units: str, converter: Callable[[float], float]): + for point in self.ep_points: try: - handle = get_handle("Meter", {"meter_name": f"{fuel}:Building"}) - point = Point(ref_id=f"whole_building_{fuel.lower()}", name=f"Whole Building {fuel}", units=units, point_type=PointType.OUTPUT) - self.run.add_point(point) - alfalfa_point = AlfalfaPoint(point, handle, converter) - self.additional_points.append(alfalfa_point) - except JobException: - return None - - add_additional_meter("Electricity", "W", lambda x: x / self.options.timesteps_per_hour) - add_additional_meter("NaturalGas", "W", lambda x: x / self.options.timesteps_per_hour) - - for id, point in self.variables.points.items(): - try: - if "input" in point: - handle = get_handle(point["input"]["type"], point["input"]["parameters"]) - self.variables.points[id]["input"]["handle"] = handle - if "output" in point: - if point["output"]["type"] == "Constant": - continue - self.variables.points[id]["output"]["handle"] = get_handle(point["output"]["type"], point["output"]["parameters"]) + point.initialize(self.ep_api, state) except JobException as e: - if point["optional"]: - self.logger.warn(f"Ignoring optional point '{point['name']}'") - self.run.get_point_by_id(id).delete() - else: - exceptions.append(e) + exceptions.append(e) + if len(exceptions) > 0: ExceptionGroup("Exceptions generated while initializing EP handles", exceptions) self.run.save() @@ -198,61 +161,21 @@ def get_sim_time(self) -> datetime: def ep_read_outputs(self): """Reads outputs from E+ state""" influx_points = [] - for point in self.run.output_points: - if point.ref_id not in self.variables.points: - continue - component = self.variables.points[point.ref_id]["output"] - if "handle" not in component: - continue - handle = component["handle"] - type = component["type"] - value = None - if type == "OutputVariable": - value = self.ep_api.exchange.get_variable_value(self.ep_state, handle) - elif type == "GlobalVariable": - value = self.ep_api.exchange.get_ems_global_value(self.ep_state, handle) - elif type == "Meter": - value = self.ep_api.exchange.get_meter_value(self.ep_state, handle) - elif type == "Actuator": - value = self.ep_api.exchange.get_actuator_value(self.ep_state, handle) - else: - raise JobException(f"Invalid point type: {type}") - if "multiplier" in component: - self.logger.info(f"Applying a multiplier of '{component['multiplier']}' to point '{point['name']}'") - value *= component["multiplier"] - if self.ep_api.exchange.api_error_flag(self.ep_state): - raise JobExceptionSimulation(f"EP returned an api error while reading from point: {point.name}") - point.value = value - if self.options.historian_enabled: + for point in self.ep_points: + value = point.update_output(self.ep_api, self.ep_state) + if self.options.historian_enabled and value is not None: influx_points.append({"fields": { "value": value }, "tags": { - "id": point.ref_id, + "id": point.point.ref_id, "point": True, "source": "alfalfa" }, "measurement": self.run.ref_id, "time": self.run.sim_time, }) - for additional_point in self.additional_points: - value = self.ep_api.exchange.get_meter_value(self.ep_state, additional_point.handle) - value = additional_point.converter(value) - additional_point.point.value = value - if self.options.historian_enabled: - influx_points.append({"fields": - { - "value": value - }, "tags": - { - "id": additional_point.point.ref_id, - "point": True, - "source": "alfalfa" - }, - "measurement": self.run.ref_id, - "time": self.run.sim_time - }) if self.historian_enabled: try: response = self.influx_client.write_points(points=influx_points, @@ -269,19 +192,8 @@ def ep_read_outputs(self): def ep_write_inputs(self): """Writes inputs to E+ state""" - for point in self.run.input_points: - component = self.variables.points[point.ref_id]["input"] - handle = component["handle"] - type = component["type"] - if type == "GlobalVariable" and point.value is not None: - self.ep_api.exchange.set_ems_global_value(self.ep_state, handle, point.value) - if type == "Actuator": - if point.value is not None: - self.ep_api.exchange.set_actuator_value(self.ep_state, handle, point.value) - component["reset"] = False - elif "reset" not in component or component["reset"] is False: - self.ep_api.exchange.reset_actuator(self.ep_state, handle) - component["reset"] = True + for point in self.ep_points: + point.update_input(self.ep_api, self.ep_state) def prepare_idf(self): """