# coding=utf-8
"""Methods to write files for URBANopt simulation from a Model."""
import sys
import os
import re
import json
import shutil
from ladybug_geometry.geometry2d import Point2D
from ladybug.futil import nukedir, preparedir
from ladybug.epw import EPW
from honeybee.config import folders
from honeybee.model import Model as hb_model
[docs]
def model_to_urbanopt(
model, location, point=Point2D(0, 0), shade_distance=None, use_multiplier=True,
add_plenum=False, solve_ceiling_adjacencies=False,
des_loop=None, electrical_network=None, road_network=None, ground_pv=None,
folder=None, tolerance=None
):
r"""Generate an URBANopt feature geoJSON and honeybee JSONs from a dragonfly Model.
Args:
model: A dragonfly Model for which an URBANopt feature geoJSON and
corresponding honeybee Model JSONs will be returned.
location: A ladybug Location object possessing longitude and latitude data.
point: A ladybug_geometry Point2D for where the location object exists
within the space of a scene. The coordinates of this point are
expected to be in the units of this Model. (Default: (0, 0)).
shade_distance: An optional number to note the distance beyond which other
objects' shade should not be exported into a given honeybee Model. This
is helpful for reducing the simulation run time of each Model when other
connected buildings are too far away to have a meaningful impact on
the results. 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).
use_multiplier: If True, the multipliers on the Model's Stories will be
passed along to the generated Honeybee Room objects, indicating the
simulation will be run once for each unique room and then results
will be multiplied. If False, full geometry objects will be written
for each and every floor in the building that are represented through
multipliers and all resulting multipliers will be 1. (Default: True).
add_plenum: Boolean to indicate whether ceiling/floor plenums should
be auto-generated for the Rooms. (Default: False).
solve_ceiling_adjacencies: Boolean to note 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).
des_loop: An optional District Energy System (DES) ThermalLoop that's
associated with the dragonfly Model. (Default: None).
electrical_network: An optional OpenDSS ElectricalNetwork that's associated
with the dragonfly Model. (Default: None).
road_network: An optional RNM RoadNetwork that's associated with the
dragonfly Model. (Default: None).
ground_pv: An optional list of REopt GroundMountPV objects representing
ground-mounted photovoltaic fields to be included in the REopt
simulation. (Default: None).
folder: An optional folder to be used as the root of the model's
URBANopt folder. If None, the files will be written into a sub-directory
of the honeybee-core default_simulation_folder.
tolerance: The minimum distance between points at which they are
not considered touching. If None, the Model tolerance will be used.
Returns:
A tuple with three values.
feature_geojson -- The path to an URBANopt feature geoJSON that has
been written by this method.
hb_model_jsons -- An array of file paths to honeybee Model JSONs that
correspond to the detailed_model_filename keys in the feature_geojson.
hb_models -- An array of honeybee Model objects that were generated in
process of writing the URBANopt files.
"""
# make sure the model is in meters and, if it's not, duplicate and scale it
conversion_factor, original_units = None, 'Meters'
tolerance = model.tolerance if tolerance is None else tolerance
if model.units != 'Meters':
original_units = model.units
conversion_factor = hb_model.conversion_factor_to_meters(model.units)
point = point.scale(conversion_factor)
if shade_distance is not None:
shade_distance = shade_distance * conversion_factor
tolerance = tolerance * conversion_factor
model = model.duplicate() # duplicate the model to avoid mutating the input
model.convert_to_units('Meters')
if des_loop is not None:
des_loop.scale(conversion_factor)
if electrical_network is not None:
electrical_network.scale(conversion_factor)
if road_network is not None:
road_network.scale(conversion_factor)
if ground_pv is not None:
for g_pv in ground_pv:
g_pv.scale(conversion_factor)
# prepare the folder for simulation
tr_msg = 'The following simulation folder is too long to be used with URBANopt:' \
'\n{}\nSpecify a shorter folder path in which to write the GeoJSON.'
if folder is None: # use the default simulation folder
assert len(folders.default_simulation_folder) < 55, \
tr_msg.format(folders.default_simulation_folder)
sim_dir = re.sub(r'[^.A-Za-z0-9_-]', '_', model.display_name)
folder = os.path.join(folders.default_simulation_folder, sim_dir)
if len(folder) >= 60:
tr_len = 58 - len(folders.default_simulation_folder)
folder = os.path.join(folders.default_simulation_folder, sim_dir[:tr_len])
else:
assert len(folder) < 60, tr_msg.format(folder)
# get rid of all simulation files that exists in the folder already
dir_to_delete = ('hb_json', 'mappers', 'run')
ext_to_delete = ('.bat', '.geojson', '.epw', '.mos')
file_to_delete = (
'Gemfile', 'Gemfile.lock', 'honeybee_scenario.csv', 'runner.conf',
'simulation_parameter.json', 'system_params.json',
'electrical_database.json', 'network.json'
)
if os.path.isdir(folder):
files = os.listdir(folder)
for f in files:
path = os.path.join(folder, f)
if os.path.isdir(path):
if f in dir_to_delete:
nukedir(path, True)
else:
if f in file_to_delete:
os.remove(path)
elif f.endswith(ext_to_delete):
os.remove(path)
else:
preparedir(folder) # create the directory if it's not there
# prepare the folder into which honeybee Model JSONs will be written
hb_model_folder = os.path.join(folder, 'hb_json') # folder for honeybee JSONs
preparedir(hb_model_folder)
# create GeoJSON dictionary
geojson_dict = model.to_geojson_dict(location, point, tolerance=tolerance)
for feature_dict in geojson_dict['features']: # add the detailed model filename
if feature_dict['properties']['type'] == 'Building':
bldg_id = feature_dict['properties']['id']
feature_dict['properties']['detailed_model_filename'] = \
os.path.join(hb_model_folder, '{}.json'.format(bldg_id))
# add the DES to the GeoJSON dictionary
if des_loop is not None:
if hasattr(des_loop, 'to_geojson_dict'):
des_features = des_loop.to_geojson_dict(
model.buildings, location, point, tolerance=tolerance)
geojson_dict['features'].extend(des_features)
sys_p_json = os.path.join(folder, 'system_params.json')
with open(sys_p_json, 'w') as fp:
des_dict = des_loop.to_des_param_dict(model.buildings, tolerance=tolerance)
json.dump(des_dict, fp, indent=2)
if conversion_factor is not None:
des_loop.scale(1 / conversion_factor)
# add the electrical network to the GeoJSON dictionary
if electrical_network is not None:
electric_features = electrical_network.to_geojson_dict(
model.buildings, location, point, tolerance=tolerance)
geojson_dict['features'].extend(electric_features)
electric_json = os.path.join(folder, 'electrical_database.json')
with open(electric_json, 'w') as fp:
json.dump(electrical_network.to_electrical_database_dict(), fp, indent=4)
if conversion_factor is not None:
electrical_network.scale(1 / conversion_factor)
# add the road network to the GeoJSON dictionary
if road_network is not None:
road_features = road_network.to_geojson_dict(location, point)
geojson_dict['features'].extend(road_features)
if conversion_factor is not None:
road_network.scale(1 / conversion_factor)
# add the ground-mounted PV to the GeoJSON dictionary
if ground_pv is not None and len(ground_pv) != 0:
pv_features = [g_pv.to_geojson_dict(location, point) for g_pv in ground_pv]
geojson_dict['features'].extend(pv_features)
if conversion_factor is not None:
for g_pv in ground_pv:
g_pv.scale(1 / conversion_factor)
# write out the GeoJSON file
feature_geojson = os.path.join(folder, '{}.geojson'.format(model.identifier))
if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
with open(feature_geojson, 'wb') as fp:
obj_str = json.dumps(geojson_dict, indent=4, ensure_ascii=False)
fp.write(obj_str.encode('utf-8'))
else:
with open(feature_geojson, 'w', encoding='utf-8') as fp:
obj_str = json.dump(geojson_dict, fp, indent=4, ensure_ascii=False)
# write out the honeybee Model JSONs from the model
hb_model_jsons = []
hb_models = model.to_honeybee(
'Building', shade_distance, use_multiplier, add_plenum,
solve_ceiling_adjacencies=solve_ceiling_adjacencies, tolerance=tolerance)
for bldg_model in hb_models:
try:
bldg_model.remove_degenerate_geometry(0.01)
except ValueError:
error = 'Failed to remove degenerate Geometry.\nYour Model units system is: {}. ' \
'Is this correct?'.format(original_units)
raise ValueError(error)
model_dict = bldg_model.to_dict(triangulate_sub_faces=True)
bldg_model.properties.energy.add_autocal_properties_to_dict(model_dict)
bld_path = os.path.join(hb_model_folder, '{}.json'.format(bldg_model.identifier))
if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
with open(bld_path, 'wb') as fp:
obj_str = json.dumps(model_dict, indent=4, ensure_ascii=False)
fp.write(obj_str.encode('utf-8'))
else:
with open(bld_path, 'w', encoding='utf-8') as fp:
obj_str = json.dump(model_dict, fp, indent=4, ensure_ascii=False)
hb_model_jsons.append(bld_path)
return feature_geojson, hb_model_jsons, hb_models
[docs]
def model_to_des(
model, des_loop, epw_file, location=None, point=Point2D(0, 0),
folder=None, tolerance=None
):
r"""Generate an URBANopt feature geoJSON and DES input files from a dragonfly Model.
This method is intended specifically for the case that District Energy
System (DES) simulation is to be performed without using URBANopt to generate
building energy loads through EnergyPlus. Accordingly, ALL Dragonfly Buildings
in the Model must have DES loads assigned directly to them in order for this
method to run correctly.
Args:
model: A dragonfly Model for which an URBANopt feature geoJSON and
corresponding DES input files will be generated.
des_loop: A District Energy System (DES) ThermalLoop that is associated
with the dragonfly Model.
epw_file: The file path to an EPW that should be associated with the
output energy model.
location: An optional ladybug Location object possessing longitude and
latitude data. If None, the Location data will be pulled from the
input epw_file, effectively placing the GeoJSON at the location
of the EPW
point: A ladybug_geometry Point2D for where the location object exists
within the space of a scene. The coordinates of this point are
expected to be in the units of this Model. (Default: (0, 0)).
folder: An optional folder to be used as the root of the model's
URBANopt folder. If None, the files will be written into a sub-directory
of the honeybee-core default_simulation_folder.
tolerance: The minimum distance between points at which they are
not considered touching. If None, the Model tolerance will be used.
Returns:
A tuple with three values.
feature_geojson -- The path to an URBANopt feature geoJSON that has
been written by this method.
scenario_csv -- The path to an URBANopt scenario CSV that has
been written by this method.
system_parameters -- The path to the DES system parameter JSON that has
been written by this method.
"""
# make sure the model is in meters and, if it's not, duplicate and scale it
conversion_factor = None
tolerance = model.tolerance if tolerance is None else tolerance
if model.units != 'Meters':
conversion_factor = hb_model.conversion_factor_to_meters(model.units)
point = point.scale(conversion_factor)
tolerance = tolerance * conversion_factor
model = model.duplicate() # duplicate the model to avoid mutating the input
model.convert_to_units('Meters')
des_loop.scale(conversion_factor)
# prepare the folder for simulation
if folder is None: # use the default simulation folder
folder = os.path.join(
folders.default_simulation_folder,
re.sub(r'[^.A-Za-z0-9_-]', '_', model.display_name)
)
nukedir(folder, True) # get rid of anything that exists in the folder already
preparedir(folder) # create the directory if it's not there
# create GeoJSON dictionary
epw_obj = EPW(epw_file)
if location is None:
location = epw_obj.location
geojson_dict = model.to_geojson_dict(location, point, tolerance=tolerance)
# create the scenario CSV file
scenario_matrix = [['Feature Id', 'Feature Name', 'Mapper Class']]
hb_mapper = 'URBANopt::Scenario::HoneybeeMapper'
for feature in geojson_dict['features']:
try:
if feature['properties']['type'] == 'Building':
props = feature['properties']
f_row = [props['id'], props['name'], hb_mapper]
scenario_matrix.append(f_row)
except KeyError: # definitely not a building
pass
scenario_csv = os.path.join(folder, 'honeybee_scenario.csv')
with open(scenario_csv, 'w') as fp:
for row in scenario_matrix:
fp.write('{}\n'.format(','.join(row)))
# write the Building loads into the scenario result folder
scn_dir = os.path.join(folder, 'run', 'honeybee_scenario')
for bldg in model.buildings:
csv_data = bldg.properties.energy.to_building_load_csv()
mos_data = bldg.properties.energy.to_building_load_mos()
bldg_dir = os.path.join(scn_dir, bldg.identifier, '004_export_modelica_loads')
preparedir(bldg_dir)
csv_path = os.path.join(bldg_dir, 'building_loads.csv')
mos_path = os.path.join(bldg_dir, 'modelica.mos')
with open(csv_path, 'w') as fp:
fp.write(csv_data)
with open(mos_path, 'w') as fp:
fp.write(mos_data)
# add the DES to the GeoJSON dictionary
if hasattr(des_loop, 'to_geojson_dict'):
des_features = des_loop.to_geojson_dict(
model.buildings, location, point, tolerance=tolerance)
geojson_dict['features'].extend(des_features)
des_dict = des_loop.to_des_param_dict(model.buildings, tolerance=tolerance)
if conversion_factor is not None: # put back the correct scale for the DES
des_loop.scale(1 / conversion_factor)
# copy the EPW to the project directory
epw_f_name = os.path.split(epw_file)[-1]
target_epw = os.path.join(folder, epw_f_name)
shutil.copy(epw_file, target_epw)
# create a MOS file from the EPW
epw_obj = EPW(target_epw)
mos_file = os.path.join(folder, epw_f_name.replace('.epw', '.mos'))
epw_obj.to_mos(mos_file)
# write the EPW path into the GeoJSON
if 'project' in geojson_dict:
if 'weather_filename' not in geojson_dict['project']:
geojson_dict['project']['weather_filename'] = epw_f_name
# if the DES system is GSHP, specify any autocalculated ground temperatures
if 'district_system' in des_dict:
if 'fifth_generation' in des_dict['district_system']:
g5_par = des_dict['district_system']['fifth_generation']
if 'soil' in g5_par and 'undisturbed_temp' in g5_par['soil']:
soil_par = g5_par['soil']
if soil_par['undisturbed_temp'] == 'Autocalculate':
epw_obj = EPW(epw_file)
soil_par['undisturbed_temp'] = \
epw_obj.dry_bulb_temperature.average
# write out the GeoJSON and system parameter files
feature_geojson = os.path.join(folder, '{}.geojson'.format(model.identifier))
system_parameters = os.path.join(folder, 'system_params.json')
if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
with open(feature_geojson, 'wb') as fp:
obj_str = json.dumps(geojson_dict, indent=4, ensure_ascii=False)
fp.write(obj_str.encode('utf-8'))
with open(system_parameters, 'wb') as fp:
obj_str = json.dumps(des_dict, indent=2, ensure_ascii=False)
fp.write(obj_str.encode('utf-8'))
else:
with open(feature_geojson, 'w', encoding='utf-8') as fp:
obj_str = json.dump(geojson_dict, fp, indent=4, ensure_ascii=False)
with open(system_parameters, 'w') as fp:
json.dump(des_dict, fp, indent=2)
return feature_geojson, scenario_csv, system_parameters