From 1fd3a8fff0b345085ab231700717a12879476029 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 14 Oct 2019 21:16:23 -0400 Subject: [PATCH] feat(output): Add module to handle simulation outputs This commit adds a module to facilitate the requesting of outputs from the simulation. --- honeybee_energy/simulation/output.py | 533 +++++++++++++++++++++++++ honeybee_energy/simulationparameter.py | 64 ++- tests/model_extend_test.py | 4 +- tests/simulation_output_test.py | 201 ++++++++++ tests/simulationparameter_test.py | 12 + 5 files changed, 803 insertions(+), 11 deletions(-) create mode 100644 honeybee_energy/simulation/output.py create mode 100644 tests/simulation_output_test.py diff --git a/honeybee_energy/simulation/output.py b/honeybee_energy/simulation/output.py new file mode 100644 index 000000000..b35e1f50f --- /dev/null +++ b/honeybee_energy/simulation/output.py @@ -0,0 +1,533 @@ +# coding=utf-8 +"""Object to hold EnergyPlus simulation outputs.""" +from __future__ import division + +from ..reader import parse_idf_string +from ..writer import generate_idf_string + + +class SimulationOutput(object): + """Object to hold EnergyPlus simulation outputs. + + Properties: + * outputs + * reporting_frequency + * include_sqlite + * include_html + * summary_reports + """ + __slots__ = ('_outputs', '_reporting_frequency', '_include_sqlite', + '_include_html', '_summary_reports') + REPORTING_FREQUENCIES = ('Annual', 'Monthly', 'Daily', 'Hourly', 'Timestep') + + def __init__(self, outputs=None, reporting_frequency='Hourly', include_sqlite=True, + include_html=False, summary_reports=['AllSummary']): + """Initialize SimulationOutput. + + Args: + outputs: A list of EnergyPlus output names as strings, which are requested + from the simulation. If None, no outputs will be requested. + Note that this object does not check whether the outputs exist + within the EnergyPlus IDD or are request-able from a given Model. + (eg. ['Zone Ideal Loads Supply Air Total Cooling Energy']). + Default: None. + reporting_frequency: Text for the frequency at which the outputs + are reported. Default: 'Hourly'. + Choose from the following: + * Annual + * Monthly + * Daily + * Hourly + * Timestep + include_sqlite: Boolean to note whether a SQLite report should be + generated from the simulation, which contains all of the outputs and + summary_reports. Default: True. + include_html: Boolean to note whether an HTML report should be generated + from the simulation, which contains all of the summary_reports. + Default: False. + summary_reports: A list of EnergyPlus summary report names as strings. + An empty list or None will result in no summary reports. + Default: ['AllSummary']. See the Input Output Reference SummaryReports + section for a full list of all reports that can be requested. + (https://bigladdersoftware.com/epx/docs/9-1/input-output-reference/ + output-table-summaryreports.html#outputtablesummaryreports). + """ + self.outputs = outputs + self.reporting_frequency = reporting_frequency + self.include_sqlite = include_sqlite + self.include_html = include_html + self.summary_reports = summary_reports + + @property + def outputs(self): + """Get or set a tuple of EnergyPlus output names as strings. + + These outputs will be requested from the simulation and, if None, + no outputs will be requested. + """ + return tuple(sorted(self._outputs)) + + @outputs.setter + def outputs(self, value): + if value is not None: + assert not isinstance(value, (str, bytes)), 'Extected list or tuple for ' \ + 'SimulationOutput outputs. Got {}.'.format(type(value)) + if not isinstance(value, set): + value = set(value) + for output in value: + assert isinstance(output, str), 'Output {} is not valid'.format(output) + else: + value = set() + self._outputs = value + + @property + def reporting_frequency(self): + """Get or set text for the frequency at which the outputs are reported. + + 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, 'reporting_frequency {} ' \ + 'is not recognized.\nChoose from the following:\n{}'.format( + value, self.REPORTING_FREQUENCIES) + self._reporting_frequency = 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): + self._include_sqlite = bool(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): + self._include_html = bool(value) + + @property + def summary_reports(self): + """Get or set a tuple of EnergyPlus summary report names as strings. + + These reports will be requested from the simulation and, if None, + no summary reports will be written. + """ + return tuple(sorted(self._summary_reports)) + + @summary_reports.setter + def summary_reports(self, value): + if value is not None: + assert not isinstance(value, (str, bytes)), 'Extected list or tuple for ' \ + 'SimulationOutput summary_reports. Got {}.'.format(type(value)) + if not isinstance(value, set): + value = set(value) + for report in value: + assert isinstance(report, str), \ + 'SummaryReport {} is not valid'.format(report) + else: + value = set() + self._summary_reports = value + + def add_summary_report(self, report_name): + """Add another summary report to the list of requested reports. + + See the Input Output Reference SummaryReports + section for a full list of all reports that can be requested. + (https://bigladdersoftware.com/epx/docs/9-1/input-output-reference/ + output-table-summaryreports.html#outputtablesummaryreports) + + Args: + report_name: The name of an EnergyPlus simulation report to be requested + from the model. Note that this method does not check whether the + output exists within the EnergyPlus IDD. + (eg. 'AnnualBuildingUtilityPerformanceSummary'). + """ + assert isinstance(report_name, str), \ + 'SummaryReport {} is not valid'.format(report_name) + self._summary_reports.add(report_name) + + def add_output(self, output_name): + """Add another output to the list of requested outputs. + + Args: + output_name: The name of an EnergyPlus output that is requested + from the simulation. Note that this method does not check whether + the output exists within the EnergyPlus IDD or are request-able + from a given Model. + (eg. 'Zone Ideal Loads Supply Air Total Cooling Energy'). + """ + assert isinstance(output_name, str), 'Output {} is not valid'.format(output_name) + self._outputs.add(output_name) + + def add_zone_energy_use(self, load_type='All'): + """Add outputs for zone energy use when ideal air loads are assigned. + + This includes, ideal air heating + cooling, lighting, electric + gas + equipment, and fan electric energy. + + Args: + load_type: A text value to set the type of load outputs requested. + Default: 'All'. Choose from the following: + * All - all energy use including heat lost from the zone + * Total - total load added/removed from the zone (sensible + latent) + * Sensible - sensible load added/removed to the zone + * Latent - latent load added/removed to the zone + """ + load_type = load_type.title() + if load_type == 'All': + outputs = ['Zone Ideal Loads Supply Air Total Cooling Energy', + 'Zone Ideal Loads Supply Air Total Heating Energy', + 'Zone Lights Electric Energy', + 'Zone Electric Equipment Electric Energy', + 'Zone Gas Equipment Gas Energy', + 'Zone Ventilation Fan Electric Energy'] + elif load_type == 'Total': + outputs = ['Zone Ideal Loads Supply Air Total Cooling Energy', + 'Zone Ideal Loads Supply Air Total Heating Energy', + 'Zone Lights Radiant Heating Energy', + 'Zone Lights Visible Radiation Heating Energy', + 'Zone Lights Convective Heating Energy', + 'Zone Electric Equipment Total Heating Energy', + 'Zone Gas Equipment Total Heating Energy'] + elif load_type == 'Sensible': + outputs = ['Zone Ideal Loads Supply Air Sensible Cooling Energy', + 'Zone Ideal Loads Supply Air Sensible Heating Energy', + 'Zone Lights Radiant Heating Energy', + 'Zone Lights Visible Radiation Heating Energy', + 'Zone Lights Convective Heating Energy', + 'Zone Electric Equipment Radiant Heating Energy', + 'Zone Electric Equipment Convective Heating Energy', + 'Zone Gas Equipment Radiant Heating Energy', + 'Zone Gas Equipment Convective Heating Energy'] + elif load_type == 'Latent': + outputs = ['Zone Ideal Loads Supply Air Latent Cooling Energy', + 'Zone Ideal Loads Supply Air Latent Heating Energy', + 'Zone Electric Equipment Latent Gain Energy', + 'Zone Gas Equipment Latent Gain Energy'] + else: + raise ValueError('load_type {} is not valid'.format(load_type)) + for outp in outputs: + self._outputs.add(outp) + + 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 = ['Cooling Coil Electric Energy', + 'Chiller Electric Energy', + 'Boiler Gas Energy', + 'Heating Coil Total Heating Energy', + 'Heating Coil Gas Energy', + 'Heating Coil Electric Energy', + 'Humidifier Electric Energy', + 'Fan Electric Energy', + 'Pump Electric Energy', + 'Zone VRF Air Terminal Cooling Electric Energy', + 'Zone VRF Air Terminal Heating Electric Energy', + 'VRF Heat Pump Cooling Electric Energy', + 'VRF Heat Pump Heating Electric Energy', + 'Chiller Heater System Cooling Electric Energy', + 'Chiller Heater System Heating Electric Energy'] + for outp in outputs: + self._outputs.add(outp) + + def add_gains_and_losses(self, load_type='Total'): + """Add outputs for zone gains and losses. + + This includes such as people gains, solar gains, infiltration losses/gains, + and ventilation losses/gains. + + Args: + load_type: A text value to set the type of load outputs requested. + Default: 'Total'. Choose from the following: + * Total - the total load added to the zone (both sensible and latent) + * Sensible - the sensible load added to the zone + * Latent - the latent load added to the zone + """ + load_type = load_type.title() + always_sensible = ['Zone Windows Total Transmitted Solar Radiation Energy'] + if load_type == 'Total': + outputs = ['Zone People Total Heating Energy', + 'Zone Ventilation Total Heat Loss Energy', + 'Zone Ventilation Total Heat Gain Energy', + 'Zone Ideal Loads Zone Total Heating Energy', + 'Zone Ideal Loads Zone Total Cooling Energy', + 'Zone Infiltration Total Heat Loss Energy', + 'Zone Infiltration Total Heat Gain Energy'] + always_sensible + elif load_type == 'Sensible': + outputs = ['Zone People Sensible Heating Energy', + 'Zone Ventilation Sensible Heat Loss Energy', + 'Zone Ventilation Sensible Heat Gain Energy', + 'Zone Ideal Loads Zone Sensible Heating Energy', + 'Zone Ideal Loads Zone Sensible Cooling Energy', + 'Zone Infiltration Sensible Heat Loss Energy', + 'Zone Infiltration Sensible Heat Gain Energy'] + always_sensible + elif load_type == 'Latent': + outputs = ['Zone People Sensible Latent Energy', + 'Zone Ventilation Latent Heat Loss Energy', + 'Zone Ventilation Latent Heat Gain Energy', + 'Zone Ideal Loads Zone Latent Heating Energy', + 'Zone Ideal Loads Zone Latent Cooling Energy', + 'Zone Infiltration Latent Heat Loss Energy', + 'Zone Infiltration Latent Heat Gain Energy'] + else: + raise ValueError('load_type {} is not valid'.format(load_type)) + for outp in outputs: + self._outputs.add(outp) + + def add_comfort_metrics(self): + """Add outputs for zone thermal comfort analysis. + + This includes air temperature, mean radiant temperature, relative + humidity. + """ + outputs = ['Zone Operative Temperature', + 'Zone Mean Air Temperature', + 'Zone Mean Radiant Temperature', + 'Zone Air Relative Humidity'] + for outp in outputs: + self._outputs.add(outp) + + def add_stratification_variables(self): + """Add outputs for estimating stratification across a zone. + + This includes all air flow into the zone as well as all heat gain + to the air. + """ + outputs = ['Zone Ventilation Standard Density Volume Flow Rate', + 'Zone Infiltration Standard Density Volume Flow Rate', + 'Zone Mechanical Ventilation Standard Density Volume Flow Rate', + 'Zone Air Heat Balance Internal Convective Heat Gain Rate', + 'Zone Air Heat Balance Surface Convection Rate', + 'Zone Air Heat Balance System Air Transfer Rate'] + for outp in outputs: + self._outputs.add(outp) + + def add_surface_temperature(self): + """Add outputs for indoor and outdoor surface temperature.""" + outputs = ['Surface Outside Face Temperature', + 'Surface Inside Face Temperature'] + for outp in outputs: + self._outputs.add(outp) + + def add_surface_energy_flow(self): + """Add outputs for energy flow across all surfaces.""" + outputs = ['Surface Average Face Conduction Heat Transfer Energy', + 'Surface Window Heat Loss Energy', + 'Surface Window Heat Gain Energy'] + for outp in outputs: + self._outputs.add(outp) + + def add_glazing_solar(self): + """Add outputs for the transmitted solar gain through individual window surfaces. + + This includes transmitted beam, diffuse, and total solar gain. + """ + outputs = ['Surface Window Transmitted Beam Solar Radiation Energy', + 'Surface Window Transmitted Diffuse Solar Radiation Energy', + 'Surface Window Transmitted Solar Radiation Energy'] + for outp in outputs: + self._outputs.add(outp) + + def add_energy_balance_variables(self, load_type='Total'): + """Add all outputs needed to generate complete energy balance graphics. + + This includes zone energy use, zone gains and losses, and surface energy flow. + + Arge: + load_type: A text value to set the type of load outputs requested. + Default: 'Total'. Choose from the following: + * Total - the total load added to the zone (both sensible and latent) + * Sensible - the sensible load added to the zone + * Latent - the latent load added to the zone + """ + self.add_zone_energy_use(load_type) + self.add_gains_and_losses(load_type) + self.add_surface_energy_flow() + + def add_comfort_map_variables(self, include_stratification=True): + """Add all outputs needed to generate detailed thermal comfort maps. + + This includes zone air temperatures, surface temperatures, and + stratification variables. + + Args: + include_stratification: Boolean to note whether stratification variables + should be included. + """ + outputs = ['Zone Mean Air Temperature', 'Zone Air Relative Humidity'] + for outp in outputs: + self._outputs.add(outp) + self.add_surface_temperature() + if include_stratification: + self.add_stratification_variables() + + @classmethod + def from_idf(cls, table_style=None, output_variables=None, summary_reports=None, + include_sqlite=True): + """Create a RunPeriod object from an EnergyPlus IDF text string. + + Args: + table_style: An IDF OutputControl:Table:Style string. + output_variables: A list of IDF Output:Variable strings for the requested + outputs. If None, no outputs will be been requested. + summary_reports: An IDF Output:Table:SummaryReports string listing + the summary reports that are requested. If None, no summary + reports will be requested. + include_sqlite: Boolean to note whether a SQLite report should be + generated from the simulation, which contains all of the outputs and + summary_reports. Default: True. + """ + # extract the table_style + include_html = False + if table_style is not None: + style_strs = parse_idf_string(table_style, 'OutputControl:Table:Style,') + try: + include_html = True if 'HTML' in style_strs[0].upper() else False + except IndexError: + pass # shorter Table:Style without separator + + # extract the output_variables + outputs = None + frequency = 'Hourly' + if output_variables is not None: + outputs = [] + for out_str in output_variables: + ep_out_str = parse_idf_string(out_str, 'Output:Variable,') + outputs.append(ep_out_str[1]) + try: + frequency = ep_out_str[2] if ep_out_str[2] != '' else 'Hourly' + except IndexError: + pass # shorter output variable with default hourly frequencey + + # extract the summary_reports + reports = None + if summary_reports is not None: + reports = parse_idf_string(summary_reports, 'Output:Table:SummaryReports,') + + return cls(outputs, frequency, include_sqlite, include_html, reports) + + @classmethod + def from_dict(cls, data): + """Create a SimulationOutput object from a dictionary. + + Args: + data: A SimulationOutput dictionary in following the format below. + + .. code-block:: python + + { + "type": "SimulationOutput", + "outputs": ['Zone Ideal Loads Supply Air Total Cooling Energy'], + "reporting_frequency": 'Annual', + "include_sqlite": False, + "include_html": True, + "summary_reports": ['AllSummary', 'AnnualBuildingUtilityPerformanceSummary'] + } + """ + assert data['type'] == 'SimulationOutput', \ + 'Expected SimulationOutput dictionary. Got {}.'.format(data['type']) + outputs = data['outputs'] if 'outputs' in data else None + frequency = data['reporting_frequency'] if \ + 'reporting_frequency' in data else 'Hourly' + sqlite = data['include_sqlite'] if 'include_sqlite' in data else True + html = data['include_html'] if 'include_html' in data else False + reports = data['summary_reports'] if 'summary_reports' in data else None + return cls(outputs, frequency, sqlite, html, reports) + + def to_idf(self): + """Get EnergyPlus string representations of the SimulationOutput. + + Returns: + table_style: An IDF OutputControl:Table:Style string for the simulation. + output_variables: A list of IDF Output:Variable strings for the requested + outputs. Will be None if no outputs have been requested. + summary_reports: An IDF Output:Table:SummaryReports string + listing the summary reports that are requested. Will be None + if no summary reports have not been requested. + sqlite: An IDF Output:SQLite string to request the SQLite file from + the simulation. Will be None if include_sqlite is False. + surfaces_list: An IDF Output:Surfaces:List string to ensure surface + information is written into the ultimate .eio file. + """ + style = 'CommaAndHTML' if self.include_html else 'Comma' + table_style = generate_idf_string( + 'OutputControl:Table:Style', + (style, 'JtoKWH'), ('column separator', 'unit conversion')) + output_variables = [self._output_to_idf(out_p) for out_p in self.outputs] if \ + len(self._outputs) != 0 else None + r_comments = ['report {}'.format(i) for i in range(len(self._summary_reports))] + summary_reports = generate_idf_string( + 'Output:Table:SummaryReports', self.summary_reports, r_comments) if \ + len(self._summary_reports) != 0 else None + sqlite = generate_idf_string( + 'Output:SQLite', ('SimpleAndTabular,'), ('option type',)) if \ + self.include_sqlite else None + surfaces_list = generate_idf_string( + 'Output:Surfaces:List', ('Details',), ('report type',)) + return table_style, output_variables, summary_reports, sqlite, surfaces_list + + def to_dict(self): + """DaylightSavingTime dictionary representation.""" + base = {'type': 'SimulationOutput', + 'reporting_frequency': self.reporting_frequency, + 'include_sqlite': self.include_sqlite, + 'include_html': self.include_html} + if len(self._outputs) != 0: + base['outputs'] = self.outputs + if len(self._summary_reports) != 0: + base['summary_reports'] = self.summary_reports + return base + + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__() + + def _output_to_idf(self, output_name): + """Convert an output name to an IDF Output:Variable string.""" + values = ('*', output_name, self.reporting_frequency) + comments = ('key value', 'name', 'frequency') + return generate_idf_string('Output:Variable', values, comments) + + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__() + + def __copy__(self): + return SimulationOutput( + self._outputs, self.reporting_frequency, self.include_sqlite, + self.include_html, self._summary_reports) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return self.outputs + self.summary_reports + \ + (self.reporting_frequency, self.include_sqlite, self.include_html) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, SimulationOutput) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'SimulationOutput:\n {}'.format('\n '.join(self.outputs)) diff --git a/honeybee_energy/simulationparameter.py b/honeybee_energy/simulationparameter.py index 2ae194bcb..91fbd94ab 100644 --- a/honeybee_energy/simulationparameter.py +++ b/honeybee_energy/simulationparameter.py @@ -2,6 +2,7 @@ """Complete set of EnergyPlus Simulation Settings.""" from __future__ import division +from .simulation.output import SimulationOutput from .simulation.runperiod import RunPeriod from .simulation.control import SimulationControl from .simulation.shadowcalculation import ShadowCalculation @@ -18,21 +19,25 @@ class SimulationParameter(object): """Complete set of EnergyPlus Simulation Settings. Properties: + * output * run_period * timestep * simulation_control * shadow_calculation * sizing_parameter """ - __slots__ = ('_run_period', '_timestep', '_simulation_control', + __slots__ = ('_output', '_run_period', '_timestep', '_simulation_control', '_shadow_calculation', '_sizing_parameter') VALIDTIMESTEPS = (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) - def __init__(self, run_period=None, timestep=6, simulation_control=None, + def __init__(self, output=None, run_period=None, timestep=6, simulation_control=None, shadow_calculation=None, sizing_parameter=None): """Initialize SimulationParameter. Args: + output: A SimulationOutput that lists the desired outputs from the + simulation and the format in which to report them. If None, no + outputs will be requested. Default: None. run_period: A RunPeriod object to describe the time period over which to run the simulation. Default: Run for the whole year starting on Sunday. timestep: An integer for the number of timesteps per hour at which the @@ -47,12 +52,27 @@ def __init__(self, run_period=None, timestep=6, simulation_control=None, multiplied by the peak heating and cooling loads. Default: 1.25 for heating; 1.15 for cooling. """ + self.output = output self.run_period = run_period self.timestep = timestep self.simulation_control = simulation_control self.shadow_calculation = shadow_calculation self.sizing_parameter = sizing_parameter + @property + def output(self): + """Get or set a SimulationOutput object for the outputs from the simulation.""" + return self._output + + @output.setter + def output(self, value): + if value is not None: + assert isinstance(value, SimulationOutput), 'Expected SimulationOutput for ' \ + 'SimulationParameter output. Got {}.'.format(type(value)) + self._output = value + else: + self._output = SimulationOutput() + @property def run_period(self): """Get or set a RunPeriod object for the time period to run the simulation.""" @@ -138,6 +158,10 @@ def from_idf(cls, idf_string): of an IDF. """ # Regex patterns for the varios objects comprising the SimulationParameter + out_style_pattern = re.compile(r"(?i)(OutputControl:Table:Style,[\s\S]*?;)") + out_var_pattern = re.compile(r"(?i)(Output:Variable,[\s\S]*?;)") + out_report_pattern = re.compile(r"(?i)(Output:Table:SummaryReports,[\s\S]*?;)") + sqlite_pattern = re.compile(r"(?i)(Output:SQLite,[\s\S]*?;)") runper_pattern = re.compile(r"(?i)(RunPeriod,[\s\S]*?;)") holiday_pattern = re.compile(r"(?i)(RunPeriodControl:SpecialDays,[\s\S]*?;)") dls_pattern = re.compile(r"(?i)(RunPeriodControl:DaylightSavingTime,[\s\S]*?;)") @@ -147,6 +171,19 @@ def from_idf(cls, idf_string): control_pattern = re.compile(r"(?i)(SimulationControl,[\s\S]*?;)") sizing_pattern = re.compile(r"(?i)(SizingParameters,[\s\S]*?;)") + # process the outputs within the idf_string + try: + out_style_str = out_style_pattern.findall(idf_string)[0] + except IndexError: # No Table:Style in the file. + out_style_str = None + try: + out_report_str = out_report_pattern.findall(idf_string)[0] + except IndexError: # No SummaryReports in the file. Default to None. + out_report_str = None + sqlite = True if len(sqlite_pattern.findall(idf_string)) != 0 else False + output = SimulationOutput.from_idf( + out_style_str, out_var_pattern.findall(idf_string), out_report_str, sqlite) + # process the RunPeriod within the idf_string try: run_period_str = runper_pattern.findall(idf_string)[0] @@ -198,7 +235,7 @@ def from_idf(cls, idf_string): except IndexError: # No SizingParameter in the file. sizing_par = None - return cls(run_period, timestep, sim_control, shadow_calc, sizing_par) + return cls(output, run_period, timestep, sim_control, shadow_calc, sizing_par) @classmethod def from_dict(cls, data): @@ -211,6 +248,7 @@ def from_dict(cls, data): { "type": "SimulationParameter", + "output": {}, // Honeybee SimulationOutput disctionary "run_period": {}, // Honeybee RunPeriod disctionary "timestep": 6, // Integer for the simulation timestep "simulation_control": {}, // Honeybee SimulationControl dictionary @@ -222,6 +260,9 @@ def from_dict(cls, data): 'Expected SimulationParameter dictionary. Got {}.'.format(data['type']) timestep = data['timestep'] if 'timestep' in data else 6 + output = None + if 'output' in data and data['output'] is not None: + output = SimulationOutput.from_dict(data['output']) run_period = None if 'run_period' in data and data['run_period'] is not None: run_period = RunPeriod.from_dict(data['run_period']) @@ -235,7 +276,7 @@ def from_dict(cls, data): if 'sizing_parameter' in data and data['sizing_parameter'] is not None: sizing_parameter = SizingParameter.from_dict(data['sizing_parameter']) - return cls(run_period, timestep, simulation_control, + return cls(output, run_period, timestep, simulation_control, shadow_calculation, sizing_parameter) def to_idf(self): @@ -246,6 +287,8 @@ def to_idf(self): etc.), """ header_str = '!- ============== SIMULATION PARAMETERS ==============\n' + table_style, output_vars, reports, sqlite, surfaces = self.output.to_idf() + output_vars_str = '/n/n'.join(output_vars) if output_vars is not None else '' run_period_str, holidays, daylight_saving = self.run_period.to_idf() holiday_str = '/n/n'.join(holidays) if holidays is not None else '' daylight_saving_time_str = daylight_saving if daylight_saving is not None else '' @@ -255,14 +298,16 @@ def to_idf(self): shadow_calc_str = self.shadow_calculation.to_idf() sizing_par_str = self.sizing_parameter.to_idf() - return '/n/n'.join([header_str, sim_control_str, shadow_calc_str, timestep_str, - run_period_str, holiday_str, daylight_saving_time_str, - sizing_par_str]) + return '/n/n'.join([table_style, output_vars_str, reports, sqlite, surfaces, + header_str, sim_control_str, shadow_calc_str, timestep_str, + run_period_str, holiday_str, daylight_saving_time_str, + sizing_par_str]) def to_dict(self): """SimulationParameter dictionary representation.""" return { 'type': 'SimulationParameter', + 'output': self.output.to_dict(), 'run_period': self.run_period.to_dict(), 'timestep': self.timestep, 'simulation_control': self.simulation_control.to_dict(), @@ -276,13 +321,14 @@ def duplicate(self): def __copy__(self): return SimulationParameter( - self.run_period.duplicate(), self.timestep, + self.output.duplicate(), self.run_period.duplicate(), self.timestep, self.simulation_control.duplicate(), self.shadow_calculation.duplicate(), self.sizing_parameter.duplicate()) def __key(self): """A tuple based on the object properties, useful for hashing.""" - return (hash(self.run_period), self.timestep, hash(self.simulation_control), + return (hash(self.output), hash(self.run_period), self.timestep, + hash(self.simulation_control), hash(self.shadow_calculation), hash(self.sizing_parameter)) def __hash__(self): diff --git a/tests/model_extend_test.py b/tests/model_extend_test.py index b5a131a33..f7b2fd48c 100644 --- a/tests/model_extend_test.py +++ b/tests/model_extend_test.py @@ -103,7 +103,7 @@ def test_check_duplicate_construction_set_names(): constr_set.roof_ceiling_set.exterior_construction = roof_constr attic.properties.energy.construction_set = constr_set - Room.solve_adjcency([first_floor, second_floor, attic], 0.01) + Room.solve_adjacency([first_floor, second_floor, attic], 0.01) model = Model('Multi Zone Single Family House', [first_floor, second_floor, attic]) @@ -466,7 +466,7 @@ def test_to_dict_multizone_house(): constr_set.roof_ceiling_set.exterior_construction = roof_constr attic.properties.energy.construction_set = constr_set - Room.solve_adjcency([first_floor, second_floor, attic], 0.01) + Room.solve_adjacency([first_floor, second_floor, attic], 0.01) model = Model('Multi Zone Single Family House', [first_floor, second_floor, attic]) model_dict = model.to_dict() diff --git a/tests/simulation_output_test.py b/tests/simulation_output_test.py new file mode 100644 index 000000000..494a8fac4 --- /dev/null +++ b/tests/simulation_output_test.py @@ -0,0 +1,201 @@ +# coding=utf-8 +from honeybee_energy.simulation.output import SimulationOutput + +import pytest + + +def test_simulation_output_init(): + """Test the initialization of SimulationOutput and basic properties.""" + sim_output = SimulationOutput() + str(sim_output) # test the string representation + + assert len(sim_output.outputs) == 0 + sim_output.add_zone_energy_use() + assert len(sim_output.outputs) > 1 + assert sim_output.reporting_frequency == 'Hourly' + assert sim_output.include_sqlite + assert not sim_output.include_html + assert len(sim_output.summary_reports) == 1 + assert sim_output.summary_reports == ('AllSummary',) + + +def test_simulation_output_setability(): + """Test the setting of properties of SimulationOutput.""" + sim_output = SimulationOutput() + + sim_output.outputs = ['Zone Ideal Loads Supply Air Total Cooling Energy'] + assert sim_output.outputs == ('Zone Ideal Loads Supply Air Total Cooling Energy',) + sim_output.reporting_frequency = 'Daily' + assert sim_output.reporting_frequency == 'Daily' + sim_output.include_sqlite = False + assert not sim_output.include_sqlite + sim_output.include_html = True + assert sim_output.include_html + sim_output.summary_reports = ['ComponentSizingSummary'] + assert sim_output.summary_reports == ('ComponentSizingSummary',) + + with pytest.raises(AssertionError): + sim_output.outputs = 'Zone Ideal Loads Supply Air Total Cooling Energy' + with pytest.raises(AssertionError): + sim_output.reporting_frequency = 'Biannually' + with pytest.raises(AssertionError): + sim_output.outputs = 'ComponentSizingSummary' + + +def test_simulation_output_add_summary_report(): + """Test the SimulationOutput add_summary_report methods.""" + sim_output = SimulationOutput() + + sim_output.add_summary_report('ComponentSizingSummary') + assert len(sim_output.summary_reports) == 2 + + +def test_simulation_output_add_output(): + """Test the SimulationOutput add_output methods.""" + sim_output = SimulationOutput() + + sim_output.outputs = ['Zone Ideal Loads Supply Air Total Cooling Energy'] + sim_output.add_output('Zone Ideal Loads Supply Air Total Cooling Energy') + assert sim_output.outputs == ('Zone Ideal Loads Supply Air Total Cooling Energy',) + sim_output.add_output('Zone Ideal Loads Supply Air Total Heating Energy') + assert len(sim_output.outputs) == 2 + + +def test_simulation_output_add_zone_energy_use(): + """Test the SimulationOutput add_zone_energy_use methods.""" + sim_output = SimulationOutput() + sim_output.add_zone_energy_use('all') + assert len(sim_output.outputs) == 6 + + sim_output = SimulationOutput() + sim_output.add_zone_energy_use('total') + assert len(sim_output.outputs) == 7 + + sim_output = SimulationOutput() + sim_output.add_zone_energy_use('sensible') + assert len(sim_output.outputs) == 9 + + sim_output = SimulationOutput() + sim_output.add_zone_energy_use('latent') + assert len(sim_output.outputs) == 4 + + with pytest.raises(ValueError): + sim_output.add_zone_energy_use('convective') + + +def test_simulation_output_add_hvac_energy_use(): + """Test the SimulationOutput add_hvac_energy_use methods.""" + sim_output = SimulationOutput() + sim_output.add_hvac_energy_use() + assert len(sim_output.outputs) >= 15 + + +def test_simulation_output_add_gains_and_losses(): + """Test the SimulationOutput add_gains_and_losses methods.""" + sim_output = SimulationOutput() + sim_output.add_gains_and_losses('total') + assert len(sim_output.outputs) == 8 + + sim_output = SimulationOutput() + sim_output.add_gains_and_losses('sensible') + assert len(sim_output.outputs) == 8 + + sim_output = SimulationOutput() + sim_output.add_gains_and_losses('latent') + assert len(sim_output.outputs) == 7 + + with pytest.raises(ValueError): + sim_output.add_gains_and_losses('convective') + + +def test_simulation_output_add_comfort_metrics(): + """Test the SimulationOutput add_comfort_metrics methods.""" + sim_output = SimulationOutput() + sim_output.add_comfort_metrics() + assert len(sim_output.outputs) == 4 + + +def test_simulation_output_add_stratification_variables(): + """Test the SimulationOutput add_stratification_variables methods.""" + sim_output = SimulationOutput() + sim_output.add_stratification_variables() + assert len(sim_output.outputs) == 6 + + +def test_simulation_output_add_surface_temperature(): + """Test the SimulationOutput add_surface_temperature methods.""" + sim_output = SimulationOutput() + sim_output.add_surface_temperature() + assert len(sim_output.outputs) == 2 + + +def test_simulation_output_add_surface_energy_flow(): + """Test the SimulationOutput add_surface_temperature methods.""" + sim_output = SimulationOutput() + sim_output.add_surface_energy_flow() + assert len(sim_output.outputs) == 3 + + +def test_simulation_output_add_glazing_solar(): + """Test the SimulationOutput add_glazing_solar methods.""" + sim_output = SimulationOutput() + sim_output.add_glazing_solar() + assert len(sim_output.outputs) == 3 + + +def test_simulation_output_add_energy_balance_variables(): + """Test the SimulationOutput add_energy_balance_variables methods.""" + sim_output = SimulationOutput() + sim_output.add_energy_balance_variables() + assert len(sim_output.outputs) == 18 + + +def test_simulation_output_add_comfort_map_variables(): + """Test the SimulationOutput add_comfort_map_variables methods.""" + sim_output = SimulationOutput() + sim_output.add_comfort_map_variables(True) + assert len(sim_output.outputs) == 10 + + sim_output = SimulationOutput() + sim_output.add_comfort_map_variables(False) + assert len(sim_output.outputs) == 4 + + +def test_simulation_output_equality(): + """Test the equality of SimulationOutput objects.""" + sim_output = SimulationOutput() + sim_output_dup = sim_output.duplicate() + sim_output_alt = SimulationOutput( + outputs=['Zone Ideal Loads Supply Air Total Cooling Energy']) + + assert sim_output is sim_output + assert sim_output is not sim_output_dup + assert sim_output == sim_output_dup + sim_output_dup.include_sqlite = False + assert sim_output != sim_output_dup + assert sim_output != sim_output_alt + + +def test_simulation_output_init_from_idf(): + """Test the initialization of SimulationOutput from_idf.""" + sim_output = SimulationOutput( + outputs=['Zone Ideal Loads Supply Air Total Cooling Energy']) + + table_style, output_variables, summary_reports, sqlite, surfaces_list = \ + sim_output.to_idf() + rebuilt_sim_output = SimulationOutput.from_idf(table_style, output_variables, + summary_reports, True) + assert sim_output == rebuilt_sim_output + assert rebuilt_sim_output.to_idf() == (table_style, output_variables, + summary_reports, sqlite, surfaces_list) + + +def test_simulation_output_dict_methods(): + """Test the to/from dict methods.""" + sim_output = SimulationOutput( + outputs=['Zone Ideal Loads Supply Air Total Cooling Energy']) + + output_dict = sim_output.to_dict() + new_sim_output = SimulationOutput.from_dict(output_dict) + assert new_sim_output == sim_output + assert output_dict == new_sim_output.to_dict() diff --git a/tests/simulationparameter_test.py b/tests/simulationparameter_test.py index 66657cb79..ce0d9c375 100644 --- a/tests/simulationparameter_test.py +++ b/tests/simulationparameter_test.py @@ -1,5 +1,6 @@ # coding=utf-8 from honeybee_energy.simulationparameter import SimulationParameter +from honeybee_energy.simulation.output import SimulationOutput from honeybee_energy.simulation.runperiod import RunPeriod from honeybee_energy.simulation.control import SimulationControl from honeybee_energy.simulation.shadowcalculation import ShadowCalculation @@ -15,6 +16,7 @@ def test_simulation_parameter_init(): sim_par = SimulationParameter() str(sim_par) # test the string representation + assert sim_par.output == SimulationOutput() assert sim_par.run_period == RunPeriod() assert sim_par.timestep == 6 assert sim_par.simulation_control == SimulationControl() @@ -26,6 +28,10 @@ def test_simulation_parameter_setability(): """Test the setting of properties of SimulationParameter.""" sim_par = SimulationParameter() + output = SimulationOutput() + output.add_zone_energy_use() + sim_par.output = output + assert sim_par.output == output run_period = RunPeriod(Date(1, 1), Date(6, 21)) sim_par.run_period = run_period assert sim_par.run_period == run_period @@ -60,6 +66,9 @@ def test_simulation_parameter_equality(): def test_simulation_parameter_init_from_idf(): """Test the initialization of SimulationParameter from_idf.""" sim_par = SimulationParameter() + output = SimulationOutput() + output.add_zone_energy_use() + sim_par.output = output run_period = RunPeriod(Date(1, 1), Date(6, 21)) sim_par.run_period = run_period sim_par.timestep = 4 @@ -80,6 +89,9 @@ def test_simulation_parameter_init_from_idf(): def test_simulation_parameter_dict_methods(): """Test the to/from dict methods.""" sim_par = SimulationParameter() + output = SimulationOutput() + output.add_zone_energy_use() + sim_par.output = output run_period = RunPeriod(Date(1, 1), Date(6, 21)) sim_par.run_period = run_period sim_par.timestep = 4