Source code for dragonfly_energy.cli.simulate

"""dragonfly energy simulation running commands."""
import click
import sys
import os
import logging
import json
import subprocess
from concurrent.futures import ProcessPoolExecutor, as_completed

from ladybug.epw import EPW
from ladybug.stat import STAT
from ladybug.futil import preparedir
from ladybug.commandutil import process_content_to_output
from honeybee.config import folders
from honeybee_energy.simulation.parameter import SimulationParameter
from dragonfly.model import Model
from dragonfly_energy.run import run_urbanopt, _recommended_processor_count


_logger = logging.getLogger(__name__)


@click.group(help='Commands for simulating Dragonfly JSON files in EnergyPlus.')
def simulate():
    pass


@simulate.command('model')
@click.argument(
    'model-file',
    type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)
)
@click.argument(
    'epw-file',
    type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)
)
@click.option(
    '--sim-par-json', '-sp', help='Full path to a honeybee energy '
    'SimulationParameter JSON that describes all of the settings for '
    'the simulation.', default=None, show_default=True,
    type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)
)
@click.option(
    '--obj-per-model', '-o', help='Text to describe how the input Model '
    'should be divided across the output Models. Choose from: District, '
    'Building, Story.', type=str, default='Building', show_default=True
)
@click.option(
    '--shade-dist', '-sd', help='An optional number to note the distance '
    'beyond which other buildings shade should not be exported into a given '
    'Model. If None, all other buildings will be included as context shade in '
    'each and every Model. Set to 0 to exclude all neighboring buildings '
    'from the resulting models.', type=float, default=None, show_default=True
)
@click.option(
    '--multiplier/--full-geometry', ' /-fg', help='Flag to note if the '
    'multipliers on each Building story will be passed along to the '
    'generated Honeybee Room objects or if full geometry objects should be '
    'written for each story in the building.', default=True, show_default=True
)
@click.option(
    '--plenum/--no-plenum', '-p/-np', help='Flag to indicate whether '
    'ceiling/floor plenum depths assigned to Room2Ds should generate '
    'distinct 3D Rooms in the translation.', default=True, show_default=True
)
@click.option(
    '--no-ceil-adjacency/--ceil-adjacency', ' /-a', help='Flag to indicate '
    'whether adjacencies should be solved between interior stories when '
    'Room2Ds perfectly match one another in their floor plate. This ensures '
    'that Surface boundary conditions are used instead of Adiabatic ones. '
    'Note that this input has no effect when the object-per-model is Story.',
    default=True, show_default=True
)
@click.option(
    '--merge-method', '-m', help='Text to describe how the Room2Ds should '
    'be merged into individual Rooms during the translation. Specifying a '
    'value here can be an effective way to reduce the number of Room '
    'volumes in the resulting Model and, ultimately, yield a faster simulation '
    'time with less results to manage. Choose from: None, Zones, PlenumZones, '
    'Stories, PlenumStories.', type=str, default='None', show_default=True
)
@click.option(
    '--measures', '-m', help='Full path to a folder containing an OSW JSON '
    'be used as the base for the execution of the OpenStudio CLI. While this '
    'OSW can contain paths to measures that exist anywhere on the machine, '
    'the best practice is to copy the measures into this measures '
    'folder and use relative paths within the OSW. '
    'This makes it easier to move the inputs for this command from one '
    'machine to another.', default=None, show_default=True,
    type=click.Path(file_okay=False, dir_okay=True, resolve_path=True)
)
@click.option(
    '--cpu-count', '-c', help='Optional integer to specify the number of'
    'processors to be used in simulating each model derived from the input model.'
    'If unspecified, this will be one less than the total number of processors '
    'available on the machine.', type=int, default=None, show_default=True)
@click.option(
    '--folder', '-f', help='Folder on this computer, into which the IDF '
    'and result files will be written. If unspecified, the files will be output '
    'to the honeybee default simulation folder and placed in a project '
    'folder with the same name as the input model.',
    default=None, show_default=True,
    type=click.Path(file_okay=False, dir_okay=True, resolve_path=True)
)
def simulate_model_cli(
    model_file, epw_file, sim_par_json, obj_per_model, shade_dist,
    multiplier, plenum, no_ceil_adjacency, merge_method,
    measures, cpu_count, folder
):
    """Simulate a Dragonfly Model JSON file in EnergyPlus.

    \b
    Args:
        model_file: Full path to a Dragonfly Model JSON file. This can also be a
            GeoJSON following the Dragonfly GeoJSON schema.
        epw_file: Full path to an .epw file.
    """
    try:
        full_geometry = not multiplier
        no_plenum = not plenum
        ceil_adjacency = not no_ceil_adjacency
        simulate_model(
            model_file, epw_file, sim_par_json, obj_per_model, shade_dist,
            full_geometry, no_plenum, ceil_adjacency, merge_method,
            measures, cpu_count, folder
        )
    except Exception as e:
        _logger.exception('Model simulation failed.\n{}'.format(e))
        sys.exit(1)
    else:
        sys.exit(0)


[docs] def simulate_model( model_file, epw_file, sim_par_json=None, obj_per_model='Building', shade_dist=None, full_geometry=False, no_plenum=False, ceil_adjacency=False, merge_method='None', measures=None, cpu_count=None, folder=None, multiplier=True, plenum=True, no_ceil_adjacency=True ): """Simulate a Dragonfly Model JSON file in EnergyPlus. Args: model_file: Full path to a Dragonfly Model JSON file. This can also be a GeoJSON following the Dragonfly GeoJSON schema. epw_file: Full path to an .epw file. sim_par_json: Full path to a honeybee energy SimulationParameter JSON that describes all of the settings for the simulation. If None, default parameters will be generated. obj_per_model: Text to describe how the input Buildings should be divided across the output Models. (Default: 'Building'). Choose from the following options: * District - All buildings will be added to a single Honeybee Model. Such a Model can take a long time to simulate so this is only recommended for small numbers of buildings or cases where exchange of data between Buildings is necessary. * Building - Each building will be exported into its own Model. For each Model, the other buildings input to this component will appear as context shade geometry. * Story - Each Story of each Building will be exported into its own Model. For each Honeybee Model, the other input Buildings will appear as context shade geometry as will all of the other stories of the same building. shade_dist: An optional number to note the distance beyond which other buildings shade should not be exported into a Model. This can include the units of the distance (eg. 100ft) or, if no units are provided, the value will be interpreted in the dragonfly model units. If None, all other buildings will be included as context shade in each and every Model. Set to 0 to exclude all neighboring buildings from the resulting models. (Default: None). full_geometry: Boolean to note if the multipliers on each Building story will be passed along to the generated Honeybee Room objects or if full geometry objects should be written for each story in the building. (Default: False). no_plenum: Boolean to indicate whether ceiling/floor plenum depths assigned to Room2Ds should generate distinct 3D Rooms in the translation. (Default: False). ceil_adjacency: Boolean to indicate whether adjacencies should be solved between interior stories when Room2Ds perfectly match one another in their floor plate. This ensures that Surface boundary conditions are used instead of Adiabatic ones. Note that this input has no effect when the object-per-model is Story. (Default: False). merge_method: An optional text string to describe how the Room2Ds should be merged into individual Rooms during the translation. Specifying a value here can be an effective way to reduce the number of Room volumes in the resulting Model and, ultimately, yield a faster simulation time with less results to manage. Note that Room2Ds will only be merged if they form a contiguous volume. Otherwise, there will be multiple Rooms per zone or story, each with an integer added at the end of their identifiers. Choose from the following options: * None - No merging will occur * Zones - Room2Ds in the same zone will be merged * PlenumZones - Only plenums in the same zone will be merged * Stories - Rooms in the same story will be merged * PlenumStories - Only plenums in the same story will be merged measures: Full path to a folder containing an OSW JSON be used as the base for the execution of the OpenStudio CLI. While this OSW can contain paths to measures that exist anywhere on the machine, the best practice is to copy the measures into this measures folder and use relative paths within the OSW. This makes it easier to move the inputs for this command from one machine to another. cpu_count: Optional integer to specify the number of processors to be used in simulating each model derived from the input model. folder: Folder on this computer, into which the IDF and result files will be written. If unspecified, the files will be output to the honeybee default simulation folder and placed in a project folder with the same name as the input model. """ # get a ddy variable that might get used later epw_folder, epw_file_name = os.path.split(epw_file) ddy_file = os.path.join(epw_folder, epw_file_name.replace('.epw', '.ddy')) stat_file = os.path.join(epw_folder, epw_file_name.replace('.epw', '.stat')) # set the default folder to the default if it's not specified if folder is None: proj_name = os.path.basename(model_file).replace('.json', '') proj_name = proj_name.replace('.dfjson', '') proj_name = proj_name.replace('.geojson', '') folder = os.path.join( folders.default_simulation_folder, proj_name, 'OpenStudio') preparedir(folder, remove_content=False) # process the simulation parameters and write new ones if necessary def ddy_from_epw(epw_file, sim_par): """Produce a DDY from an EPW file.""" epw_obj = EPW(epw_file) des_days = [epw_obj.approximate_design_day('WinterDesignDay'), epw_obj.approximate_design_day('SummerDesignDay')] sim_par.sizing_parameter.design_days = des_days if sim_par_json is None: # generate some default simulation parameters sim_par = SimulationParameter() sim_par.output.add_zone_energy_use() sim_par.output.add_hvac_energy_use() sim_par.output.add_electricity_generation() sim_par.output.reporting_frequency = 'Monthly' else: with open(sim_par_json) as json_file: data = json.load(json_file) sim_par = SimulationParameter.from_dict(data) if len(sim_par.sizing_parameter.design_days) == 0 and os.path.isfile(ddy_file): try: sim_par.sizing_parameter.add_from_ddy_996_004(ddy_file) except AssertionError: # no design days within the DDY file ddy_from_epw(epw_file, sim_par) elif len(sim_par.sizing_parameter.design_days) == 0: ddy_from_epw(epw_file, sim_par) if sim_par.sizing_parameter.climate_zone is None and \ os.path.isfile(stat_file): stat_obj = STAT(stat_file) sim_par.sizing_parameter.climate_zone = stat_obj.ashrae_climate_zone # re-serialize the Dragonfly Model from a DFJSON or GeoJSON with open(model_file) as json_file: data = json.load(json_file) if 'type' in data and data['type'] == 'Model': model = Model.from_dict(data) model.convert_to_units('Meters') else: # assume that it is a GeoJSON model, _ = Model.from_geojson(model_file) model.separate_top_bottom_floors() # convert Dragonfly Model to Honeybee no_plenum = not plenum ceil_adjacency = not no_ceil_adjacency hb_models = model.to_honeybee( obj_per_model, shade_dist, use_multiplier=multiplier, exclude_plenums=no_plenum, solve_ceiling_adjacencies=ceil_adjacency, merge_method=merge_method ) # write Honeybee models to JSONs in their own sub-folders hbjson_files = [] for hb_model in hb_models: directory = os.path.join(folder, hb_model.identifier) if not os.path.isdir(directory): os.makedirs(directory) hbjson_files.append(hb_model.to_hbjson(folder=directory)) # execute simulations in parallel cpu_count = cpu_count if cpu_count is not None else _recommended_processor_count() print('Simulating {} models with {} processors.'.format(len(hbjson_files), cpu_count)) with ProcessPoolExecutor(max_workers=cpu_count) as executor: # submit all tasks to the executor futures = { executor.submit(_simulate_hbjson, path, epw_file, sim_par_json, measures): path for path in hbjson_files } # yield results as soon as each process completes for future in as_completed(futures): success, original_path, msg = future.result() filename = os.path.basename(original_path) if success: suc_str = 'SUCCESS: Simulated {}' print(suc_str.format(filename)) else: print('FAILED: Could not simulate {}'.format(filename)) print(' Error details: {}'.format(msg.strip()))
def _simulate_hbjson(hbjson_path, epw_file, sim_par_json, measures): """Translate a HBJSON file in EnergyPlus.""" # honeybee-energy CLI command for translation sim_folder = os.path.dirname(hbjson_path) cmd = [ folders.python_exe_path, '-m', 'honeybee_energy', 'simulate', 'model', hbjson_path, epw_file, '--folder', sim_folder ] if sim_par_json is not None: cmd.append('--sim-par-json') cmd.append(sim_par_json) if measures is not None: cmd.append('--measures') cmd.append(measures) try: # execute the CLI command process = subprocess.run( cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) return True, hbjson_path, process.stdout except subprocess.CalledProcessError as e: return False, hbjson_path, e.stderr @simulate.command('urbanopt') @click.argument( 'feature-file', type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True) ) @click.argument( 'scenario-file', type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True) ) @click.option( '--log-file', '-log', help='Optional log file to output the paths to the ' 'generated simulation files if they were successfully created. ' 'By default this will be printed out to stdout', type=click.File('w'), default='-', show_default=True) def simulate_urbanopt_cli(feature_file, scenario_file, log_file): """Simulate am URBANopt project folder that is already prepared for simulation. \b Args: feature_geojson: The full path to a .geojson file containing the footprints of buildings to be simulated. scenario_csv: The full path to a .csv file for the URBANopt scenario. """ try: simulate_urbanopt(feature_file, scenario_file, log_file) except Exception as e: _logger.exception('URBANopt simulation failed.\n{}'.format(e)) sys.exit(1) else: sys.exit(0)
[docs] def simulate_urbanopt(feature_file, scenario_file, log_file=None): """Simulate am URBANopt project folder that is already prepared for simulation. Args: feature_geojson: The full path to a .geojson file containing the footprints of buildings to be simulated. scenario_csv: The full path to a .csv file for the URBANopt scenario. log_file: Optional log file to output the paths to the generated simulation files if they were successfully created. By default this string will be returned from this method. """ # run the URBANopt CLI to complete the simulation osm, idf, sql, zsz, rdd, html, err = run_urbanopt(feature_file, scenario_file) # process all of the output files into the log file gen_files = { 'osm': osm, 'idf': idf, 'sql': sql, 'zsz': zsz, 'rdd': rdd, 'html': html, 'err': err } return process_content_to_output(json.dumps(gen_files, indent=4), log_file)