diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index a69cb531..9a8b8cc3 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -57,14 +57,10 @@ def run(self): # Get executable using shutil.which (determines the extension based on # the platform, eg: .exe. And copy the executable to tmp - self.basement_exe = Path( - shutil.which( - "Basement", path=self.eplus_home / "PreProcess" / "GrndTempCalc" - ) - ).copy(self.run_dir) - self.basement_idd = ( - self.eplus_home / "PreProcess" / "GrndTempCalc" / "BasementGHT.idd" - ).copy(self.run_dir) + self.basement_exe = Path(shutil.which("Basement", path=self.eplus_home)).copy( + self.run_dir + ) + self.basement_idd = (self.eplus_home / "BasementGHT.idd").copy(self.run_dir) self.outfile = self.idf.name # The BasementGHTin.idf file is copied from the self.include list ( @@ -207,11 +203,13 @@ def cancelled_callback(self, stdin, stdout): @property def eplus_home(self): - eplus_exe, eplus_home = paths_from_version(self.idf.as_version.dash) - if not Path(eplus_home).exists(): - raise EnergyPlusVersionError( - msg=f"No EnergyPlus Executable found for version " - f"{EnergyPlusVersion(self.idf.as_version)}" - ) + """Get the version-dependant directory where executables are installed.""" + if self.idf.file_version <= EnergyPlusVersion("7.2"): + install_dir = self.idf.file_version.current_install_dir / "bin" else: - return Path(eplus_home) + install_dir = ( + self.idf.file_version.current_install_dir + / "PreProcess" + / "GrndTempCalc" + ) + return install_dir diff --git a/archetypal/eplus_interface/expand_objects.py b/archetypal/eplus_interface/expand_objects.py index 710920f0..817bd946 100644 --- a/archetypal/eplus_interface/expand_objects.py +++ b/archetypal/eplus_interface/expand_objects.py @@ -59,9 +59,8 @@ def run(self): self.epw = self.idf.epw.copy(tmp / "in.epw").expand() self.idfname = Path(self.idf.savecopy(tmp / "in.idf")).expand() self.idd = self.idf.iddname.copy(tmp / "Energy+.idd").expand() - self.expandobjectsexe = Path( - shutil.which("ExpandObjects", path=self.eplus_home.expand()) - ).copy2(tmp) + expand_object_exe = shutil.which("ExpandObjects", path=self.eplus_home) + self.expandobjectsexe = Path(expand_object_exe).copy2(tmp) self.run_dir = Path(tmp).expand() # Run ExpandObjects Program @@ -151,11 +150,9 @@ def cancelled_callback(self, stdin, stdout): @property def eplus_home(self): - eplus_exe, eplus_home = paths_from_version(self.idf.as_version.dash) - if not Path(eplus_home).exists(): - raise EnergyPlusVersionError( - msg=f"No EnergyPlus Executable found for version " - f"{EnergyPlusVersion(self.idf.as_version)}" - ) + """Get the version-dependant directory where executables are installed.""" + if self.idf.file_version <= EnergyPlusVersion("7.2"): + install_dir = self.idf.file_version.current_install_dir / "bin" else: - return Path(eplus_home) + install_dir = self.idf.file_version.current_install_dir + return install_dir diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 56a2e53d..04bec17b 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -58,12 +58,10 @@ def run(self): # Get executable using shutil.which (determines the extension based on # the platform, eg: .exe. And copy the executable to tmp - self.slabexe = Path( - shutil.which("Slab", path=self.eplus_home / "PreProcess" / "GrndTempCalc") - ).copy(self.run_dir) - self.slabidd = ( - self.eplus_home / "PreProcess" / "GrndTempCalc" / "SlabGHT.idd" - ).copy(self.run_dir) + self.slabexe = Path(shutil.which("Slab", path=self.eplus_home)).copy( + self.run_dir + ) + self.slabidd = (self.eplus_home / "SlabGHT.idd").copy(self.run_dir) # The GHTin.idf file is copied from the self.include list (added by # ExpandObjects. If self.include is empty, no need to run Slab. @@ -164,11 +162,13 @@ def cancelled_callback(self, stdin, stdout): @property def eplus_home(self): - eplus_exe, eplus_home = paths_from_version(self.idf.as_version.dash) - if not Path(eplus_home).exists(): - raise EnergyPlusVersionError( - msg=f"No EnergyPlus Executable found for version " - f"{EnergyPlusVersion(self.idf.as_version)}" - ) + """Get the version-dependant directory where executables are installed.""" + if self.idf.file_version <= EnergyPlusVersion("7.2"): + install_dir = self.idf.file_version.current_install_dir / "bin" else: - return Path(eplus_home) + install_dir = ( + self.idf.file_version.current_install_dir + / "PreProcess" + / "GrndTempCalc" + ) + return install_dir diff --git a/archetypal/eplus_interface/transition.py b/archetypal/eplus_interface/transition.py index b75a6267..945d3cd0 100644 --- a/archetypal/eplus_interface/transition.py +++ b/archetypal/eplus_interface/transition.py @@ -171,6 +171,9 @@ def run(self): generator = TransitionExe(self.idf, tmp_dir=tmp) + # set the initial version from which we are transitioning + last_successful_transition = self.idf.file_version + for trans in tqdm( generator, total=len(generator.transitions), @@ -214,10 +217,14 @@ def run(self): time.time() - start_time ) ) + last_successful_transition = trans.trans self.success_callback() for line in self.p.stderr: self.msg_callback(line.decode("utf-8")) else: + # set the version of the IDF the latest it was able to transition + # to. + self.idf.as_version = last_successful_transition self.msg_callback("Transition failed") self.failure_callback() diff --git a/archetypal/eplus_interface/version.py b/archetypal/eplus_interface/version.py index 830e69d8..0bc3f883 100644 --- a/archetypal/eplus_interface/version.py +++ b/archetypal/eplus_interface/version.py @@ -9,9 +9,7 @@ from path import Path from archetypal import settings -from archetypal.eplus_interface.exceptions import ( - InvalidEnergyPlusVersion, -) +from archetypal.eplus_interface.exceptions import InvalidEnergyPlusVersion class EnergyPlusVersion(Version): diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 60f3c231..f27823a8 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -288,23 +288,24 @@ def __init__( self.upgrade(to_version=self.as_version, overwrite=False) finally: # Set model outputs - self._outputs = Outputs(idf=self) + self._outputs = Outputs(idf=self, include_html=False, include_sqlite=False) if self.prep_outputs: - ( - self._outputs.add_basics() - .add_umi_template_outputs() - .add_custom(outputs=self.prep_outputs) - .add_profile_gas_elect_ouputs() - .apply() - ) + self._outputs.include_html = True + self._outputs.include_sqlite = True + self._outputs.add_basics() + if isinstance(self.prep_outputs, list): + self._outputs.add_custom(outputs=self.prep_outputs) + self._outputs.add_profile_gas_elect_outputs() + self._outputs.add_umi_template_outputs() + self._outputs.apply() @property def outputtype(self): + """Get or set the outputtype for the idf string representation of self.""" return self._outputtype @outputtype.setter def outputtype(self, value): - """Get or set the outputtype for the idf string representation of self.""" assert value in self.OUTPUTTYPES, ( f'Invalid input "{value}" for output_type.' f"\nOutput type must be one of the following: {self.OUTPUTTYPES}" @@ -688,6 +689,10 @@ def prep_outputs(self): @prep_outputs.setter def prep_outputs(self, value): + assert isinstance(value, (bool, list)), ( + f"Expected bool or list of dict for " + f"SimulationOutput outputs. Got {type(value)}." + ) self._prep_outputs = value @property @@ -1522,6 +1527,29 @@ def process_results(self): else: return results + def add_idf_object_from_idf_string(self, idf_string): + """Add an IDF object (or more than one) from an EnergyPlus text string. + + Args: + idf_string (str): A text string fully describing an EnergyPlus object. + """ + loaded_string = IDF( + StringIO(idf_string), + file_version=self.file_version, + as_version=self.as_version, + prep_outputs=False, + ) + added_objects = [] + for sequence in loaded_string.idfobjects.values(): + if sequence: + for obj in sequence: + data = obj.to_dict() + key = data.pop("key") + added_objects.append(self.newidfobject(key=key.upper(), **data)) + # added_objects.extend(self.addidfobjects(list(sequence))) + del loaded_string # remove + return added_objects + def upgrade(self, to_version=None, overwrite=True): """`EnergyPlus` idf version updater using local transition program. @@ -1906,6 +1934,8 @@ def removeidfobject(self, idfobject): def removeidfobjects(self, idfobjects: Iterable[EpBunch]): """Remove an IDF object from the model. + Resetting dependent variables will wait after all objects have been removed. + Args: idfobjects: The object to remove from the model. """ diff --git a/archetypal/idfclass/meters.py b/archetypal/idfclass/meters.py index f28cb7e7..748a1585 100644 --- a/archetypal/idfclass/meters.py +++ b/archetypal/idfclass/meters.py @@ -83,7 +83,10 @@ def values( # the environment_type is specified by the simulationcontrol. try: for ctrl in self._idf.idfobjects["SIMULATIONCONTROL"]: - if ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() == "yes": + if ( + ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() + == "yes" + ): environment_type = 3 else: environment_type = 1 diff --git a/archetypal/idfclass/outputs.py b/archetypal/idfclass/outputs.py index ab2fd530..5def7f0f 100644 --- a/archetypal/idfclass/outputs.py +++ b/archetypal/idfclass/outputs.py @@ -1,3 +1,6 @@ +from typing import Iterable + + class Outputs: """Handles preparation of EnergyPlus outputs. Different instance methods allow to chain methods together and to add predefined bundles of outputs in @@ -6,18 +9,256 @@ class Outputs: Examples: >>> from archetypal import IDF >>> idf = IDF(prep_outputs=False) # True be default - >>> idf.outputs.add_output_control().add_umi_ouputs( - >>> ).add_profile_gas_elect_ouputs().apply() + >>> idf.outputs.add_output_control().add_umi_outputs( + >>> ).add_profile_gas_elect_outputs().apply() """ - def __init__(self, idf): + REPORTING_FREQUENCIES = ("Annual", "Monthly", "Daily", "Hourly", "Timestep") + COOLING = ( + "Zone Ideal Loads Supply Air Total Cooling Energy", + "Zone Ideal Loads Zone Sensible Cooling Energy", + "Zone Ideal Loads Zone Latent Cooling Energy", + ) + HEATING = ( + "Zone Ideal Loads Supply Air Total Heating Energy", + "Zone Ideal Loads Zone Sensible Heating Energy", + "Zone Ideal Loads Zone Latent Heating Energy", + ) + LIGHTING = ( + "Zone Lights Electric Energy", + "Zone Lights Total Heating Energy", + ) + ELECTRIC_EQUIP = ( + "Zone Electric Equipment Electricity Energy", + "Zone Electric Equipment Total Heating Energy", + "Zone Electric Equipment Radiant Heating Energy", + "Zone Electric Equipment Convective Heating Energy", + "Zone Electric Equipment Latent Gain Energy", + ) + GAS_EQUIP = ( + "Zone Gas Equipment NaturalGas Energy", + "Zone Gas Equipment Total Heating Energy", + "Zone Gas Equipment Radiant Heating Energy", + "Zone Gas Equipment Convective Heating Energy", + "Zone Gas Equipment Latent Gain Energy", + ) + HOT_WATER = ( + "Water Use Equipment Zone Sensible Heat Gain Energy", + "Water Use Equipment Zone Latent Gain Energy", + ) + PEOPLE_GAIN = ( + "Zone People Total Heating Energy", + "Zone People Sensible Heating Energy", + "Zone People Latent Gain Energy", + ) + SOLAR_GAIN = ("Zone Windows Total Transmitted Solar Radiation Energy",) + INFIL_GAIN = ( + "Zone Infiltration Total Heat Gain Energy", + "Zone Infiltration Sensible Heat Gain Energy", + "Zone Infiltration Latent Heat Gain Energy", + "AFN Zone Infiltration Sensible Heat Gain Energy", + "AFN Zone Infiltration Latent Heat Gain Energy", + ) + INFIL_LOSS = ( + "Zone Infiltration Total Heat Loss Energy", + "Zone Infiltration Sensible Heat Loss Energy", + "Zone Infiltration Latent Heat Loss Energy", + "AFN Zone Infiltration Sensible Heat Loss Energy", + "AFN Zone Infiltration Latent Heat Loss Energy", + ) + VENT_LOSS = ( + "Zone Ideal Loads Zone Total Heating Energy", + "Zone Ideal Loads Zone Sensible Heating Energy", + "Zone Ideal Loads Zone Latent Heating Energy", + ) + VENT_GAIN = ( + "Zone Ideal Loads Zone Total Cooling Energy", + "Zone Ideal Loads Zone Sensible Cooling Energy", + "Zone Ideal Loads Zone Latent Cooling Energy", + ) + NAT_VENT_GAIN = ( + "Zone Ventilation Total Heat Gain Energy", + "Zone Ventilation Sensible Heat Gain Energy", + "Zone Ventilation Latent Heat Gain Energy", + "AFN Zone Ventilation Sensible Heat Gain Energy", + "AFN Zone Ventilation Latent Heat Gain Energy", + ) + NAT_VENT_LOSS = ( + "Zone Ventilation Total Heat Loss Energy", + "Zone Ventilation Sensible Heat Loss Energy", + "Zone Ventilation Latent Heat Loss Energy", + "AFN Zone Ventilation Sensible Heat Loss Energy", + "AFN Zone Ventilation Latent Heat Loss Energy", + ) + OPAQUE_ENERGY_FLOW = ("Surface Average Face Conduction Heat Transfer Energy",) + WINDOW_LOSS = ("Surface Window Heat Loss Energy",) + WINDOW_GAIN = ("Surface Window Heat Gain Energy",) + + def __init__( + self, + idf, + variables=None, + meters=None, + outputs=None, + reporting_frequency="Hourly", + include_sqlite=True, + include_html=True, + unit_conversion=None, + ): """Initialize an outputs object. Args: idf (IDF): the IDF object for wich this outputs object is created. """ self.idf = idf - self._outputs = [] + self.output_variables = set( + a.Variable_Name for a in idf.idfobjects["Output:Variable".upper()] + ) + self.output_meters = set( + getattr(a, "Key_Name") + if getattr(a, "Key_Name") + else getattr(a, "Name") # Backwards compatibility + for a in idf.idfobjects["Output:Meter".upper()] + ) + # existing_ouputs = [] + # for key in idf.getiddgroupdict()["Output Reporting"]: + # if key not in ["Output:Variable", "Output:Meter"]: + # existing_ouputs.extend(idf.idfobjects[key.upper()].to_dict()) + # self.other_outputs = existing_ouputs + self.other_outputs = outputs + + self.output_variables += tuple(variables or ()) + self.output_meters += tuple(meters or ()) + self.other_outputs += tuple(outputs or ()) + self.reporting_frequency = reporting_frequency + self.include_sqlite = include_sqlite + self.include_html = include_html + self.unit_conversion = unit_conversion + + @property + def unit_conversion(self): + return self._unit_conversion + + @unit_conversion.setter + def unit_conversion(self, value): + if not value: + value = "None" + assert value in ["None", "JtoKWH", "JtoMJ", "JtoGJ", "InchPound"] + for obj in self.idf.idfobjects["OutputControl:Table:Style".upper()]: + obj.Unit_Conversion = value + self._unit_conversion = value + + @property + def include_sqlite(self): + """Get or set a boolean for whether a SQLite report should be generated.""" + return self._include_sqlite + + @include_sqlite.setter + def include_sqlite(self, value): + value = bool(value) + if value: + self.add_sql().apply() + else: + # if False, try to remove sql, if exists. + for obj in self.idf.idfobjects["Output:SQLite".upper()]: + self.idf.removeidfobject(obj) + self._include_sqlite = value + + @property + def include_html(self): + """Get or set a boolean for whether an HTML report should be generated.""" + return self._include_html + + @include_html.setter + def include_html(self, value): + value = bool(value) + if value: + self.add_output_control().apply() + else: + # if False, try to remove sql, if exists. + for obj in self.idf.idfobjects["OutputControl:Table:Style".upper()]: + obj.Column_Separator = "Comma" + self._include_html = value + + @property + def output_variables(self) -> tuple: + """Get or set a tuple of EnergyPlus simulation output variables.""" + return tuple(sorted(self._output_variables)) + + @output_variables.setter + def output_variables(self, value): + if value is not None: + assert not isinstance( + value, (str, bytes) + ), f"Expected list or tuple. Got {type(value)}." + values = [] + for output in value: + values.append(str(output)) + value = set(values) + else: + value = set() + self._output_variables = value + + @property + def output_meters(self): + """Get or set a tuple of EnergyPlus simulation output meters.""" + return tuple(sorted(self._output_meters)) + + @output_meters.setter + def output_meters(self, value): + if value is not None: + assert not isinstance( + value, (str, bytes) + ), f"Expected list or tuple. Got {type(value)}." + values = [] + for output in value: + values.append(str(output)) + value = set(values) + else: + value = set() + self._output_meters = value + + @property + def other_outputs(self): + """Get or set a list of outputs.""" + return self._other_outputs + + @other_outputs.setter + def other_outputs(self, value): + if value is not None: + assert all( + isinstance(item, dict) for item in value + ), f"Expected list of dict. Got {type(value)}." + values = [] + for output in value: + values.append(output) + value = values + else: + value = [] + self._other_outputs = value + + @property + def reporting_frequency(self): + """Get or set the reporting frequency of outputs. + + Choose from the following: + + * Annual + * Monthly + * Daily + * Hourly + * Timestep + """ + return self._reporting_frequency + + @reporting_frequency.setter + def reporting_frequency(self, value): + value = value.title() + assert value in self.REPORTING_FREQUENCIES, ( + f"reporting_frequency {value} is not recognized.\nChoose from the " + f"following:\n{self.REPORTING_FREQUENCIES}" + ) + self._reporting_frequency = value def add_custom(self, outputs): """Add custom-defined outputs as a list of objects. @@ -38,8 +279,14 @@ def add_custom(self, outputs): Returns: Outputs: self """ - if isinstance(outputs, list): - self._outputs.extend(outputs) + assert isinstance(outputs, Iterable), "outputs must be some sort of iterable" + for output in outputs: + if "meter" in output["key"].lower(): + self._output_meters.add(output) + elif "variable" in output["key"].lower(): + self._output_variables.add(output) + else: + self._other_outputs.append(output) return self def add_basics(self): @@ -47,7 +294,6 @@ def add_basics(self): return ( self.add_summary_report() .add_output_control() - .add_sql() .add_schedules() .add_meter_variables() ) @@ -55,8 +301,8 @@ def add_basics(self): def add_schedules(self): """Adds Schedules object""" outputs = [{"key": "Output:Schedules".upper(), **dict(Key_Field="Hourly")}] - - self._outputs.extend(outputs) + for output in outputs: + self._other_outputs.append(output) return self def add_meter_variables(self, format="IDF"): @@ -75,7 +321,8 @@ def add_meter_variables(self, format="IDF"): Outputs: self """ outputs = [dict(key="Output:VariableDictionary".upper(), Key_Field=format)] - self._outputs.extend(outputs) + for output in outputs: + self._other_outputs.append(output) return self def add_summary_report(self, summary="AllSummary"): @@ -105,8 +352,8 @@ def add_summary_report(self, summary="AllSummary"): **dict(Report_1_Name=summary), } ] - - self._outputs.extend(outputs) + for output in outputs: + self._other_outputs.append(output) return self def add_sql(self, sql_output_style="SimpleAndTabular"): @@ -125,9 +372,12 @@ def add_sql(self, sql_output_style="SimpleAndTabular"): Returns: Outputs: self """ - output = {"key": "Output:SQLite".upper(), **dict(Option_Type=sql_output_style)} + outputs = [ + {"key": "Output:SQLite".upper(), **dict(Option_Type=sql_output_style)} + ] - self._outputs.extend([output]) + for output in outputs: + self._other_outputs.append(output) return self def add_output_control(self, output_control_table_style="CommaAndHTML"): @@ -139,6 +389,17 @@ def add_output_control(self, output_control_table_style="CommaAndHTML"): Returns: Outputs: self """ + assert output_control_table_style in [ + "Comma", + "Tab", + "Fixed", + "HTML", + "XML", + "CommaAndHTML", + "TabAndHTML", + "XMLAndHTML", + "All", + ] outputs = [ { "key": "OutputControl:Table:Style".upper(), @@ -146,195 +407,52 @@ def add_output_control(self, output_control_table_style="CommaAndHTML"): } ] - self._outputs.extend(outputs) + for output in outputs: + self._other_outputs.append(output) return self def add_umi_template_outputs(self): """Adds the necessary outputs in order to create an UMI template.""" # list the outputs here - outputs = [ - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Air System Total Heating Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Air System Total Cooling Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Zone Ideal Loads Zone Total Cooling Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Zone Ideal Loads Zone Total Heating Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Zone Thermostat Heating Setpoint Temperature", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Zone Thermostat Cooling Setpoint Temperature", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Heat Exchanger Total Heating Rate", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Heat Exchanger Sensible Effectiveness", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Heat Exchanger Latent Effectiveness", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Water Heater Heating Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Air System Outdoor Air Minimum Flow Fraction", - Reporting_Frequency="hourly", - ), - }, - { - "key": "OUTPUT:METER", - **dict( - Key_Name="HeatRejection:EnergyTransfer", - Reporting_Frequency="hourly", - ), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Heating:EnergyTransfer", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Cooling:EnergyTransfer", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict( - Key_Name="Heating:DistrictHeating", Reporting_Frequency="hourly" - ), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Heating:Electricity", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Heating:Gas", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict( - Key_Name="Cooling:DistrictCooling", Reporting_Frequency="hourly" - ), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Cooling:Electricity", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Cooling:Electricity", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Cooling:Gas", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict( - Key_Name="WaterSystems:EnergyTransfer", Reporting_Frequency="hourly" - ), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Fans:Electricity", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Pumps:Electricity", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict( - Key_Name="Refrigeration:Electricity", Reporting_Frequency="hourly" - ), - }, - { - "key": "OUTPUT:METER", - **dict( - Key_Name="Refrigeration:EnergyTransfer", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Meter".upper(), - **dict( - Key_Name="HeatingCoils:EnergyTransfer", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Meter".upper(), - **dict( - Key_Name="Baseboard:EnergyTransfer", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Meter".upper(), - **dict( - Key_Name="HeatRejection:Electricity", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Meter".upper(), - **dict( - Key_Name="CoolingCoils:EnergyTransfer", - Reporting_Frequency="hourly", - ), - }, + variables = [ + "Air System Outdoor Air Minimum Flow Fraction", + "Air System Total Cooling Energy", + "Air System Total Heating Energy", + "Heat Exchanger Latent Effectiveness", + "Heat Exchanger Sensible Effectiveness", + "Heat Exchanger Total Heating Rate", + "Water Heater Heating Energy", + "Zone Ideal Loads Zone Total Cooling Energy", + "Zone Ideal Loads Zone Total Heating Energy", + "Zone Thermostat Cooling Setpoint Temperature", + "Zone Thermostat Heating Setpoint Temperature", ] + for output in variables: + self._output_variables.add(output) - self._outputs.extend(outputs) + meters = [ + "Baseboard:EnergyTransfer", + "Cooling:DistrictCooling", + "Cooling:Electricity", + "Cooling:Electricity", + "Cooling:EnergyTransfer", + "Cooling:Gas", + "CoolingCoils:EnergyTransfer", + "Fans:Electricity", + "HeatRejection:Electricity", + "HeatRejection:EnergyTransfer", + "Heating:DistrictHeating", + "Heating:Electricity", + "Heating:EnergyTransfer", + "Heating:Gas", + "HeatingCoils:EnergyTransfer", + "Pumps:Electricity", + "Refrigeration:Electricity", + "Refrigeration:EnergyTransfer", + "WaterSystems:EnergyTransfer", + ] + for meter in meters: + self._output_meters.add(meter) return self def add_dxf(self): @@ -344,90 +462,209 @@ def add_dxf(self): **dict(Report_Type="DXF", Report_Specifications_1="ThickPolyline"), } ] - self._outputs.extend(outputs) + for output in outputs: + self._other_outputs.append(output) return self - def add_umi_ouputs(self): + def add_umi_outputs(self): """Adds the necessary outputs in order to return the same energy profile as in UMI. """ # list the outputs here outputs = [ - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Air System Total Heating Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Air System Total Cooling Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Zone Ideal Loads Zone Total Cooling Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Zone Ideal Loads Zone Total Heating Energy", - Reporting_Frequency="hourly", - ), - }, - { - "key": "Output:Variable".upper(), - **dict( - Variable_Name="Water Heater Heating Energy", - Reporting_Frequency="hourly", - ), - }, + "Air System Total Heating Energy", + "Air System Total Cooling Energy", + "Zone Ideal Loads Zone Total Cooling Energy", + "Zone Ideal Loads Zone Total Heating Energy", + "Water Heater Heating Energy", ] - - self._outputs.extend(outputs) + for output in outputs: + self._output_variables.add(output) return self - def add_profile_gas_elect_ouputs(self): + def add_sensible_heat_gain_summary_components(self): + hvac_input_sensible_air_heating = [ + "Zone Air Heat Balance System Air Transfer Rate", + "Zone Air Heat Balance System Convective Heat Gain Rate", + ] + hvac_input_sensible_air_cooling = [ + "Zone Air Heat Balance System Air Transfer Rate", + "Zone Air Heat Balance System Convective Heat Gain Rate", + ] + + hvac_input_heated_surface_heating = [ + "Zone Radiant HVAC Heating Energy", + "Zone Ventilated Slab Radiant Heating Energy", + ] + + hvac_input_cooled_surface_cooling = [ + "Zone Radiant HVAC Cooling Energy", + "Zone Ventilated Slab Radiant Cooling Energy", + ] + people_sensible_heat_addition = ["Zone People Sensible Heating Energy"] + + lights_sensible_heat_addition = ["Zone Lights Total Heating Energy"] + + equipment_sensible_heat_addition_and_equipment_sensible_heat_removal = [ + "Zone Electric Equipment Radiant Heating Energy", + "Zone Gas Equipment Radiant Heating Energy", + "Zone Steam Equipment Radiant Heating Energy", + "Zone Hot Water Equipment Radiant Heating Energy", + "Zone Other Equipment Radiant Heating Energy", + "Zone Electric Equipment Convective Heating Energy", + "Zone Gas Equipment Convective Heating Energy", + "Zone Steam Equipment Convective Heating Energy", + "Zone Hot Water Equipment Convective Heating Energy", + "Zone Other Equipment Convective Heating Energy", + ] + + window_heat_addition_and_window_heat_removal = [ + "Zone Windows Total Heat Gain Energy" + ] + + interzone_air_transfer_heat_addition_and_interzone_air_transfer_heat_removal = [ + "Zone Air Heat Balance Interzone Air Transfer Rate" + ] + + infiltration_heat_addition_and_infiltration_heat_removal = [ + "Zone Air Heat Balance Outdoor Air Transfer Rate" + ] + + tuple(map(self._output_variables.add, hvac_input_sensible_air_heating)) + tuple(map(self._output_variables.add, hvac_input_sensible_air_cooling)) + tuple(map(self._output_variables.add, hvac_input_heated_surface_heating)) + tuple(map(self._output_variables.add, hvac_input_cooled_surface_cooling)) + tuple(map(self._output_variables.add, people_sensible_heat_addition)) + tuple(map(self._output_variables.add, lights_sensible_heat_addition)) + tuple( + map( + self._output_variables.add, + equipment_sensible_heat_addition_and_equipment_sensible_heat_removal, + ) + ) + tuple( + map( + self._output_variables.add, window_heat_addition_and_window_heat_removal + ) + ) + tuple( + map( + self._output_variables.add, + interzone_air_transfer_heat_addition_and_interzone_air_transfer_heat_removal, + ) + ) + tuple( + map( + self._output_variables.add, + infiltration_heat_addition_and_infiltration_heat_removal, + ) + ) + + # The Opaque Surface Conduction and Other Heat Addition and Opaque Surface Conduction and Other Heat Removal + # columns are also calculated on an timestep basis as the negative value of the other removal and gain columns + # so that the total for the timestep sums to zero. These columns are derived strictly from the other columns. + + def add_load_balance_components(self): + + for group in [ + self.COOLING, + self.HEATING, + self.LIGHTING, + self.ELECTRIC_EQUIP, + self.GAS_EQUIP, + self.HOT_WATER, + self.PEOPLE_GAIN, + self.SOLAR_GAIN, + self.INFIL_GAIN, + self.INFIL_LOSS, + self.VENT_LOSS, + self.VENT_GAIN, + self.NAT_VENT_GAIN, + self.NAT_VENT_LOSS, + self.OPAQUE_ENERGY_FLOW, + self.WINDOW_LOSS, + self.WINDOW_GAIN, + ]: + for item in group: + self._output_variables.add(item) + + def add_profile_gas_elect_outputs(self): """Adds the following meters: Electricity:Facility, Gas:Facility, WaterSystems:Electricity, Heating:Electricity, Cooling:Electricity """ # list the outputs here outputs = [ - { - "key": "OUTPUT:METER", - **dict(Key_Name="Electricity:Facility", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Gas:Facility", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict( - Key_Name="WaterSystems:Electricity", Reporting_Frequency="hourly" - ), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Heating:Electricity", Reporting_Frequency="hourly"), - }, - { - "key": "OUTPUT:METER", - **dict(Key_Name="Cooling:Electricity", Reporting_Frequency="hourly"), - }, + "Electricity:Facility", + "Gas:Facility", + "WaterSystems:Electricity", + "Heating:Electricity", + "Cooling:Electricity", ] - self._outputs.extend(outputs) + for output in outputs: + self._output_meters.add(output) return self + def add_hvac_energy_use(self): + """Add outputs for HVAC energy use when detailed systems are assigned. + + This includes a range of outputs for different pieces of equipment, + which is meant to catch all energy-consuming parts of a system. + (eg. chillers, boilers, coils, humidifiers, fans, pumps). + """ + outputs = [ + "Baseboard Electricity Energy", + "Boiler NaturalGas Energy", + "Chiller Electricity Energy", + "Chiller Heater System Cooling Electricity Energy", + "Chiller Heater System Heating Electricity Energy", + "Cooling Coil Electricity Energy", + "Cooling Tower Fan Electricity Energy", + "District Cooling Chilled Water Energy", + "District Heating Hot Water Energy", + "Evaporative Cooler Electricity Energy", + "Fan Electricity Energy", + "Heating Coil Electricity Energy", + "Heating Coil NaturalGas Energy", + "Heating Coil Total Heating Energy", + "Hot_Water_Loop_Central_Air_Source_Heat_Pump Electricity Consumption", + "Humidifier Electricity Energy", + "Pump Electricity Energy", + "VRF Heat Pump Cooling Electricity Energy", + "VRF Heat Pump Crankcase Heater Electricity Energy", + "VRF Heat Pump Defrost Electricity Energy", + "VRF Heat Pump Heating Electricity Energy", + "Zone VRF Air Terminal Cooling Electricity Energy", + "Zone VRF Air Terminal Heating Electricity Energy", + ] + for output in outputs: + self._output_variables.add(output) + def apply(self): """Applies the outputs to the idf model. Modifies the model by calling :meth:`~archetypal.idfclass.idf.IDF.newidfobject`""" - for output in self._outputs: + for output in self.output_variables: + self.idf.newidfobject( + key="Output:Variable".upper(), + **dict( + Variable_Name=output, Reporting_Frequency=self.reporting_frequency + ), + ) + for meter in self.output_meters: + self.idf.newidfobject( + key="Output:Meter".upper(), + **dict(Key_Name=meter, Reporting_Frequency=self.reporting_frequency), + ) + for output in self.other_outputs: + key = output.pop("key", None) + if key: + output["key"] = key.upper() self.idf.newidfobject(**output) return self + + def __repr__(self): + variables = "OutputVariables:\n {}".format("\n ".join(self.output_variables)) + meters = "OutputMeters:\n {}".format("\n ".join(self.output_meters)) + outputs = "Outputs:\n {}".format( + "\n ".join((a["key"] for a in self.other_outputs)) + ) + return "\n".join([variables, meters, outputs]) diff --git a/archetypal/idfclass/variables.py b/archetypal/idfclass/variables.py index 2a1a22be..52f0c1e3 100644 --- a/archetypal/idfclass/variables.py +++ b/archetypal/idfclass/variables.py @@ -74,8 +74,10 @@ def values( # the environment_type is specified by the simulationcontrol. try: for ctrl in self._idf.idfobjects["SIMULATIONCONTROL"]: - if ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() \ - == "yes": + if ( + ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() + == "yes" + ): environment_type = 3 else: environment_type = 1 diff --git a/archetypal/umi_template.py b/archetypal/umi_template.py index e1c5d633..210e14f3 100644 --- a/archetypal/umi_template.py +++ b/archetypal/umi_template.py @@ -257,6 +257,7 @@ def from_idf_files( def template_complexity_reduction(idfname, epw, **kwargs): """Wrap IDF, simulate and BuildingTemplate for parallel processing.""" idf = IDF(idfname, epw=epw, **kwargs) + idf._outputs.add_umi_template_outputs() # remove daylight saving time modifiers for daylight in idf.idfobjects["RunPeriodControl:DaylightSavingTime".upper()]: diff --git a/tests/test_outputs.py b/tests/test_outputs.py new file mode 100644 index 00000000..321fcd88 --- /dev/null +++ b/tests/test_outputs.py @@ -0,0 +1,62 @@ +import pytest + +from archetypal import IDF +from archetypal.idfclass import Outputs + + +class TestOutput: + @pytest.fixture() + def idf(self): + yield IDF(prep_outputs=False) + + def test_output_init(self, idf): + """Test initialization of the Output class.""" + + outputs = Outputs(idf) + str(outputs) # test the string representation of the object + + assert len(outputs.other_outputs) == 2 + assert len(outputs.output_variables) == 0 + assert len(outputs.output_meters) == 0 + + outputs.add_umi_template_outputs() + assert len(outputs.output_variables) > 1 + assert len(outputs.output_meters) > 1 + assert outputs.reporting_frequency == "Hourly" + assert outputs.include_sqlite + assert outputs.include_html + + def test_output_properties(self, idf): + """Test changing properties of Outputs.""" + outputs = Outputs(idf) + + outputs.output_variables = ["Air System Outdoor Air Minimum Flow Fraction"] + assert outputs.output_variables == ( + "Air System Outdoor Air Minimum Flow Fraction", + ) + outputs.reporting_frequency = "daily" # lower case + assert outputs.reporting_frequency == "Daily" # should be upper case + outputs.unit_conversion = "InchPound" + assert outputs.unit_conversion == "InchPound" + outputs.include_sqlite = False + assert not outputs.include_sqlite + outputs.include_html = True + assert outputs.include_html + + with pytest.raises(AssertionError): + outputs.output_variables = ( + "Zone Ideal Loads Supply Air Total Cooling Energy" + ) + with pytest.raises(AssertionError): + outputs.reporting_frequency = "annually" + with pytest.raises(AssertionError): + outputs.other_outputs = "ComponentSizingSummary" + with pytest.raises(AssertionError): + outputs.unit_conversion = "IP" + + def test_add_basics(self, idf): + """Test the Output add_basics method""" + outputs = Outputs(idf).add_basics() + assert len(outputs.output_variables) == 0 + assert len(outputs.output_meters) == 0 + assert len(outputs.other_outputs) == 6