Example #1
0
def test_newidfobject():
    """py.test for newidfobject"""
    # make a blank idf
    # make a function for this and then continue.
    idf = IDF()
    idf.new()
    objtype = 'material:airgap'.upper()
    obj = idf.newidfobject(objtype, Name='Argon')
    obj = idf.newidfobject(objtype, Name='Krypton')
    obj = idf.newidfobject(objtype, Name='Xenon')
    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'],
                                     ['MATERIAL:AIRGAP', 'Krypton'],
                                     ['MATERIAL:AIRGAP', 'Xenon'],
                                     ]
    # remove an object
    idf.popidfobject(objtype, 1)
    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'],
                                     ['MATERIAL:AIRGAP', 'Xenon'],
                                     ]
    lastobject = idf.idfobjects[objtype][-1]
    idf.removeidfobject(lastobject)
    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'], ]
    # copyidfobject
    onlyobject = idf.idfobjects[objtype][0]
    idf.copyidfobject(onlyobject)

    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'],
                                     ['MATERIAL:AIRGAP', 'Argon'],
                                     ]
    # test some functions
    objtype = 'FENESTRATIONSURFACE:DETAILED'
    obj = idf.newidfobject(objtype, Name='A Wall')
    assert obj.coords == []
    assert obj.fieldvalues[1] == 'A Wall'
Example #2
0
 def test_popidfobject(self):
     idftxt = ""
     idfhandle = StringIO(idftxt)
     idf = IDF(idfhandle)
     key = "BUILDING"
     idf.newidfobject(key, Name="Building_remove")
     idf.newidfobject(key, Name="Building1")
     idf.newidfobject(key, Name="Building_remove")
     idf.newidfobject(key, Name="Building2")
     buildings = idf.idfobjects["building"]
     removethis = buildings[-2]
     idf.popidfobject(key, 2)
     assert buildings[2].Name == "Building2"
     assert idf.model.dt[key][2][1] == "Building2"
Example #3
0
 def test_popidfobject(self):
     idftxt = ""
     idfhandle = StringIO(idftxt)
     idf = IDF(idfhandle)
     key = "BUILDING"
     idf.newidfobject(key, Name="Building_remove")
     idf.newidfobject(key, Name="Building1")
     idf.newidfobject(key, Name="Building_remove")
     idf.newidfobject(key, Name="Building2")
     buildings = idf.idfobjects["building".upper()]
     removethis = buildings[-2]
     idf.popidfobject(key, 2)
     assert buildings[2].Name == "Building2"
     assert idf.model.dt[key][2][1] == "Building2"
Example #4
0
def test_newidfobject():
    """py.test for newidfobject"""
    # make a blank idf
    # make a function for this and then continue.
    idf = IDF()
    idf.new()
    objtype = "material:airgap".upper()
    obj = idf.newidfobject(objtype, Name="Argon")
    obj = idf.newidfobject(objtype, Name="Krypton")
    obj = idf.newidfobject(objtype, Name="Xenon")
    assert idf.model.dt[objtype] == [
        ["MATERIAL:AIRGAP", "Argon"],
        ["MATERIAL:AIRGAP", "Krypton"],
        ["MATERIAL:AIRGAP", "Xenon"],
    ]
    # remove an object
    idf.popidfobject(objtype, 1)
    assert idf.model.dt[objtype] == [
        ["MATERIAL:AIRGAP", "Argon"],
        ["MATERIAL:AIRGAP", "Xenon"],
    ]
    lastobject = idf.idfobjects[objtype][-1]
    idf.removeidfobject(lastobject)
    assert idf.model.dt[objtype] == [["MATERIAL:AIRGAP", "Argon"]]
    # copyidfobject
    onlyobject = idf.idfobjects[objtype][0]
    idf.copyidfobject(onlyobject)

    assert idf.model.dt[objtype] == [
        ["MATERIAL:AIRGAP", "Argon"],
        ["MATERIAL:AIRGAP", "Argon"],
    ]
    # remove all objects
    idf.removeallidfobjects(objtype)
    assert len(idf.idfobjects[objtype]) == 0
    # test some functions
    objtype = "FENESTRATIONSURFACE:DETAILED"
    obj = idf.newidfobject(objtype, Name="A Wall")
    assert obj.coords == []
    assert obj.fieldvalues[1] == "A Wall"

    # test defaultvalues=True and defaultvalues=False
    sim_deftrue = idf.newidfobject("SimulationControl".upper(),
                                   defaultvalues=True)
    assert sim_deftrue.Do_Zone_Sizing_Calculation == "No"
    sim_deffalse = idf.newidfobject("SimulationControl".upper(),
                                    defaultvalues=False)
    assert sim_deffalse.Do_Zone_Sizing_Calculation == ""
Example #5
0
def test_newidfobject():
    """py.test for newidfobject"""
    # make a blank idf
    # make a function for this and then continue.
    idf = IDF()
    idf.new()
    objtype = 'material:airgap'.upper()
    obj = idf.newidfobject(objtype, Name='Argon')
    obj = idf.newidfobject(objtype, Name='Krypton')
    obj = idf.newidfobject(objtype, Name='Xenon')
    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
        ['MATERIAL:AIRGAP', 'Krypton'],
        ['MATERIAL:AIRGAP', 'Xenon'],
    ]
    # remove an object
    idf.popidfobject(objtype, 1)
    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
        ['MATERIAL:AIRGAP', 'Xenon'],
    ]
    lastobject = idf.idfobjects[objtype][-1]
    idf.removeidfobject(lastobject)
    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
    ]
    # copyidfobject
    onlyobject = idf.idfobjects[objtype][0]
    idf.copyidfobject(onlyobject)

    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
        ['MATERIAL:AIRGAP', 'Argon'],
    ]
    # test some functions
    objtype = 'FENESTRATIONSURFACE:DETAILED'
    obj = idf.newidfobject(objtype, Name='A Wall')
    assert obj.coords == []
    assert obj.fieldvalues[1] == 'A Wall'

    # test defaultvalues=True and defaultvalues=False
    sim_deftrue = idf.newidfobject('SimulationControl'.upper(),
                                   defaultvalues=True)
    assert sim_deftrue.Do_Zone_Sizing_Calculation == 'No'
    sim_deffalse = idf.newidfobject('SimulationControl'.upper(),
                                    defaultvalues=False)
    assert sim_deffalse.Do_Zone_Sizing_Calculation == ''
Example #6
0
def test_newidfobject():
    """py.test for newidfobject"""
    # make a blank idf
    # make a function for this and then continue.
    idf = IDF()
    idf.new()
    objtype = 'material:airgap'.upper()
    obj = idf.newidfobject(objtype, Name='Argon')
    obj = idf.newidfobject(objtype, Name='Krypton')
    obj = idf.newidfobject(objtype, Name='Xenon')
    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'],
                                     ['MATERIAL:AIRGAP', 'Krypton'],
                                     ['MATERIAL:AIRGAP', 'Xenon'],
                                     ]
    # remove an object
    idf.popidfobject(objtype, 1)
    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'],
                                     ['MATERIAL:AIRGAP', 'Xenon'],
                                     ]
    lastobject = idf.idfobjects[objtype][-1]
    idf.removeidfobject(lastobject)
    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'], ]
    # copyidfobject
    onlyobject = idf.idfobjects[objtype][0]
    idf.copyidfobject(onlyobject)

    assert idf.model.dt[objtype] == [['MATERIAL:AIRGAP', 'Argon'],
                                     ['MATERIAL:AIRGAP', 'Argon'],
                                     ]
    # test some functions
    objtype = 'FENESTRATIONSURFACE:DETAILED'
    obj = idf.newidfobject(objtype, Name='A Wall')
    assert obj.coords == []
    assert obj.fieldvalues[1] == 'A Wall'
    
    # test defaultvalues=True and defaultvalues=False
    sim_deftrue = idf.newidfobject('SimulationControl'.upper(), defaultvalues=True)
    assert sim_deftrue.Do_Zone_Sizing_Calculation == 'No'
    sim_deffalse = idf.newidfobject('SimulationControl'.upper(), defaultvalues=False)
    assert sim_deffalse.Do_Zone_Sizing_Calculation == ''
Example #7
0
def test_newidfobject():
    """py.test for newidfobject"""
    # make a blank idf
    # make a function for this and then continue.
    idf = IDF()
    idf.new()
    objtype = 'material:airgap'.upper()
    obj = idf.newidfobject(objtype, Name='Argon')
    obj = idf.newidfobject(objtype, Name='Krypton')
    obj = idf.newidfobject(objtype, Name='Xenon')
    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
        ['MATERIAL:AIRGAP', 'Krypton'],
        ['MATERIAL:AIRGAP', 'Xenon'],
    ]
    # remove an object
    idf.popidfobject(objtype, 1)
    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
        ['MATERIAL:AIRGAP', 'Xenon'],
    ]
    lastobject = idf.idfobjects[objtype][-1]
    idf.removeidfobject(lastobject)
    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
    ]
    # copyidfobject
    onlyobject = idf.idfobjects[objtype][0]
    idf.copyidfobject(onlyobject)

    assert idf.model.dt[objtype] == [
        ['MATERIAL:AIRGAP', 'Argon'],
        ['MATERIAL:AIRGAP', 'Argon'],
    ]
    # test some functions
    objtype = 'FENESTRATIONSURFACE:DETAILED'
    obj = idf.newidfobject(objtype, Name='A Wall')
    assert obj.coords == []
    assert obj.fieldvalues[1] == 'A Wall'
Example #8
0
# 
# 1. Shiny new material object
# 2. Lousy material
# 3. third material

# <headingcell level=3>

# Deleting an idf object

# <markdowncell>

# Let us remove 2. Lousy material. It is the second material in the list. So let us remove the second material

# <codecell>

idf.popidfobject('MATERIAL', 1) # first material is '0', second is '1'

# <codecell>

print(idf.idfobjects['MATERIAL'])

# <markdowncell>

# You can see that the second material is gone ! Now let us remove the first material, but do it using a different function

# <codecell>

firstmaterial = idf.idfobjects['MATERIAL'][-1]

# <codecell>
Example #9
0
#
# 1. Shiny new material object
# 2. Lousy material
# 3. third material

# <headingcell level=3>

# Deleting an idf object

# <markdowncell>

# Let us remove 2. Lousy material. It is the second material in the list. So let us remove the second material

# <codecell>

idf.popidfobject("MATERIAL", 1)  # first material is '0', second is '1'

# <codecell>

print(idf.idfobjects["MATERIAL"])

# <markdowncell>

# You can see that the second material is gone ! Now let us remove the first material, but do it using a different function

# <codecell>

firstmaterial = idf.idfobjects["MATERIAL"][-1]

# <codecell>
Example #10
0
class Model:
    """
    The environment class.
    """
    model_import_flag = False

    @classmethod
    def set_energyplus_folder(cls, path):
        """
        Add the pyenergyplus into the path so the program can find the EnergyPlus.

        :parameter path: The installation path of the EnergyPlus 9.3.0.
        :type path: str

        :return: None
        """
        sys.path.insert(0, path)
        IDF.setiddname(f"{path}Energy+.idd")
        cls.model_import_flag = True

    def __init__(self,
                 idf_file_name: str = None,
                 prototype: str = None,
                 climate_zone: str = None,
                 weather_file: str = None,
                 heating_type: str = None,
                 foundation_type: str = None,
                 agent: Agent = None,
                 reward=None,
                 eplus_naming_dict=None,
                 eplus_var_types=None,
                 buffer_capacity=None,
                 buffer_seed=None,
                 buffer_chkpt_dir=None,
                 tmp_idf_path=None):
        """
        Initialize the building by loading the IDF file to the model.

        :parameter idf_file_name: The relative path to the IDF file. Use it if you want to use your own model.
        :parameter prototype: Either "multi" and "single", indicates the Multi-family low-rise apartment building and Single-family detached house.
        :parameter climate_zone: The climate zone code of the building. Please refer to https://codes.iccsafe.org/content/iecc2018/chapter-3-ce-general-requirements.
        :parameter weather_file: The relative path to the weather file associate with the building.
        :parameter heating_type: Select one from "electric", "gas", "oil", and "pump"
        :parameter foundation_type: Select one from "crawspace", "heated", "slab", and "unheated"
        :parameter agent: The user-defined Agent class object if the agent is implemented in a class.
        :parameter reward: The user-defined reward class object that contains a reward(state, actions) method.
        :parameter eplus_naming_dict: A dictionary map the state variable name to some specified names.
        :parameter eplus_var_types: A dictionary contains the state name and the state source location.
        :parameter buffer_capacity: The maximum number of historical state, action, new_state pair store in the buffer.
        :parameter buffer_seed: The random seed when sample from the buffer.
        :parameter buffer_chkpt_dir: The location of the buffer checkpoint should save.
        """
        if not Model.model_import_flag:
            raise ImportError("You have to set the energyplus folder first")
        self.api = None
        self.current_state = dict()
        self.idf = None
        self.occupancy = None
        self.run_parameters = None
        self.queue = EventQueue()
        self.agent = agent
        self.ignore_list = set()
        self.zone_names = None
        self.thermal_names = None
        self.counter = 0
        self.replay = ReplayBuffer(buffer_capacity, buffer_seed,
                                   buffer_chkpt_dir)
        self.warmup_complete = False
        self.terminate = False
        self.wait_for_step = Event()
        self.wait_for_state = Event()
        self.parent, self.child_energy = Pipe(duplex=True)
        self.child = None
        self.use_lock = False
        self.reward = reward
        self.eplus_naming_dict = dict(
        ) if eplus_naming_dict is None else eplus_naming_dict
        self.eplus_var_types = dict(
        ) if eplus_var_types is None else eplus_var_types
        self.prev_reward = None
        self.total_timestep = -1
        self.leap_weather = False
        self.state_modifier = StateModifier()

        # TODO: Validate input parameters

        if idf_file_name is None and climate_zone is not None:
            idf_file_name = f"./buildings/{prototype}_{climate_zone}_{heating_type}_{foundation_type}.idf"
            if weather_file is None and climate_zone is not None:
                weather_file = f"./weathers/{climate_zone}.epw"

        if tmp_idf_path is None:
            self.input_idf = "input.idf"
        else:
            self.input_idf = os.path.join(tmp_idf_path, "input.idf")

        self.run_parameters = ["-d", "result", self.input_idf]
        if weather_file:
            self.run_parameters = ["-w", weather_file] + self.run_parameters
            with open(weather_file, 'r') as w_file:
                for line in w_file:
                    line = line.split(',')
                    if len(line) > 3 and line[0].upper(
                    ) == "HOLIDAYS/DAYLIGHT SAVINGS":
                        self.leap_weather = True if line[1].upper(
                        ) == "YES" else False
                        break

        try:
            self.idf = IDF(idf_file_name)
        except:
            raise ValueError(
                "IDF file is damaged or not match with your EnergyPlus version."
            )

    @staticmethod
    def name_reformat(name):
        """
        Convert the entry from the space separated entry to the underline separated entry to match the IDF.

        :parameter name: The space separated entry.

        :return: The underline separated entry.
        """
        name = name.replace(' ', '_').replace(':', '').split('_')
        return '_'.join([word for word in name])

    def list_all_available_configurations(self):
        """
        Generate a list of all type of components appeared in the current building.

        :return: list of components entry.
        """
        return list(self.idf.idfobjects.keys())

    def get_all_configurations(self):
        """
        Read the IDF file, and return the content formatted with the IDD file.

        :return: the full IDF file with names and comments aside.
        """
        return self.idf.idfobjects

    def get_sub_configuration(self, idf_header_name: str):
        """
        Show all available settings for the given type of component.

        :parameter idf_header_name: The type of the component.

        :return: List of settings entry.
        """
        idf_header_name = idf_header_name.upper()
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        return self.idf.idfobjects[idf_header_name][0].fieldnames

    def get_available_names_under_group(self, idf_header_name: str):
        """
        Given the type of components, find all available components in the building by their entry.

        :parameter idf_header_name: The type of the component.

        :return: List of names.
        """
        idf_header_name = idf_header_name.upper()
        available_names = self.get_sub_configuration(idf_header_name)
        if "Name" in available_names:
            return [
                entry["Name"] for entry in self.idf.idfobjects[idf_header_name]
            ]
        else:
            for field_name in available_names:
                if "name" in field_name.lower():
                    return [
                        entry[field_name]
                        for entry in self.idf.idfobjects[idf_header_name]
                    ]
            raise KeyError(f"No entry field available for {idf_header_name}")

    def get_configuration(self,
                          idf_header_name: str,
                          component_name: str = None):
        """
        Given the type of component, the entry of the target component, find the settings of that component.

        :parameter idf_header_name: The type of the component.

        :parameter component_name: The entry of the component.

        :return: Settings of this component.
        """
        idf_header_name = idf_header_name.upper()
        if component_name is None:
            return self.idf.idfobjects[idf_header_name]
        else:
            names = self.get_available_names_under_group(idf_header_name)
            if component_name in names:
                return self.idf.idfobjects[idf_header_name][names.index(
                    component_name)]
            else:
                raise KeyError(
                    f"Failed to locate {component_name} in {idf_header_name}")

    def get_value_range(self,
                        idf_header_name: str,
                        field_name: str,
                        validate: bool = False):
        """
        Get the range of acceptable values of the specific setting.

        :parameter idf_header_name: The type of the component.

        :parameter field_name: The setting entry.

        :parameter validate: Set to True to check the current value is valid or not.

        :return: Validation result or the range of all acceptable values retrieved from the IDD file.
        """
        idf_header_name = idf_header_name.upper()
        field_name = field_name.replace(' ', '_')
        if field_name not in self.get_sub_configuration(idf_header_name):
            raise KeyError(
                f"Failed to locate {field_name} in {idf_header_name}")
        if validate:
            return self.idf.idfobjects[idf_header_name][0].checkrange(
                field_name)
        else:
            return self.idf.idfobjects[idf_header_name][0].getrange(field_name)

    def add_configuration(self, idf_header_name: str, values: dict = None):
        """
        Create and add a new component into the building model with the specific type and setting values.

        :parameter idf_header_name: The type of the component.

        :parameter values: A dictionary map the setting entry and the setting value.

        :return: The new component.
        """
        idf_header_name = idf_header_name.upper()
        object = self.idf.newidfobject(idf_header_name.upper())
        if values is None:
            return object
        for key, value in values.items():
            key = Model.name_reformat(key)
            if isinstance(value, (int, float)):
                exec(f"object.{key} = {value}")
            else:
                exec(f"object.{key} = '{value}'")
        return object

    def delete_configuration(self,
                             idf_header_name: str,
                             component_name: str = None):
        """
        Delete an existing component from the building model.

        :parameter idf_header_name: The type of the component.

        :parameter component_name: The entry of the component.

        :return: None.
        """
        idf_header_name = idf_header_name.upper()
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        if component_name is None:
            while len(self.idf.idfobjects[idf_header_name]):
                self.idf.popidfobject(idf_header_name, 0)
        else:
            names = self.get_available_names_under_group(idf_header_name)
            if component_name in names:
                self.idf.popidfobject(idf_header_name,
                                      names.index(component_name))
            else:
                raise KeyError(
                    f"Failed to locate {component_name} in {idf_header_name}")

    def edit_configuration(self, idf_header_name: str, identifier: dict,
                           update_values: dict):
        """
        Edit an existing component in the building model.

        :parameter idf_header_name: The type of the component.

        :parameter identifier: A dictionary map the setting entry and the setting value to locate the target component.

        :parameter update_values: A dictionary map the setting entry and the setting value that needs to update.

        :return: None.
        """
        idf_header_name = idf_header_name.upper()
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        fields = self.get_sub_configuration(idf_header_name)
        for entry in self.idf.idfobjects[idf_header_name]:
            valid = True
            for key, value in identifier.items():
                key = Model.name_reformat(key)
                if key in fields:
                    if entry[key] != value:
                        valid = False
            if valid:
                for key, value in update_values.items():
                    key = Model.name_reformat(key)
                    if isinstance(value, (int, float)):
                        exec(f"entry.{key} = {value}")
                    else:
                        exec(f"entry.{key} = '{value}'")

    def _get_thermal_names(self):
        """
        Initialize all available thermal zones.

        :return: None
        """
        people_zones = self.get_configuration("People")
        self.thermal_names = dict()
        for zone in people_zones:
            try:
                if zone[Model.name_reformat("Thermal Comfort Model 1 Type")]:
                    self.thermal_names[zone["Name"]] = zone[
                        Model.name_reformat("Zone or ZoneList Name")]
            except BadEPFieldError:
                pass

    def save_idf_file(self, path: str):
        """
        Save the modified building model for EnergyPlus simulation

        :parameter path: The relative path to the modified IDF file.

        :return: None.
        """
        self.idf.saveas(path)

    def _initialization(self):
        """
        Initialize the EnergyPlus simulation by letting the EnergyPlus finish the warmup.

        :return: None
        """
        if not self.api.exchange.api_data_fully_ready():
            return
        self.warmup_complete = True

    def _generate_output_files(self):
        """
        Assert errors to terminate the simulation after the warmup in order to generate the EDD file to list all available actions for the current building.

        :return: None
        """
        assert False

    def _step_callback(self):
        """
        Get the state value at each timestep, and modify the building model based on the actions from the ``EventQueue``.

        :return: None

        """
        # print("Child: Not ready")
        if not self.api.exchange.api_data_fully_ready(
        ) or not self.warmup_complete:
            return
        current_state = dict()
        # print("Child: Simulating")
        current_state["timestep"] = self.counter
        # print(self.get_date())
        current_state["time"] = self.get_date()
        current_state["temperature"] = dict()
        current_state["occupancy"] = dict()
        current_state["terminate"] = self.total_timestep == self.counter
        # if self.occupancy is not None:
        #     current_state["occupancy"] = {zone: value[self.counter] for zone, value in self.occupancy.items()}
        for name in self.zone_names:
            handle = self.api.exchange.get_variable_handle(
                "Zone People Occupant Count", name)
            if handle == -1:
                continue
            current_state["occupancy"][
                name] = self.api.exchange.get_variable_value(handle)
        for name in self.zone_names:
            handle = self.api.exchange.get_variable_handle(
                "Zone Air Temperature", name)
            if handle == -1:
                continue
            # print("Child: Simulating 2")
            current_state["temperature"][
                name] = self.api.exchange.get_variable_value(handle)
        handle = self.api.exchange.get_meter_handle("Heating:EnergyTransfer")
        current_state["energy"] = self.api.exchange.get_meter_value(handle)
        if self.reward is not None:
            current_state["reward"] = self.prev_reward

        # print("Child: Simulating 1")

        if "Zone Thermal Comfort Fanger Model PMV" in self.get_available_names_under_group(
                "Output:Variable"):
            current_state["PMV"] = dict()
            for zone in self.thermal_names:
                handle = self.api.exchange.get_variable_handle(
                    "Zone Thermal Comfort Fanger Model PMV", zone)
                if handle == -1:
                    continue
                current_state["PMV"][self.thermal_names[
                    zone]] = self.api.exchange.get_variable_value(handle)

        # Add state values
        state_vars = self.get_current_state_variables()

        # Add for temp extra output
        for entry in self.idf.idfobjects['OUTPUT:VARIABLE']:
            # we only care about the output vars for Gnu-RL
            if (entry['Variable_Name'], entry['Key_Value']) in self.eplus_naming_dict.keys() or \
               (entry['Variable_Name'], entry['Key_Value']) in state_vars:
                var_name = entry['Variable_Name']

                # if the key value is not associated with a zone return None for variable handler
                # key_val = entry['Key_Value'] if entry['Key_Value'] != '*' else None
                if entry['Key_Value'] == '*':
                    key_val = self.eplus_var_types.get(var_name, None)
                    if key_val is None:
                        continue
                else:
                    key_val = entry['Key_Value']
                handle = self.api.exchange.get_variable_handle(
                    var_name, key_val)
                if handle == -1:
                    continue
                # name the state value based on Gnu-RL paper
                key = self.eplus_naming_dict.get(
                    (var_name, entry['Key_Value']), f"{var_name}_{key_val}")
                current_state[key] = self.api.exchange.get_variable_value(
                    handle)

        self.state_modifier.get_update_states(current_state, self)
        # current_state.update(update_dict)
        # print(current_state)

        if self.use_lock:
            # print("Child: Sending current states")
            self.child_energy.send(current_state)
            self.wait_for_state.set()
            # Take all actions
            self.wait_for_step.clear()
            # print("Child: Waiting for actions")
            if not self.child_energy.poll():
                self.wait_for_step.wait()
            # print("Child: Receiving actions")
            events = self.child_energy.recv()
        else:
            # print(self.current_state)
            if self.counter != 0:
                self.replay.push(
                    self.current_state,
                    self.queue.get_event(self.current_state["timestep"]),
                    current_state, current_state["terminate"])
            self.current_state = current_state
            # self.historical_values.append(self.current_state)
            events = self.queue.trigger(self.counter)
        self.counter += 1

        # Trigger modifiers
        # for modifier in self.modifier:
        #     modifier.update(current_state)
        # print("Child: executing actions")

        # print("Child: Printing Reward")
        # Calculate Reward
        if self.reward is not None:
            self.prev_reward = self.reward.reward(current_state, events)

        # Trigger events
        for key in events["actuator"]:
            component_type, control_type, actuator_key = key.split("|*|")
            value = events["actuator"][key][1]
            handle = self.api.exchange.get_actuator_handle(
                component_type, control_type, actuator_key)
            if handle == -1:
                raise ValueError('Actuator handle could not be found: ',
                                 component_type, control_type, actuator_key)
            self.api.exchange.set_actuator_value(handle, value)
        for key in events["global"]:
            var_name = key
            value = events["global"][key][1]
            handle = self.api.exchange.get_global_handle(var_name)
            if handle == -1:
                raise ValueError('Actuator handle could not be found: ',
                                 component_type, control_type, actuator_key)
            self.api.exchange.set_global_value(handle, value)

        # if self.use_lock:
        #     # wait for next call of step
        #     self.wait_for_step.clear()
        #     self.wait_for_step.wait()
        # else:
        if not self.use_lock and self.agent:
            self.agent.step(self.current_state, self.queue, self.counter - 1)

    def step(self, action_list=None):
        """
        Add all actions into the ``EventQueue``, and then generate the state value of the next timestep.

        :parameter action_list: list of dictionarys contains the arguments for ``EventQueue.schedule_event()``.

        :return: The state value of the current timestep.
        """
        if action_list is not None:
            for action in action_list:
                self.queue.schedule_event(**action)
        # print("Parent: Sending actions")
        self.parent.send(self.queue.trigger(self.counter))
        self.counter += 1
        # Let process grab and execute actions
        # print("Parent: Releasing child's lock")
        self.wait_for_state.clear()
        self.wait_for_step.set()
        # print("Parent: Waiting for state values")
        if not self.parent.poll():
            self.wait_for_state.wait()
        self.wait_for_state.clear()
        current_state = self.parent.recv()
        # if isinstance(current_state, dict):
        self.replay.push(self.current_state,
                         self.queue.get_event(self.current_state["timestep"]),
                         current_state, current_state["terminate"])
        self.current_state = current_state
        # self.historical_values.append(self.current_state)
        if current_state["terminate"]:
            self.terminate = True
            self.parent.send(self.queue.trigger(self.counter))
            self.wait_for_step.set()
            self.child.join()
            # self.replay.terminate()
        self.wait_for_state.clear()
        # print("Parent: received state values")
        return self.current_state

    def is_terminate(self):
        """
        Determine if the simulation is finished or not.

        :return: True if the simulation is done, and False otherwise.
        """
        return self.terminate

    def reset(self):
        """
        Clear the actions and buffer, reset the environment and start the simulation.

        :return: The initial state of the simulation.
        """
        self._init_simulation()
        self.counter = 0
        self.total_timestep = self.get_total_timestep() - 1
        self.queue = EventQueue()
        self.replay.reset()
        # self.ignore_list = set()
        self.wait_for_state.clear()
        self.wait_for_step.clear()
        self.terminate = False
        self.use_lock = True
        self.parent, self.child_energy = Pipe(duplex=True)
        self.child = Process(target=self.simulate)
        self.child.start()
        # print("Waiting")
        if not self.parent.poll():
            self.wait_for_state.wait()
        self.current_state = self.parent.recv()
        # self.historical_values.append(self.current_state)
        self.wait_for_state.clear()
        return self.current_state

    def get_total_timestep(self):
        if "-w" not in self.run_parameters:
            return self.get_configuration(
                "Timestep")[0].Number_of_Timesteps_per_Hour * 24 * 8
        elif len(self.get_configuration("RunPeriod")) == 0:
            raise ValueError(
                "Your IDF files does not specify the run period."
                "Please manually edit the IDF file or use Model().set_runperiod(...)"
            )
        run_period = self.get_configuration("RunPeriod")[0]
        start = datetime(
            year=run_period.Begin_Year if run_period.Begin_Year else 2000,
            month=run_period.Begin_Month,
            day=run_period.Begin_Day_of_Month)
        end = datetime(
            year=run_period.End_Year if run_period.End_Year else start.year,
            month=run_period.End_Month,
            day=run_period.End_Day_of_Month)
        end += timedelta(days=1)

        if not self.leap_weather:
            offset = 0
            for year in range(start.year, end.year + 1):
                if isleap(year) and datetime(year, 2, 29) > start and datetime(
                        year, 2, 29) < end:
                    offset += 1
            end -= timedelta(days=offset)

        timestep = self.get_configuration(
            "Timestep")[0].Number_of_Timesteps_per_Hour
        if 60 % timestep != 0:
            timestep = 60 // round(60 / timestep)

        return int((end - start).total_seconds() // 3600 * timestep)

    def simulate(self, terminate_after_warmup=False):
        """
        Run the whole simulation once. If user use this method instead of the reset function, the user need to provide the Agent.

        :parameter terminate_after_warmup: True if the simulation should terminate after the warmup.

        :return: None.
        """
        from pyenergyplus.api import EnergyPlusAPI

        self.replay.set_ignore(self.state_modifier.get_ignore_by_checkpoint())
        if not self.use_lock:
            self._init_simulation()
        # for entry in self.zone_names:
        #     print(entry)
        #     self.current_handle["temperature"][entry] = \
        #         self.api.exchange.get_variable_handle("Zone Air Temperature", entry)
        # self.current_handle["temperature"] = self.api.exchange.get_variable_handle("SITE OUTDOOR AIR DRYBULB TEMPERATURE", "ENVIRONMENT")
        # self.current_handle["energy"] = self.api.exchange.get_meter_handle("Electricity:Facility")
        self.api = EnergyPlusAPI()
        if not terminate_after_warmup:
            self.api.runtime.callback_after_new_environment_warmup_complete(
                self._initialization)
            self.api.runtime.callback_begin_system_timestep_before_predictor(
                self._step_callback)
        else:
            self.api.runtime.callback_begin_new_environment(
                self._generate_output_files)
        self.api.runtime.run_energyplus(self.run_parameters)
        # if self.use_lock:
        #     self.child_energy.send("Terminated")
        #     self.wait_for_state.set()
        # else:
        #     self.replay.terminate()

    def _init_simulation(self):
        """
        Save the modified building model and initialize the zones for states.

        :return: None.
        """
        try:
            self.get_configuration("Output:Variable",
                                   "Zone People Occupant Count")
        except KeyError:
            self.add_configuration(
                "Output:Variable", {
                    "Key Value": '*',
                    "Variable Name": "Zone People Occupant Count",
                    "Reporting Frequency": "Timestep"
                })
        self.idf.saveas(self.input_idf)
        self.use_lock = False
        self.zone_names = self.get_available_names_under_group("Zone")
        self._get_thermal_names()
        self.warmup_complete = False

    def get_current_state_variables(self):
        """
        Find the current entries in the state.

        :return: List of entry names that is currently available in the state.
        """
        state_values = list(
            set(self.get_possible_state_variables()) - self.ignore_list)
        state_values.sort()
        return state_values

    def select_state_variables(self, entry=None, index=None):
        """
        Select interested state entries. If selected entry is not available for the current building, it will be ignored.

        :parameter entry: Entry names and corresponding objects that the state of the environment should have.

        :parameter index: Index of all available entries that the state of the environment should have.

        :return: None.
        """
        current_state = self.get_current_state_variables()
        if entry is None:
            entry = list()
        elif isinstance(entry, tuple):
            entry = list(entry)
        if index is not None:
            if isinstance(index, int):
                index = [index]
            for i in index:
                if i < len(current_state):
                    entry.append(current_state[i])
        self.ignore_list = set(
            self.get_possible_state_variables()) - set(entry)

    def add_state_variables(self, entry):
        """
        Add entries to the state. If selected entry is not available for the current building, it will be ignored.

        :parameter entry: Entry names and corresponding objects that the state of the environment should have.

        :return: None.
        """
        if not self.ignore_list:
            return
        if isinstance(entry, tuple):
            entry = [entry]

        self.ignore_list -= set(entry)

    def remove_state_variables(self, entry):
        """
        Remove entries from the state. If selected entry is not available in the state, it will be ignored.

        :parameter entry: Entry names and corresponding objects that the state of the environment should not have.

        :return: None.
        """
        if isinstance(entry, tuple):
            entry = [entry]
        self.ignore_list = self.ignore_list.union(set(entry))

    def pop_state_variables(self, index):
        """
        Remove entries from the state by its index. If selected index is not available in the state, it will be ignored.

        :parameter index: Entry index that the state of the environment should not have.

        :return: All entry names that is removed.
        """
        current_state = self.get_current_state_variables()
        pop_values = list()
        if isinstance(index, int):
            index = [index]
        for i in index:
            if i < len(current_state):
                self.ignore_list.add(current_state[i])
                pop_values.append(current_state[i])

        return pop_values

    def get_possible_state_variables(self):
        """
        Get all available state entries. This list of entries only depends on the building architecture.

        :return: List of available state entry names.
        """

        output = [(var["Variable_Name"], var["Key_Value"])
                  for var in self.get_configuration("Output:Variable")
                  if var["Key_Value"] != "*"]
        output.sort()
        return output

    def get_possible_actions(self):
        """
        Get all available actions that the user-defined agent can take. This list of actions only depends on the building architecture.

        :return: List of available actions in dictionaries.
        """
        if not os.path.isfile("./result/eplusout.edd"):
            if not self.get_configuration("Output:EnergyManagementSystem"):
                self.add_configuration(
                    "Output:EnergyManagementSystem",
                    values={
                        "Actuator Availability Dictionary Reporting":
                        "Verbose",
                        "Internal Variable Availability Dictionary Reporting":
                        "Verbose",
                        "EMS Runtime Language Debug Output Level": "ErrorsOnly"
                    })
            try:
                self.simulate(terminate_after_warmup=True)
            except AssertionError:
                pass

        actions = list()
        with open("./result/eplusout.edd", 'r') as edd:
            for line in edd:
                line = line.strip()
                if len(line) == 0 or line[0] == '!':
                    continue
                line = line.split(',')
                actions.append({
                    "Component Type": line[2],
                    "Control Type": line[3],
                    "Actuator Key": line[1]
                })
        return actions

    def get_link_zones(self):
        """
        Generate a graph that shows the connectivity of zones of the current building.

        :return: A bi-directional graph represented by a dictionary where the key is the source zone name and the value is a set of all neighbor zone name.
        """
        link_zones = {"Outdoor": set()}

        wall_to_zone = {}
        walls = self.get_configuration("BuildingSurface:Detailed")
        for wall in walls:
            if wall.Surface_Type != "WALL":
                continue
            wall_to_zone[wall.Name] = wall.Zone_Name
            link_zones[wall.Zone_Name] = set()
        for wall in walls:
            if wall.Surface_Type != "WALL":
                continue
            if wall.Outside_Boundary_Condition == "Outdoors":
                link_zones[wall.Zone_Name].add("Outdoor")
                link_zones["Outdoor"].add(wall.Zone_Name)
            elif wall.Outside_Boundary_Condition_Object:
                link_zones[wall_to_zone[
                    wall.Outside_Boundary_Condition_Object]].add(
                        wall.Zone_Name)
                link_zones[wall.Zone_Name].add(
                    wall_to_zone[wall.Outside_Boundary_Condition_Object])

        return link_zones

    def get_date(self):
        """
        Get the current time in the simulation environment.

        :return: None
        """
        year = self.api.exchange.year()
        month = self.api.exchange.month()
        day = self.api.exchange.day_of_month()
        hour = self.api.exchange.hour()
        minute = self.api.exchange.minutes()
        current_time = datetime(year, month, day,
                                hour) + timedelta(minutes=minute)
        return current_time

    def get_windows(self):
        """
        Get the zone-window matching dictionary based on the IDF file.

        :return: A dictionary where key is the zone name, and the value is a set of window available in the zone.
        """
        zone_window = {
            name: set()
            for name in self.get_available_names_under_group("Zone")
        }
        all_window = dict()
        for window in self.get_configuration("FenestrationSurface:Detailed"):
            if window.Surface_Type != "WINDOW":
                continue
            all_window[window.Building_Surface_Name] = window.Name

        for wall in self.get_configuration("BuildingSurface:Detailed"):
            if wall.Surface_Type != "WALL":
                continue
            if wall.Name in all_window:
                zone_window[wall.Zone_Name].add(all_window[wall.Name])
        return zone_window

    def get_doors(self):
        """
        Get the zone-door matching dictionary based on the IDF file.

        :return: A dictionary where key is the zone name, and the value is a set of door available in the zone.
        """
        zone_door = {
            name: set()
            for name in self.get_available_names_under_group("Zone")
        }
        all_door = dict()
        for door in self.get_configuration("FenestrationSurface:Detailed"):
            if door.Surface_Type != "GLASSDOOR":
                continue
            all_door[door.Building_Surface_Name] = door.Name

        for wall in self.get_configuration("BuildingSurface:Detailed"):
            if wall.Surface_Type != "WALL":
                continue
            if wall.Name in all_door:
                zone_door[wall.Zone_Name].add(all_door[wall.Name])
        return zone_door

    def get_lights(self):
        """
        Get the zone-light matching dictionary based on the IDF file.

        :return: A dictionary where key is the zone name, and the value is a set of light available in the zone.
        """
        zone_light = {
            name: set()
            for name in self.get_available_names_under_group("Zone")
        }
        for light in self.get_configuration("Lights"):
            zone_light[light.Zone_or_ZoneList_Name].add(light.Name)
        return zone_light

    def get_blinds(self):
        """
        Get the zone-blind matching dictionary based on the IDF file.

        :return: A dictionary where key is the zone name, and the value is a set of blind available in the zone.
        """
        window_with_blinds = set()
        for shade in self.get_configuration("WindowShadingControl"):
            window_with_blinds.add(shade.Fenestration_Surface_1_Name)

        zone_blinds = self.get_windows()
        for zone in zone_blinds:
            zone_blinds[zone] = zone_blinds[zone].intersection(
                window_with_blinds)

        return zone_blinds

    def set_blinds(self,
                   windows,
                   blind_material_name=None,
                   shading_control_type='AlwaysOff',
                   setpoint=50,
                   agent_control=False):
        """
        Install blinds that can be controlled on some given windows.

        :param windows: An iterable object that includes all windows' name that plan to install the blind.
        :param blind_material_name: The name of an existing blind in the IDF file  as the blind for all windows.
        :param shading_control_type: Specify default EPlus control strategy (only works if control=False)
        :param setpoint: Specify default blind angle.
        :param agent_control: False if using a default EPlus control strategy or no control (ie blinds always off). True if using an external agent to control the blinds.
        :return: None
        """
        if agent_control:
            shading_control_type = 'OnIfScheduleAllows'

        blind_material = None
        if blind_material_name:
            try:
                blind_material = self.get_configuration(
                    "WindowMaterial:Blind", blind_material_name)
            except KeyError:
                pass

        zone_window = self.get_windows()

        for zone in zone_window:
            for window in zone_window[zone]:
                if window in windows:
                    window_idf = self.get_configuration(
                        "FenestrationSurface:Detailed", window)
                    if blind_material is None:
                        blind = {
                            "Name": f"{window}_blind",
                            "Slat Orientation": "Horizontal",
                            "Slat Width": 0.025,
                            "Slat Separation": 0.01875,
                            "Front Side Slat Beam Solar Reflectance": 0.8,
                            "Back Side Slat Beam Solar Reflectance": 0.8,
                            "Front Side Slat Diffuse Solar Reflectance": 0.8,
                            "Back Side Slat Diffuse Solar Reflectance": 0.8,
                            "Slat Beam Visible Transmittance": 0.0
                        }
                        blind_mat = self.add_configuration(
                            "WindowMaterial:Blind", values=blind)
                    else:
                        blind_mat = self.idf.copyidfobject(blind_material)
                        blind_mat.Name = blind_mat.Name + f" {window}"

                    shading = {
                        "Name": f"{window}_blind_shading",
                        "Zone Name": zone,
                        "Shading Type": "InteriorBlind",
                        "Shading Device Material Name": f"{blind_mat.Name}",
                        "Shading Control Type": shading_control_type,
                        "Setpoint": setpoint,
                        "Type of Slat Angle Control for Blinds":
                        "ScheduledSlatAngle",
                        "Fenestration Surface 1 Name": window_idf.Name
                    }

                    if agent_control:
                        shading[
                            "Slat Angle Schedule Name"] = f"{window}_shading_schedule"
                        shading["Multiple Surface Control Type"] = "Group"
                        shading["Shading Control Is Scheduled"] = "Yes"
                        angle_schedule = {
                            "Name": f"{window}_shading_schedule",
                            "Schedule Type Limits Name": "Angle",
                            "Hourly Value": 45
                        }
                        self.add_configuration("Schedule:Constant",
                                               values=angle_schedule)

                    self.add_configuration("WindowShadingControl",
                                           values=shading)

    def set_occupancy(self, occupancy, locations):
        """
        Include the occupancy schedule generated by the OccupancyGenerator to the model as the occupancy data in
        EnergyPlus simulated environment is broken.

        :param occupancy: Numpy matrix contains the number of occupanct in each zone at each time slot.
        :param locations: List of zone names.
        :return: None
        """
        occupancy = occupancy.astype(int)
        self.occupancy = {
            locations[i]: occupancy[i, :]
            for i in range(len(locations))
        }
        if "Outdoor" in self.occupancy.keys():
            self.occupancy.pop("Outdoor")
        if "busy" in self.occupancy.keys():
            self.occupancy.pop("busy")

    def set_runperiod(self,
                      days,
                      start_year: int = 2000,
                      start_month: int = 1,
                      start_day: int = 1,
                      specify_year: bool = False):
        """
        Set the simulation run period.

        :param days: How many days in total the simulation should perform.
        :param start_year: Start from which year
        :param start_month: Start from which month of the year
        :param start_day: Start from which day of the month
        :param specify_year: Use default year or a specific year when simulation is within a year.
        :return: None
        """
        if "-w" not in self.run_parameters:
            raise KeyError("You must include a weather file to set run period")
        start = datetime(start_year, start_month, start_day)
        end = start + timedelta(days=days - 1)
        if not self.leap_weather:
            test_year = start_year - 1
            while datetime(test_year, 1, 1) < end:
                test_year += 1
                if not isleap(test_year):
                    continue
                if datetime(test_year, 2, 29) > start and datetime(
                        test_year, 2, 29) < end:
                    end += timedelta(days=1)

        values = {
            "Begin Month": start_month,
            "Begin Day of Month": start_day,
            "End Month": end.month,
            "End Day of Month": end.day
        }
        if end.year != start_year or specify_year:
            values.update({"Begin Year": start_year, "End Year": end.year})
        run_setting = self.get_configuration("RunPeriod")
        if len(run_setting) == 0:
            values["Name"] = "RunPeriod 1"
            self.add_configuration("RunPeriod", values)
        else:
            name = self.get_configuration("RunPeriod")[0].Name
            self.edit_configuration("RunPeriod", {"Name": name}, values)

    def set_timestep(self, timestep_per_hour):
        """
        Set the timestep per hour for the simulation.

        :param timestep_per_hour: How many timesteps within a hour.
        :return: None
        """
        self.get_configuration(
            "Timestep")[0].Number_of_Timesteps_per_Hour = timestep_per_hour

    def add_state_modifier(self, model):
        """
        Add a state modifier model, including predictive model, state estimator, controller, etc.

        :param model: A class object that follows the template (contains step(true_state, environment) method).
        :return: None
        """
        self.state_modifier.add_model(model)

    def flatten_state(self, order, state=None):
        """
        Flatten the state to a list of values by a given order.

        :param order: The order that the values should follow.
        :param state: The state to flatten. If not specified, then the current state is selected.
        :return: List of values follows the given order.
        """
        if state is None:
            state = self.current_state
        return [self.current_state.get(name, None) for name in order]

    def sample_buffer(self, batch_size):
        """
        Sample a batch of experience from the replay buffer.

        :param batch_size: Number of entries in a batch.
        :return: (state, action, next state, is terminate)
        """
        return self.replay.sample(batch_size)

    def sample_flattened_buffer(self, order, batch_size):
        """
        Sample a batch of experience from the replay buffer and flatten the states by a given order.

        :param order: The order that the values should follow.
        :param batch_size: Number of entries in a batch.
        :return: (state, action, next state, is terminate) where states are flatten.
        """
        state, action, next_state, done = self.replay.sample(batch_size)
        for i, row in state:
            state[i] = self.flatten_state(order, row)
        for i, row in next_state:
            next_state[i] = self.flatten_state(order, row)
        return state, action, next_state, done
Example #11
0
File: model.py Project: zha/COBS
class Model:
    """
    The environment class.
    """
    model_import_flag = False

    @classmethod
    def set_energyplus_folder(cls, path):
        """
        Add the pyenergyplus into the path so the program can find the EnergyPlus.

        :parameter path: The installation path of the EnergyPlus 9.3.0.
        :type path: str

        :return: None
        """
        sys.path.insert(0, path)
        IDF.setiddname(f"{path}Energy+.idd")
        cls.model_import_flag = True

    def __init__(self,
                 idf_file_name: str = None,
                 prototype: str = None,
                 climate_zone: str = None,
                 weather_file: str = None,
                 heating_type: str = None,
                 foundation_type: str = None,
                 agent: Agent = None):
        """
        Initialize the building by loading the IDF file to the model.

        :parameter idf_file_name: The relative path to the IDF file. Use it if you want to use your own model.

        :parameter prototype: Either "multi" and "single", indicates the Multi-family low-rise apartment building and Single-family detached house.
        
        :parameter climate_zone: The climate zone code of the building. Please refer to https://codes.iccsafe.org/content/iecc2018/chapter-3-ce-general-requirements.
        
        :parameter weather_file: The relative path to the weather file associate with the building.
        
        :parameter heating_type: Select one from "electric", "gas", "oil", and "pump"
        
        :parameter foundation_type: Select one from "crawspace", "heated", "slab", and "unheated"
        
        :parameter agent: The user-defined Agent class object if the agent is implemented in a class.
        """
        if not Model.model_import_flag:
            raise ImportError("You have to set the energyplus folder first")
        self.api = None
        self.current_state = dict()
        self.idf = None
        self.run_parameters = None
        self.queue = EventQueue()
        self.agent = agent
        self.ignore_list = set()
        self.zone_names = None
        self.thermal_names = None
        self.counter = 0
        self.historical_values = list()
        self.warmup_complete = False
        self.terminate = False
        self.wait_for_step = Event()
        self.wait_for_state = Event()
        self.parent, self.child_energy = Pipe(duplex=True)
        self.child = None
        self.use_lock = False

        # TODO: Validate input parameters

        if idf_file_name is None:
            idf_file_name = f"./buildings/{prototype}_{climate_zone}_{heating_type}_{foundation_type}.idf"
            if weather_file is None:
                weather_file = f"./weathers/{climate_zone}.epw"

        self.run_parameters = ["-d", "result", "input.idf"]
        if weather_file:
            self.run_parameters = ["-w", weather_file] + self.run_parameters

        try:
            self.idf = IDF(idf_file_name)
        except:
            raise ValueError(
                "IDF file is damaged or not match with your EnergyPlus version."
            )

    @staticmethod
    def name_reformat(name):
        """
        Convert the entry from the space separated entry to the underline separated entry to match the IDF.

        :parameter name: The space separated entry.

        :return: The underline separated entry.
        """
        name = name.replace(' ', '_').split('_')
        return '_'.join([word for word in name])

    def list_all_available_configurations(self):
        """
        Generate a list of all type of components appeared in the current building.
        
        :return: list of components entry.
        """
        return list(self.idf.idfobjects.keys())

    def get_all_configurations(self):
        """
        Read the IDF file, and return the content formatted with the IDD file.

        :return: the full IDF file with names and comments aside.
        """
        return self.idf.idfobjects

    def get_sub_configuration(self, idf_header_name: str):
        """
        Show all available settings for the given type of component.
        
        :parameter idf_header_name: The type of the component.
        
        :return: List of settings entry.
        """
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        return self.idf.idfobjects[idf_header_name][0].fieldnames

    def get_available_names_under_group(self, idf_header_name: str):
        """
        Given the type of components, find all available components in the building by their entry.
        
        :parameter idf_header_name: The type of the component.

        :return: List of names.
        """
        available_names = self.get_sub_configuration(idf_header_name)
        if "Name" in available_names:
            return [
                entry["Name"] for entry in self.idf.idfobjects[idf_header_name]
            ]
        else:
            for field_name in available_names:
                if "name" in field_name.lower():
                    return [
                        entry[field_name]
                        for entry in self.idf.idfobjects[idf_header_name]
                    ]
            raise KeyError(f"No entry field available for {idf_header_name}")

    def get_configuration(self,
                          idf_header_name: str,
                          component_name: str = None):
        """
        Given the type of component, the entry of the target component, find the settings of that component.
        
        :parameter idf_header_name: The type of the component.
        
        :parameter component_name: The entry of the component.
        
        :return: Settings of this component.
        """
        if component_name is None:
            return self.idf.idfobjects[idf_header_name]
        else:
            names = self.get_available_names_under_group(idf_header_name)
            if component_name in names:
                return self.idf.idfobjects[idf_header_name][names.index(
                    component_name)]
            else:
                raise KeyError(
                    f"Failed to locate {component_name} in {idf_header_name}")

    def get_value_range(self,
                        idf_header_name: str,
                        field_name: str,
                        validate: bool = False):
        """
        Get the range of acceptable values of the specific setting.
        
        :parameter idf_header_name: The type of the component.
        
        :parameter field_name: The setting entry.
        
        :parameter validate: Set to True to check the current value is valid or not.
        
        :return: Validation result or the range of all acceptable values retrieved from the IDD file.
        """
        field_name = field_name.replace(' ', '_')
        if field_name not in self.get_sub_configuration(idf_header_name):
            raise KeyError(
                f"Failed to locate {field_name} in {idf_header_name}")
        if validate:
            return self.idf.idfobjects[idf_header_name][0].checkrange(
                field_name)
        else:
            return self.idf.idfobjects[idf_header_name][0].getrange(field_name)

    def add_configuration(self, idf_header_name: str, values: dict = None):
        """
        Create and add a new component into the building model with the specific type and setting values.
        
        :parameter idf_header_name: The type of the component.
        
        :parameter values: A dictionary map the setting entry and the setting value.
        
        :return: The new component.
        """
        object = self.idf.newidfobject(idf_header_name.upper())
        if values is None:
            return object
        for key, value in values.items():
            key = Model.name_reformat(key)
            if isinstance(value, (int, float)):
                exec(f"object.{key} = {value}")
            else:
                exec(f"object.{key} = '{value}'")
        return object

    def delete_configuration(self,
                             idf_header_name: str,
                             component_name: str = None):
        """
        Delete an existing component from the building model.
        
        :parameter idf_header_name: The type of the component.
        
        :parameter component_name: The entry of the component.
        
        :return: None.
        """
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        if component_name is None:
            while len(self.idf.idfobjects[idf_header_name]):
                self.idf.popidfobject(idf_header_name, 0)
        else:
            names = self.get_available_names_under_group(idf_header_name)
            if component_name in names:
                self.idf.popidfobject(idf_header_name,
                                      names.index(component_name))
            else:
                raise KeyError(
                    f"Failed to locate {component_name} in {idf_header_name}")

    def edit_configuration(self, idf_header_name: str, identifier: dict,
                           update_values: dict):
        """
        Edit an existing component in the building model.
        
        :parameter idf_header_name: The type of the component.
        
        :parameter identifier: A dictionary map the setting entry and the setting value to locate the target component.
        
        :parameter update_values: A dictionary map the setting entry and the setting value that needs to update.
        
        :return: None.
        """
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        fields = self.get_sub_configuration(idf_header_name)
        for entry in self.idf.idfobjects[idf_header_name]:
            valid = True
            for key, value in identifier:
                key = Model.name_reformat(key)
                if key in fields:
                    if entry[key] != value:
                        valid = False
            if valid:
                for key, value in update_values:
                    key = Model.name_reformat(key)
                    if isinstance(value, (int, float)):
                        exec(f"entry.{key} = {value}")
                    else:
                        exec(f"entry.{key} = '{value}'")

    def _get_thermal_names(self):
        """
        Initialize all available thermal zones.
        
        :return: None
        """
        people_zones = self.get_configuration("People")
        self.thermal_names = list()
        for zone in people_zones:
            try:
                if zone[Model.name_reformat("Thermal Comfort Model 1 Type")]:
                    self.thermal_names.append(zone["Name"])
            except BadEPFieldError:
                pass

    def save_idf_file(self, path: str):
        """
        Save the modified building model for EnergyPlus simulation
        
        :parameter path: The relative path to the modified IDF file.
        
        :return: None.
        """
        self.idf.saveas(path)

    def _initialization(self):
        """
        Initialize the EnergyPlus simulation by letting the EnergyPlus finish the warmup.
        
        :return: None
        """
        if not self.api.exchange.api_data_fully_ready():
            return
        self.warmup_complete = True

    def _generate_output_files(self):
        """
        Assert errors to terminate the simulation after the warmup in order to generate the EDD file to list all available actions for the current building.
        
        :return: None
        """
        assert False

    def _step_callback(self):
        """
        Get the state value at each timestep, and modify the building model based on the actions from the ``EventQueue``.
        :return: None
        
        """
        if not self.api.exchange.api_data_fully_ready(
        ) or not self.warmup_complete:
            return
        current_state = dict()
        # print("Child: Simulating")
        current_state["temperature"] = dict()
        current_state["occupancy"] = dict()
        for name in self.zone_names:
            handle = self.api.exchange.get_variable_handle(
                "Zone Air Temperature", name)
            current_state["temperature"][
                name] = self.api.exchange.get_variable_value(handle)
        handle = self.api.exchange.get_meter_handle("Heating:EnergyTransfer")
        current_state["energy"] = self.api.exchange.get_meter_value(handle)

        if "Zone Thermal Comfort Fanger Model PMV" in self.get_available_names_under_group(
                "Output:Variable"):
            current_state["PMV"] = dict()
            for zone in self.thermal_names:
                handle = self.api.exchange.get_variable_handle(
                    "Zone Thermal Comfort Fanger Model PMV", zone)
                current_state["PMV"][
                    zone] = self.api.exchange.get_variable_value(handle)

        if self.use_lock:
            # print("Child: Sending current states")
            self.child_energy.send(current_state)
            self.wait_for_state.set()
            # Take all actions
            self.wait_for_step.clear()
            # print("Child: Waiting for actions")
            if not self.child_energy.poll():
                self.wait_for_step.wait()
            # print("Child: Receiving actions")
            events = self.child_energy.recv()
        else:
            self.current_state = current_state
            self.historical_values.append(self.current_state)
            events = self.queue.trigger(self.counter)
            self.counter += 1

        # print("Child: executing actions")

        # Trigger events
        for key in events["actuator"]:
            component_type, control_type, actuator_key = key.split("|*|")
            value = events["actuator"][key][1]
            handle = self.api.exchange.get_actuator_handle(
                component_type, control_type, actuator_key)
            self.api.exchange.set_actuator_value(handle, value)
        for key in events["global"]:
            var_name = key
            value = events["global"][key][1]
            handle = self.api.exchange.get_global_handle(var_name)
            self.api.exchange.set_global_value(handle, value)

        # if self.use_lock:
        #     # wait for next call of step
        #     self.wait_for_step.clear()
        #     self.wait_for_step.wait()
        # else:
        if not self.use_lock and self.agent:
            self.agent.step(self.current_state, self.queue, self.counter - 1)

    def step(self, action_list: list):
        """
        Add all actions into the ``EventQueue``, and then generate the state value of the next timestep.

        :parameter action_list: list of dictionarys contains the arguments for ``EventQueue.schedule_event()``.

        :return: The state value of the current timestep.
        """
        if action_list is not None:
            for action in action_list:
                self.queue.schedule_event(**action)
        # print("Parent: Sending actions")
        self.parent.send(self.queue.trigger(self.counter))
        self.counter += 1
        # Let process grab and execute actions
        # print("Parent: Releasing child's lock")
        self.wait_for_state.clear()
        self.wait_for_step.set()
        # print("Parent: Waiting for state values")
        if not self.parent.poll():
            self.wait_for_state.wait()
        self.wait_for_state.clear()
        current_state = self.parent.recv()
        if current_state != "Terminated":
            self.current_state = current_state
            self.historical_values.append(self.current_state)
        else:
            self.terminate = True
            self.child.join()
        self.wait_for_state.clear()
        # print("Parent: received state values")
        return self.current_state

    def is_terminate(self):
        """
        Determine if the simulation is finished or not.
        
        :return: True if the simulation is done, and False otherwise.
        """
        return self.terminate

    def reset(self):
        """
        Clear the actions and buffer, reset the environment and start the simulation.
        
        :return: The initial state of the simulation.
        """
        self._init_simulation()
        self.queue = EventQueue()
        self.historical_values = list()
        self.ignore_list = set()
        self.wait_for_state.clear()
        self.wait_for_step.clear()
        self.terminate = False
        self.use_lock = True
        self.parent, self.child_energy = Pipe(duplex=True)
        self.child = Process(target=self.simulate)
        self.child.start()
        self.wait_for_state.wait()
        self.current_state = self.parent.recv()
        self.historical_values.append(self.current_state)
        self.wait_for_state.clear()
        return self.current_state

    def simulate(self, terminate_after_warmup=False):
        """
        Run the whole simulation once. If user use this method instead of the reset function, the user need to provide the Agent.
        
        :parameter terminate_after_warmup: True if the simulation should terminate after the warmup.
        
        :return: None.
        """
        from pyenergyplus.api import EnergyPlusAPI

        if not self.use_lock:
            self._init_simulation()
        # for entry in self.zone_names:
        #     print(entry)
        #     self.current_handle["temperature"][entry] = \
        #         self.api.exchange.get_variable_handle("Zone Air Temperature", entry)
        # self.current_handle["temperature"] = self.api.exchange.get_variable_handle("SITE OUTDOOR AIR DRYBULB TEMPERATURE", "ENVIRONMENT")
        # self.current_handle["energy"] = self.api.exchange.get_meter_handle("Electricity:Facility")
        self.api = EnergyPlusAPI()
        if not terminate_after_warmup:
            self.api.runtime.callback_after_new_environment_warmup_complete(
                self._initialization)
            self.api.runtime.callback_begin_system_timestep_before_predictor(
                self._step_callback)
        else:
            self.api.runtime.callback_begin_new_environment(
                self._generate_output_files)
        self.api.runtime.run_energyplus(self.run_parameters)
        if self.use_lock:
            self.child_energy.send("Terminated")
            self.wait_for_state.set()

    def _init_simulation(self):
        """
        Save the modified building model and initialize the zones for states.
        
        :return: None.
        """
        self.idf.saveas("input.idf")
        self.use_lock = False
        self.zone_names = self.get_available_names_under_group("Zone")
        self._get_thermal_names()
        self.warmup_complete = False

    def get_current_state_values(self):
        """
        Find the current entries in the state.
        
        :return: List of entry names that is currently available in the state.
        """
        state_values = list(
            set(self.get_possible_state_entries()) - self.ignore_list)
        state_values.sort()
        return state_values

    def select_state_values(self, entry=None, index=None):
        """
        Select interested state entries. If selected entry is not available for the current building, it will be ignored.
        
        :parameter entry: Entry names that the state of the environment should have.
        
        :parameter index: Index of all available entries that the state of the environment should have.
        
        :return: None.
        """
        current_state = self.get_current_state_values()
        if entry is None:
            entry = list()
        elif isinstance(entry, str):
            entry = list(entry)
        if index is not None:
            if isinstance(index, int):
                index = [index]
            for i in index:
                if i < len(current_state):
                    entry.append(current_state[i])
        self.ignore_list = set(self.get_possible_state_entries()) - set(entry)

    def add_state_values(self, entry):
        """
        Add entries to the state. If selected entry is not available for the current building, it will be ignored.
        
        :parameter entry: Entry names that the state of the environment should have.
        
        :return: None.
        """
        if not self.ignore_list:
            return
        if isinstance(entry, str):
            entry = [entry]

        self.ignore_list -= set(entry)

    def remove_state_values(self, entry):
        """
        Remove entries from the state. If selected entry is not available in the state, it will be ignored.
        
        :parameter entry: Entry names that the state of the environment should not have.
        
        :return: None.
        """
        if isinstance(entry, str):
            entry = [entry]
        self.ignore_list = self.ignore_list.union(set(entry))

    def pop_state_values(self, index):
        """
        Remove entries from the state by its index. If selected index is not available in the state, it will be ignored.
        
        :parameter index: Entry index that the state of the environment should not have.
        
        :return: All entry names that is removed.
        """
        current_state = self.get_current_state_values()
        pop_values = list()
        if isinstance(index, int):
            index = [index]
        for i in index:
            if i < len(current_state):
                self.ignore_list.add(current_state[i])
                pop_values.append(current_state[i])

        return pop_values

    def get_possible_state_entries(self):
        """
        Get all available state entries. This list of entries only depends on the building architecture.
        
        :return: List of available state entry names.
        """

        output = self.get_available_names_under_group("Output:Variable")
        output.sort()
        return output

    def get_possible_actions(self):
        """
        Get all available actions that the user-defined agent can take. This list of actions only depends on the building architecture.
        
        :return: List of available actions in dictionaries.
        """
        if not os.path.isfile("./result/eplusout.edd"):
            if not self.get_configuration("Output:EnergyManagementSystem"):
                self.add_configuration(
                    "Output:EnergyManagementSystem",
                    values={
                        "Actuator Availability Dictionary Reporting":
                        "Verbose",
                        "Internal Variable Availability Dictionary Reporting":
                        "Verbose",
                        "EMS Runtime Language Debug Output Level": "ErrorsOnly"
                    })
            try:
                self.simulate(terminate_after_warmup=True)
            except AssertionError:
                pass

        actions = list()
        with open("./result/eplusout.edd", 'r') as edd:
            for line in edd:
                line = line.strip()
                if len(line) == 0 or line[0] == '!':
                    continue
                line = line.split(',')
                actions.append({
                    "Component Type": line[2],
                    "Control Type": line[3],
                    "Actuator Key": line[1]
                })
        return actions

    def get_link_zones(self):
        """
        Generate a graph that shows the connectivity of zones of the current building.
        
        :return: A bi-directional graph represented by a dictionary where the key is the source zone name and the value is a set of all neighbor zone name.
        """
        link_zones = {"Outdoor": set()}

        wall_to_zone = {}
        walls = self.get_configuration("BuildingSurface:Detailed")
        for wall in walls:
            if wall.Surface_Type != "WALL":
                continue
            wall_to_zone[wall.Name] = wall.Zone_Name
            link_zones[wall.Zone_Name] = set()
        for wall in walls:
            if wall.Surface_Type != "WALL":
                continue
            if wall.Outside_Boundary_Condition == "Outdoors":
                link_zones[wall.Zone_Name].add("Outdoor")
                link_zones["Outdoor"].add(wall.Zone_Name)
            elif wall.Outside_Boundary_Condition_Object:
                link_zones[wall_to_zone[
                    wall.Outside_Boundary_Condition_Object]].add(
                        wall.Zone_Name)
                link_zones[wall.Zone_Name].add(
                    wall_to_zone[wall.Outside_Boundary_Condition_Object])

        return link_zones
Example #12
0
class Model:
    model_import_flag = False

    eplus2gnurl = {
        ('Site Outdoor Air Drybulb Temperature', '*'): "Outdoor Temp.",
        ('Site Outdoor Air Relative Humidity', '*'): "Outdoor RH",
        ('Site Wind Speed', '*'): "Wind Speed",
        ('Site Wind Direction', '*'): "Wind Direction",
        ('Site Diffuse Solar Radiation Rate per Area', '*'): "Diff. Solar Rad.",
        ('Site Direct Solar Radiation Rate per Area', '*'): "Direct Solar Rad.",
        ('Building Mean Temperature', '*'): "Indoor Temp.",
        ('Zone Thermostat Heating Setpoint Temperature', 'SPACE1-1'): "Htg SP",
        ('Zone Thermostat Cooling Setpoint Temperature', 'SPACE1-1'): "Clg SP",
        ('Building Mean PPD', '*'): "PPD",
        ('Occupancy Flag', '*'): "Occupancy Flag",
        ('Indoor Air Temperature Setpoint', '*'): "Indoor Temp. Setpoint",
        # ('Building Total Occupants', '*'): "Occupancy Flag",
        ('Heating Coil Electric Power', 'Main Heating Coil 1'): "Coil Power",
        ('Facility Total HVAC Electric Demand Power', '*'): "HVAC Power",
        ('System Node Temperature', 'VAV SYS 1 OUTLET NODE'): "Sys Out Temp.",
        ('System Node Mass Flow Rate', 'VAV SYS 1 OUTLET NODE'): "Sys Out Mdot",
        ('System Node Temperature', 'VAV SYS 1 Inlet NODE'): "Sys In Temp.",
        ('System Node Mass Flow Rate', 'VAV SYS 1 Inlet NODE'): "Sys In Mdot",
        ('System Node Temperature', 'Mixed Air Node 1'): "MA Temp.",
        ('System Node Mass Flow Rate', 'Mixed Air Node 1'): "MA Mdot",
        ('System Node Temperature', 'Outside Air Inlet Node 1'): "OA Temp",
        ('System Node Mass Flow Rate', 'Outside Air Inlet Node 1'): "OA Mdot",
    }

    var_types = {
        'Site Outdoor Air Drybulb Temperature': "Environment",
        'Site Outdoor Air Relative Humidity': "Environment",
        'Site Wind Speed': "Environment",
        'Site Wind Direction': "Environment",
        'Site Diffuse Solar Radiation Rate per Area': "Environment",
        'Site Direct Solar Radiation Rate per Area': "Environment",
        'Building Mean Temperature': "EMS",
        'Facility Total HVAC Electric Demand Power': 'Whole Building',
        'Building Mean PPD': "EMS",
        'Occupancy Flag': "EMS",
        'Indoor Air Temperature Setpoint': "EMS",
    }

    @classmethod
    def set_energyplus_folder(cls, path):
        sys.path.insert(0, path)
        IDF.setiddname(f"{path}Energy+.idd")
        cls.model_import_flag = True

    def __init__(self,
                 idf_file_name: str = None,
                 basement_type: str = None,
                 prototype: str = None,
                 climate_zone: str = None,
                 weather_file: str = None,
                 heating_type: str = None,
                 foundation_type: str = None,
                 agent: Agent = None
                 ):
        if not Model.model_import_flag:
            raise ImportError("You have to set the energyplus folder first")
        self.api = None
        self.idf = None
        self.run_parameters = None
#         self.queue = EventQueue()
        self.agent = agent
        self.current_state = None
        self.state_history = []
        self.zone_names = None
        self.thermal_names = None
        self.counter = 0
        self.historical_values = list()
        self.warmup_complete = False

        # TODO: Validate input parameters

        if idf_file_name is None:
            idf_file_name = f"./buildings/{prototype}_{climate_zone}_{heating_type}_{foundation_type}.idf"
            if weather_file is None:
                weather_file = f"./weathers/{climate_zone}.epw"

        self.run_parameters = ["-d", "result", "input.idf"]
        if weather_file:
            self.run_parameters = ["-w", weather_file] + self.run_parameters

        try:
            self.idf = IDF(idf_file_name)
        except:
            raise ValueError("IDF file is damaged or not match with your EnergyPlus version.")

    @staticmethod
    def name_reformat(name):
        name = name.replace(' ', '_').split('_')
        return '_'.join([word for word in name])

    def list_all_available_configurations(self):
        return list(self.idf.idfobjects.keys())

    def get_all_configurations(self):
        return self.idf.idfobjects

    def get_sub_configuration(self,
                              idf_header_name: str):
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        return self.idf.idfobjects[idf_header_name][0].fieldnames

    def get_available_names_under_group(self,
                                        idf_header_name: str):
        available_names = self.get_sub_configuration(idf_header_name)
        if "Name" in available_names:
            return [entry["Name"] for entry in self.idf.idfobjects[idf_header_name]]
        else:
            for field_name in available_names:
                if "name" in field_name.lower():
                    return [entry[field_name] for entry in self.idf.idfobjects[idf_header_name]]
            raise KeyError(f"No name field available for {idf_header_name}")

    def get_configuration(self,
                          idf_header_name: str,
                          component_name: str = None):
        if component_name is None:
            return self.idf.idfobjects[idf_header_name]
        else:
            names = self.get_available_names_under_group(idf_header_name)
            if component_name in names:
                return self.idf.idfobjects[idf_header_name][names.index(component_name)]
            else:
                raise KeyError(f"Failed to locate {component_name} in {idf_header_name}")

    def get_value_range(self,
                        idf_header_name: str,
                        field_name: str,
                        validate: bool = False):
        field_name = field_name.replace(' ', '_')
        if field_name not in self.get_sub_configuration(idf_header_name):
            raise KeyError(f"Failed to locate {field_name} in {idf_header_name}")
        if validate:
            return self.idf.idfobjects[idf_header_name][0].checkrange(field_name)
        else:
            return self.idf.idfobjects[idf_header_name][0].getrange(field_name)

    def add_configuration(self,
                          idf_header_name: str,
                          values: dict = None):
        object = self.idf.newidfobject(idf_header_name.upper())
        if values is None:
            return object
        for key, value in values.items():
            key = Model.name_reformat(key)
            if isinstance(value, (int, float)):
                exec(f"object.{key} = {value}")
            else:
                exec(f"object.{key} = '{value}'")
        return object

    def delete_configuration(self,
                             idf_header_name: str,
                             component_name: str = None):
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        if component_name is None:
            while len(self.idf.idfobjects[idf_header_name]):
                self.idf.popidfobject(idf_header_name, 0)
        else:
            names = self.get_available_names_under_group(idf_header_name)
            if component_name in names:
                self.idf.popidfobject(idf_header_name, names.index(component_name))
            else:
                raise KeyError(f"Failed to locate {component_name} in {idf_header_name}")

    def edit_configuration(self,
                           idf_header_name: str,
                           identifier: dict,
                           update_values: dict):
        if not self.idf.idfobjects.get(idf_header_name):
            raise KeyError(f"No {idf_header_name} section in current IDF file")
        fields = self.get_sub_configuration(idf_header_name)
        for entry in self.idf.idfobjects[idf_header_name]:
            valid = True
            for key, value in identifier:
                key = Model.name_reformat(key)
                if key in fields:
                    if entry[key] != value:
                        valid = False
            if valid:
                for key, value in update_values:
                    key = Model.name_reformat(key)
                    if isinstance(value, (int, float)):
                        exec(f"entry.{key} = {value}")
                    else:
                        exec(f"entry.{key} = '{value}'")

    def get_thermal_names(self):
        people_zones = self.get_configuration("PEOPLE")
        self.thermal_names = list()
        for zone in people_zones:
            try:
                if zone[Model.name_reformat("Thermal Comfort Model 1 Type")]:
                    self.thermal_names.append(zone["Name"])
            except BadEPFieldError:
                pass

    def save_idf_file(self,
                      path: str):
        self.idf.saveas(path)

    def validate(self):
        pass

    def initialization(self):
        # print('========== STARTING INIT')
        if not self.api.exchange.api_data_fully_ready():
            # print('======= RETURN NOT READY')
            return
        self.warmup_complete = True
        # print('========== READY PRINT', self.api.exchange.list_available_api_data_csv())
        self.save_extended_history()

    def save_extended_history(self):
        state_history = {}
        for entry in self.idf.idfobjects['OUTPUT:VARIABLE']:
            # we only care about the output vars for Gnu-RL
            if (entry['Variable_Name'], entry['Key_Value']) in self.eplus2gnurl.keys():
                var_name = entry['Variable_Name']

                # if the key value is not associated with a zone return None for variable handler
                # key_val = entry['Key_Value'] if entry['Key_Value'] != '*' else None
                if entry['Key_Value'] == '*':
                    key_val = self.var_types[var_name]
                else:
                    key_val = entry['Key_Value']
                handle = self.api.exchange.get_variable_handle(var_name, key_val)
                # name the state value based on Gnu-RL paper
                key = self.eplus2gnurl.get((var_name, entry['Key_Value']))
                state_history[key] = self.api.exchange.get_variable_value(handle)

        # Save the time to the state history
        # set year manual because this gets the year from the epw which is all over the place
        if len(self.state_history) == 0:
            state_history['year'] = 1991  # TODO may want to make a parameter for this
            state_history['month'] = self.api.exchange.month()
            state_history['day'] = self.api.exchange.day_of_month()
            state_history['hour'] = 0
            state_history['minute'] = 0

        else:
            state_history['year'] = 1991  # TODO may want to make a parameter for this
            state_history['month'] = self.api.exchange.month()
            state_history['day'] = self.api.exchange.day_of_month()
            state_history['hour'] = self.api.exchange.hour()
            state_history['minute'] = self.api.exchange.minutes()
        self.state_history.append(state_history)

#     def trigger_events(self):
#         events = self.queue.trigger(self.counter)
#         for key in events["actuator"]:
#             print('============ ACTUATOR')
#             component_type, control_type, actuator_key = key.split("|*|")
#             value = events["actuator"][key][1]
#             handle = self.api.exchange.get_actuator_handle(component_type, control_type, actuator_key)
#             self.api.exchange.set_actuator_value(handle, value)
#         for key in events["global"]:
#             print('============ ACTUATOR')
#             var_name = key
#             value = events["global"][key][1]
#             handle = self.api.exchange.get_global_handle(var_name)
#             self.api.exchange.set_global_value(handle, value)

    def save_current_state(self):
        self.current_state = dict()

        self.current_state["temperature"] = dict()
        for name in self.zone_names:
            handle = self.api.exchange.get_variable_handle("Zone Air Temperature", name)
            self.current_state["temperature"][name] = self.api.exchange.get_variable_value(handle)
        handle = self.api.exchange.get_meter_handle("Electricity:Facility")
        self.current_state["electricity"] = self.api.exchange.get_meter_value(handle)

        if "Zone Thermal Comfort Fanger Model PMV" in self.get_available_names_under_group("OUTPUT:VARIABLE"):
            self.current_state["PMV"] = dict()
            for zone in self.thermal_names:
                handle = self.api.exchange.get_variable_handle("Zone Thermal Comfort Fanger Model PMV", zone)
                self.current_state["PMV"][zone] = self.api.exchange.get_variable_value(handle)

        self.historical_values.append(self.current_state)

    def is_terminal(self):
        pass

    def step(self):
        # TODO api.exchange.warmup_flag seems like it should work here but for some reason it is always true
        if not self.api.exchange.api_data_fully_ready():
            return
        if not self.warmup_complete:
            return

        self.save_current_state()
        # self.trigger_events()
        self.save_extended_history()
        self.counter += 1

        if self.agent:
            # TODO - if task is episodic need a check for terminal state
            self.agent.step(self.current_state)

    def simulate(self):
        from pyenergyplus.api import EnergyPlusAPI

        self.idf.saveas("input.idf")
        self.zone_names = self.get_available_names_under_group("ZONE")
        self.get_thermal_names()
        # for name in self.zone_names:
        #     print(name)
        #     self.current_handle["temperature"][name] = \
        #         self.api.exchange.get_variable_handle("Zone Air Temperature", name)
        # self.current_handle["temperature"] = self.api.exchange.get_variable_handle(
        # "SITE OUTDOOR AIR DRYBULB TEMPERATURE", "ENVIRONMENT")
        # self.current_handle["electricity"] = self.api.exchange.get_meter_handle("Electricity:Facility")
        self.api = EnergyPlusAPI()
        # self.api.runtime.callback_begin_new_environment(self.initialization)
        self.api.runtime.callback_after_new_environment_warmup_complete(self.initialization)
        self.api.runtime.callback_begin_system_timestep_before_predictor(self.step)
        status = self.api.runtime.run_energyplus(self.run_parameters)
        print('Simulator return status: ', status)
        # self.api.runtime.clear_all_states()