honeybee-energy 1.116.64__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of honeybee-energy might be problematic. Click here for more details.

Files changed (162) hide show
  1. honeybee_energy/__init__.py +24 -0
  2. honeybee_energy/__main__.py +4 -0
  3. honeybee_energy/_extend_honeybee.py +145 -0
  4. honeybee_energy/altnumber.py +21 -0
  5. honeybee_energy/baseline/__init__.py +2 -0
  6. honeybee_energy/baseline/create.py +608 -0
  7. honeybee_energy/baseline/data/__init__.py +1 -0
  8. honeybee_energy/baseline/data/constructions.csv +64 -0
  9. honeybee_energy/baseline/data/fen_ratios.csv +15 -0
  10. honeybee_energy/baseline/data/lpd_building.csv +21 -0
  11. honeybee_energy/baseline/data/pci_2016.csv +22 -0
  12. honeybee_energy/baseline/data/pci_2019.csv +22 -0
  13. honeybee_energy/baseline/data/pci_2022.csv +22 -0
  14. honeybee_energy/baseline/data/shw.csv +21 -0
  15. honeybee_energy/baseline/pci.py +512 -0
  16. honeybee_energy/baseline/result.py +371 -0
  17. honeybee_energy/boundarycondition.py +128 -0
  18. honeybee_energy/cli/__init__.py +69 -0
  19. honeybee_energy/cli/baseline.py +475 -0
  20. honeybee_energy/cli/edit.py +327 -0
  21. honeybee_energy/cli/lib.py +1154 -0
  22. honeybee_energy/cli/result.py +810 -0
  23. honeybee_energy/cli/setconfig.py +124 -0
  24. honeybee_energy/cli/settings.py +569 -0
  25. honeybee_energy/cli/simulate.py +380 -0
  26. honeybee_energy/cli/translate.py +1601 -0
  27. honeybee_energy/cli/validate.py +224 -0
  28. honeybee_energy/config.json +11 -0
  29. honeybee_energy/config.py +842 -0
  30. honeybee_energy/construction/__init__.py +1 -0
  31. honeybee_energy/construction/_base.py +374 -0
  32. honeybee_energy/construction/air.py +325 -0
  33. honeybee_energy/construction/dictutil.py +89 -0
  34. honeybee_energy/construction/dynamic.py +607 -0
  35. honeybee_energy/construction/opaque.py +460 -0
  36. honeybee_energy/construction/shade.py +319 -0
  37. honeybee_energy/construction/window.py +1096 -0
  38. honeybee_energy/construction/windowshade.py +847 -0
  39. honeybee_energy/constructionset.py +1655 -0
  40. honeybee_energy/dictutil.py +56 -0
  41. honeybee_energy/generator/__init__.py +5 -0
  42. honeybee_energy/generator/loadcenter.py +204 -0
  43. honeybee_energy/generator/pv.py +535 -0
  44. honeybee_energy/hvac/__init__.py +21 -0
  45. honeybee_energy/hvac/_base.py +124 -0
  46. honeybee_energy/hvac/_template.py +270 -0
  47. honeybee_energy/hvac/allair/__init__.py +22 -0
  48. honeybee_energy/hvac/allair/_base.py +349 -0
  49. honeybee_energy/hvac/allair/furnace.py +168 -0
  50. honeybee_energy/hvac/allair/psz.py +131 -0
  51. honeybee_energy/hvac/allair/ptac.py +163 -0
  52. honeybee_energy/hvac/allair/pvav.py +109 -0
  53. honeybee_energy/hvac/allair/vav.py +128 -0
  54. honeybee_energy/hvac/detailed.py +337 -0
  55. honeybee_energy/hvac/doas/__init__.py +28 -0
  56. honeybee_energy/hvac/doas/_base.py +345 -0
  57. honeybee_energy/hvac/doas/fcu.py +127 -0
  58. honeybee_energy/hvac/doas/radiant.py +329 -0
  59. honeybee_energy/hvac/doas/vrf.py +81 -0
  60. honeybee_energy/hvac/doas/wshp.py +91 -0
  61. honeybee_energy/hvac/heatcool/__init__.py +23 -0
  62. honeybee_energy/hvac/heatcool/_base.py +177 -0
  63. honeybee_energy/hvac/heatcool/baseboard.py +61 -0
  64. honeybee_energy/hvac/heatcool/evapcool.py +72 -0
  65. honeybee_energy/hvac/heatcool/fcu.py +92 -0
  66. honeybee_energy/hvac/heatcool/gasunit.py +53 -0
  67. honeybee_energy/hvac/heatcool/radiant.py +269 -0
  68. honeybee_energy/hvac/heatcool/residential.py +77 -0
  69. honeybee_energy/hvac/heatcool/vrf.py +54 -0
  70. honeybee_energy/hvac/heatcool/windowac.py +70 -0
  71. honeybee_energy/hvac/heatcool/wshp.py +62 -0
  72. honeybee_energy/hvac/idealair.py +699 -0
  73. honeybee_energy/internalmass.py +310 -0
  74. honeybee_energy/lib/__init__.py +1 -0
  75. honeybee_energy/lib/_loadconstructions.py +194 -0
  76. honeybee_energy/lib/_loadconstructionsets.py +117 -0
  77. honeybee_energy/lib/_loadmaterials.py +83 -0
  78. honeybee_energy/lib/_loadprogramtypes.py +125 -0
  79. honeybee_energy/lib/_loadschedules.py +87 -0
  80. honeybee_energy/lib/_loadtypelimits.py +64 -0
  81. honeybee_energy/lib/constructions.py +207 -0
  82. honeybee_energy/lib/constructionsets.py +95 -0
  83. honeybee_energy/lib/materials.py +67 -0
  84. honeybee_energy/lib/programtypes.py +125 -0
  85. honeybee_energy/lib/schedules.py +61 -0
  86. honeybee_energy/lib/scheduletypelimits.py +31 -0
  87. honeybee_energy/load/__init__.py +1 -0
  88. honeybee_energy/load/_base.py +190 -0
  89. honeybee_energy/load/daylight.py +397 -0
  90. honeybee_energy/load/dictutil.py +47 -0
  91. honeybee_energy/load/equipment.py +771 -0
  92. honeybee_energy/load/hotwater.py +543 -0
  93. honeybee_energy/load/infiltration.py +460 -0
  94. honeybee_energy/load/lighting.py +480 -0
  95. honeybee_energy/load/people.py +497 -0
  96. honeybee_energy/load/process.py +472 -0
  97. honeybee_energy/load/setpoint.py +816 -0
  98. honeybee_energy/load/ventilation.py +502 -0
  99. honeybee_energy/material/__init__.py +1 -0
  100. honeybee_energy/material/_base.py +166 -0
  101. honeybee_energy/material/dictutil.py +59 -0
  102. honeybee_energy/material/frame.py +367 -0
  103. honeybee_energy/material/gas.py +1087 -0
  104. honeybee_energy/material/glazing.py +854 -0
  105. honeybee_energy/material/opaque.py +1351 -0
  106. honeybee_energy/material/shade.py +1360 -0
  107. honeybee_energy/measure.py +472 -0
  108. honeybee_energy/programtype.py +723 -0
  109. honeybee_energy/properties/__init__.py +1 -0
  110. honeybee_energy/properties/aperture.py +333 -0
  111. honeybee_energy/properties/door.py +342 -0
  112. honeybee_energy/properties/extension.py +244 -0
  113. honeybee_energy/properties/face.py +274 -0
  114. honeybee_energy/properties/model.py +2621 -0
  115. honeybee_energy/properties/room.py +1747 -0
  116. honeybee_energy/properties/shade.py +314 -0
  117. honeybee_energy/properties/shademesh.py +262 -0
  118. honeybee_energy/reader.py +48 -0
  119. honeybee_energy/result/__init__.py +1 -0
  120. honeybee_energy/result/colorobj.py +648 -0
  121. honeybee_energy/result/emissions.py +290 -0
  122. honeybee_energy/result/err.py +101 -0
  123. honeybee_energy/result/eui.py +100 -0
  124. honeybee_energy/result/generation.py +160 -0
  125. honeybee_energy/result/loadbalance.py +890 -0
  126. honeybee_energy/result/match.py +202 -0
  127. honeybee_energy/result/osw.py +90 -0
  128. honeybee_energy/result/rdd.py +59 -0
  129. honeybee_energy/result/zsz.py +190 -0
  130. honeybee_energy/run.py +1573 -0
  131. honeybee_energy/schedule/__init__.py +1 -0
  132. honeybee_energy/schedule/day.py +626 -0
  133. honeybee_energy/schedule/dictutil.py +59 -0
  134. honeybee_energy/schedule/fixedinterval.py +1012 -0
  135. honeybee_energy/schedule/rule.py +619 -0
  136. honeybee_energy/schedule/ruleset.py +1867 -0
  137. honeybee_energy/schedule/typelimit.py +310 -0
  138. honeybee_energy/shw.py +315 -0
  139. honeybee_energy/simulation/__init__.py +1 -0
  140. honeybee_energy/simulation/control.py +214 -0
  141. honeybee_energy/simulation/daylightsaving.py +185 -0
  142. honeybee_energy/simulation/dictutil.py +51 -0
  143. honeybee_energy/simulation/output.py +646 -0
  144. honeybee_energy/simulation/parameter.py +606 -0
  145. honeybee_energy/simulation/runperiod.py +443 -0
  146. honeybee_energy/simulation/shadowcalculation.py +295 -0
  147. honeybee_energy/simulation/sizing.py +546 -0
  148. honeybee_energy/ventcool/__init__.py +5 -0
  149. honeybee_energy/ventcool/_crack_data.py +91 -0
  150. honeybee_energy/ventcool/afn.py +289 -0
  151. honeybee_energy/ventcool/control.py +269 -0
  152. honeybee_energy/ventcool/crack.py +126 -0
  153. honeybee_energy/ventcool/fan.py +493 -0
  154. honeybee_energy/ventcool/opening.py +365 -0
  155. honeybee_energy/ventcool/simulation.py +314 -0
  156. honeybee_energy/writer.py +1084 -0
  157. honeybee_energy-1.116.64.dist-info/METADATA +113 -0
  158. honeybee_energy-1.116.64.dist-info/RECORD +162 -0
  159. honeybee_energy-1.116.64.dist-info/WHEEL +5 -0
  160. honeybee_energy-1.116.64.dist-info/entry_points.txt +2 -0
  161. honeybee_energy-1.116.64.dist-info/licenses/LICENSE +661 -0
  162. honeybee_energy-1.116.64.dist-info/top_level.txt +1 -0
honeybee_energy/run.py ADDED
@@ -0,0 +1,1573 @@
1
+ # coding=utf-8
2
+ """Module for running IDF files through EnergyPlus."""
3
+ from __future__ import division
4
+
5
+ import os
6
+ import sys
7
+ import json
8
+ import shutil
9
+ import subprocess
10
+ import xml.etree.ElementTree as ET
11
+
12
+ if (sys.version_info < (3, 0)):
13
+ readmode = 'rb'
14
+ writemode = 'wb'
15
+ else:
16
+ readmode = 'r'
17
+ writemode = 'w'
18
+
19
+ from ladybug.futil import write_to_file, preparedir
20
+ from honeybee.model import Model
21
+ from honeybee.facetype import Wall
22
+ from honeybee.boundarycondition import Surface, boundary_conditions
23
+ from honeybee.units import parse_distance_string, conversion_factor_to_meters
24
+ from honeybee.config import folders as hb_folders
25
+
26
+ from .config import folders
27
+ from .measure import Measure
28
+ from .result.osw import OSW
29
+
30
+ HB_OS_MSG = 'Honeybee-openstudio is not installed. Translation to OpenStudio cannot ' \
31
+ 'be performed.\nRun pip install honeybee-energy[openstudio] to get all ' \
32
+ 'dependencies needed for OpenStudio translation.'
33
+
34
+
35
+ def to_openstudio_sim_folder(
36
+ model, directory, epw_file=None, sim_par=None, schedule_directory=None,
37
+ enforce_rooms=False, use_geometry_names=False, use_resource_names=False,
38
+ additional_measures=None, base_osw=None, strings_to_inject=None,
39
+ report_units=None, viz_variables=None, print_progress=False):
40
+ """Create a .osw to translate honeybee JSONs to an .osm file.
41
+
42
+ Args:
43
+ model: The Honeybee Model to be converted into an OpenStudio Model. This
44
+ can also be the path to an .osm file that is already written into
45
+ the directory, in which case the process of translating the model
46
+ will be skipped and this function will only evaluate whether an OSW
47
+ is needed to run the simulation or a directly-translated IDF is suitable.
48
+ directory: The directory into which the output files will be written to.
49
+ epw_file: Optional file path to an EPW that should be associated with the
50
+ output energy model. If None, no EPW file will be assigned to the
51
+ resulting OpenStudio Model and the OSM will not have everything it
52
+ needs to be simulate-able in EnergyPlus. (Default: None).
53
+ sim_par: Optional SimulationParameter object that describes all of the
54
+ settings for the simulation. If None, the resulting OSM will not have
55
+ everything it needs to be simulate-able in EnergyPlus. (Default: None).
56
+ schedule_directory: An optional file directory to which all file-based
57
+ schedules should be written to. If None, all ScheduleFixedIntervals
58
+ will be translated to Schedule:Compact and written fully into the
59
+ IDF string instead of to Schedule:File. (Default: None).
60
+ enforce_rooms: Boolean to note whether this method should enforce the
61
+ presence of Rooms in the Model, which is as necessary prerequisite
62
+ for simulation in EnergyPlus. (Default: False).
63
+ use_geometry_names: Boolean to note whether a cleaned version of all
64
+ geometry display names should be used instead of identifiers when
65
+ translating the Model to OSM and IDF. Using this flag will affect
66
+ all Rooms, Faces, Apertures, Doors, and Shades. It will generally
67
+ result in more read-able names in the OSM and IDF but this means
68
+ that it will not be easy to map the EnergyPlus results back to the
69
+ input Honeybee Model. Cases of duplicate IDs resulting from
70
+ non-unique names will be resolved by adding integers to the ends
71
+ of the new IDs that are derived from the name. (Default: False).
72
+ use_resource_names: Boolean to note whether a cleaned version of all
73
+ resource display names should be used instead of identifiers when
74
+ translating the Model to OSM and IDF. Using this flag will affect
75
+ all Materials, Constructions, ConstructionSets, Schedules, Loads,
76
+ and ProgramTypes. It will generally result in more read-able names
77
+ for the resources in the OSM and IDF. Cases of duplicate IDs
78
+ resulting from non-unique names will be resolved by adding integers
79
+ to the ends of the new IDs that are derived from the name. (Default: False).
80
+ additional_measures: An optional array of honeybee-energy Measure objects
81
+ to be included in the output osw. These Measure objects must have
82
+ values for all required input arguments or an exception will be
83
+ raised while running this function. (Default: None).
84
+ base_osw: Optional file path to an existing OSW JSON be used as the base
85
+ for the output .osw. This is another way that outside measures
86
+ can be incorporated into the workflow. (Default: None).
87
+ strings_to_inject: An additional text string to get appended to the IDF
88
+ before simulation. The input should include complete EnergyPlus objects
89
+ as a single string following the IDF format.
90
+ report_units: A text value to set the units of the OpenStudio Results report
91
+ that can optionally be included in the OSW. If set to None, no report
92
+ will be produced. (Default: None). Choose from the following.
93
+
94
+ * si - all units will be in SI
95
+ * ip - all units will be in IP
96
+
97
+ viz_variables: An optional list of EnergyPlus output variable names to
98
+ be visualized on the geometry in an output view_data HTML report.
99
+ If None or an empty list, no view_data report is produced. See below
100
+ for an example.
101
+ print_progress: Set to True to have the progress of the translation
102
+ printed as it is completed.
103
+
104
+ .. code-block:: python
105
+
106
+ viz_variables = [
107
+ "Zone Air System Sensible Heating Rate",
108
+ "Zone Air System Sensible Cooling Rate"
109
+ ]
110
+
111
+ Returns:
112
+ A series of file paths to the simulation input files.
113
+
114
+ - osm -- Path to an OpenStudio Model (.osm) file containing the direct
115
+ translation of the Honeybee Model to OpenStudio. None of the
116
+ additional_measures will be applied to this OSM if they are specified
117
+ and any efficiency standards in the simulation parameters won't be
118
+ applied until the OSW is run.
119
+
120
+ - osw -- Path to an OpenStudio Workflow (.osw) JSON file that can be run
121
+ with the OpenStudio CLI. Will be None if the OpenStudio CLI is not
122
+ needed for simulation. This can happen if there is no base_osw,
123
+ no additional_measures, no report_units or viz_variables, and no
124
+ efficiency standard specified in the simulation parameters (which
125
+ requires openstudio-standards).
126
+
127
+ - idf -- Path to an EnergyPlus Input Data File (.idf) that is ready to
128
+ be simulated in EnergyPlus. Will be None if there is an OSW that must
129
+ be run to create the IDF for simulation.
130
+ """
131
+ # check that honeybee-openstudio is installed
132
+ try:
133
+ from honeybee_openstudio.openstudio import openstudio, OSModel, os_path
134
+ from honeybee_openstudio.simulation import simulation_parameter_to_openstudio, \
135
+ assign_epw_to_model
136
+ from honeybee_openstudio.writer import model_to_openstudio
137
+ except ImportError as e: # honeybee-openstudio is not installed
138
+ raise ImportError('{}\n{}'.format(HB_OS_MSG, e))
139
+
140
+ # write the OpenStudio model into the directory
141
+ if not os.path.isdir(directory):
142
+ os.makedirs(directory)
143
+ osm = os.path.abspath(os.path.join(directory, 'in.osm'))
144
+ if isinstance(model, str):
145
+ assert os.path.isfile(model), \
146
+ 'No file path for an OSM was found at "{}"'.format(model)
147
+ model = os.path.abspath(model)
148
+ if os.path.normcase(model) == os.path.normcase(osm):
149
+ shutil.copy(model, osm)
150
+ osm, os_model = model, None
151
+ else: # translate the Honeybee SimPar and Model to OpenStudio
152
+ os_model = OSModel()
153
+ set_cz = True
154
+ if sim_par is not None and sim_par.sizing_parameter.climate_zone is not None:
155
+ set_cz = False
156
+ if epw_file is not None:
157
+ assign_epw_to_model(epw_file, os_model, set_cz)
158
+ if sim_par is not None:
159
+ simulation_parameter_to_openstudio(sim_par, os_model)
160
+ os_model = model_to_openstudio(
161
+ model, os_model, schedule_directory=schedule_directory,
162
+ use_geometry_names=use_geometry_names, use_resource_names=use_resource_names,
163
+ enforce_rooms=enforce_rooms, print_progress=print_progress)
164
+ os_model.save(os_path(osm), overwrite=True)
165
+
166
+ # load the OpenStudio Efficiency Standards measure if one is specified
167
+ if sim_par is not None and sim_par.sizing_parameter.efficiency_standard is not None:
168
+ assert folders.efficiency_standard_measure_path is not None, \
169
+ 'Efficiency standard was specified in the simulation parameters ' \
170
+ 'but the efficiency_standard measure is not installed.'
171
+ eff_measure = Measure(folders.efficiency_standard_measure_path)
172
+ units_arg = eff_measure.arguments[0]
173
+ units_arg.value = sim_par.sizing_parameter.efficiency_standard
174
+ if additional_measures is None:
175
+ additional_measures = [eff_measure]
176
+ else:
177
+ additional_measures = list(additional_measures)
178
+ additional_measures.append(eff_measure)
179
+
180
+ # load the OpenStudio Results measure if report_units have bee specified
181
+ if report_units is not None and report_units.lower() != 'none':
182
+ assert folders.openstudio_results_measure_path is not None, 'OpenStudio report' \
183
+ ' requested but the openstudio_results measure is not installed.'
184
+ report_measure = Measure(folders.openstudio_results_measure_path)
185
+ units_arg = report_measure.arguments[0]
186
+ units_arg.value = report_units.upper()
187
+ if additional_measures is None:
188
+ additional_measures = [report_measure]
189
+ else:
190
+ additional_measures = list(additional_measures)
191
+ additional_measures.append(report_measure)
192
+
193
+ # load the OpenStudio view_data measure if outputs have been requested
194
+ if viz_variables is not None and len(viz_variables) != 0:
195
+ assert folders.view_data_measure_path is not None, 'A visualization variable' \
196
+ 'has been requested but the view_data measure is not installed.'
197
+ viz_measure = Measure(folders.view_data_measure_path)
198
+ if len(viz_variables) > 3:
199
+ viz_variables = viz_variables[:3]
200
+ for i, var in enumerate(viz_variables):
201
+ var_arg = viz_measure.arguments[i + 2]
202
+ var_arg.value = var
203
+ if additional_measures is None:
204
+ additional_measures = [viz_measure]
205
+ else:
206
+ additional_measures = list(additional_measures)
207
+ additional_measures.append(viz_measure)
208
+
209
+ osw, idf = None, None
210
+ if additional_measures or base_osw: # prepare an OSW with all of the measures to run
211
+ # load the inject IDF measure if strings_to_inject have bee specified
212
+ if strings_to_inject is not None and strings_to_inject != '':
213
+ assert folders.inject_idf_measure_path is not None, \
214
+ 'Additional IDF strings input but the inject_idf measure is not installed.'
215
+ idf_measure = Measure(folders.inject_idf_measure_path)
216
+ inject_idf = os.path.join(directory, 'inject.idf')
217
+ with open(inject_idf, "w") as idf_file:
218
+ idf_file.write(strings_to_inject)
219
+ units_arg = idf_measure.arguments[0]
220
+ units_arg.value = inject_idf
221
+ if additional_measures is None:
222
+ additional_measures = [idf_measure]
223
+ else:
224
+ additional_measures = list(additional_measures)
225
+ additional_measures.append(idf_measure)
226
+ # create a dictionary representation of the .osw
227
+ if base_osw is None:
228
+ osw_dict = {'steps': []}
229
+ else:
230
+ assert os.path.isfile(base_osw), \
231
+ 'No base OSW file found at {}.'.format(base_osw)
232
+ with open(base_osw, readmode) as base_file:
233
+ osw_dict = json.load(base_file)
234
+ osw_dict['seed_file'] = osm
235
+ if schedule_directory is not None:
236
+ if 'file_paths' not in osw_dict:
237
+ osw_dict['file_paths'] = [schedule_directory]
238
+ else:
239
+ osw_dict['file_paths'].append(schedule_directory)
240
+ if epw_file is not None:
241
+ osw_dict['weather_file'] = epw_file
242
+ if additional_measures is not None:
243
+ if 'measure_paths' not in osw_dict:
244
+ osw_dict['measure_paths'] = []
245
+ measure_paths = set() # set of all unique measure paths
246
+ # ensure measures are correctly ordered
247
+ m_dict = {'ModelMeasure': [], 'EnergyPlusMeasure': [], 'ReportingMeasure': []}
248
+ for measure in additional_measures:
249
+ m_dict[measure.type].append(measure)
250
+ sorted_measures = m_dict['ModelMeasure'] + m_dict['EnergyPlusMeasure'] + \
251
+ m_dict['ReportingMeasure']
252
+ # add the measures and the measure paths to the OSW
253
+ for measure in sorted_measures:
254
+ measure.validate() # ensure that all required arguments have values
255
+ measure_paths.add(os.path.dirname(measure.folder))
256
+ osw_dict['steps'].append(measure.to_osw_dict()) # add measure to workflow
257
+ for m_path in measure_paths:
258
+ osw_dict['measure_paths'].append(m_path)
259
+ # if there were reporting measures, add the ladybug adapter to get sim progress
260
+ adapter = folders.honeybee_adapter_path
261
+ if adapter is not None:
262
+ if 'run_options' not in osw_dict:
263
+ osw_dict['run_options'] = {}
264
+ osw_dict['run_options']['output_adapter'] = {
265
+ 'custom_file_name': adapter,
266
+ 'class_name': 'HoneybeeAdapter',
267
+ 'options': {}
268
+ }
269
+ # write the dictionary to a workflow.osw
270
+ osw = os.path.abspath(os.path.join(directory, 'workflow.osw'))
271
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
272
+ with open(osw, writemode) as fp:
273
+ workflow_str = json.dumps(osw_dict, indent=4, ensure_ascii=False)
274
+ fp.write(workflow_str.encode('utf-8'))
275
+ else:
276
+ with open(osw, writemode, encoding='utf-8') as fp:
277
+ workflow_str = json.dump(osw_dict, fp, indent=4, ensure_ascii=False)
278
+ else: # the OSM is ready for simulation; translate it to IDF
279
+ run_directory = os.path.join(directory, 'run')
280
+ if not os.path.isdir(run_directory):
281
+ os.mkdir(run_directory)
282
+ idf = os.path.abspath(os.path.join(run_directory, 'in.idf'))
283
+ if not os.path.isfile(idf):
284
+ if os_model is None: # load the model from the file
285
+ exist_os_model = OSModel.load(os_path(osm))
286
+ if exist_os_model.is_initialized():
287
+ os_model = exist_os_model.get()
288
+ if (sys.version_info < (3, 0)):
289
+ idf_translator = openstudio.EnergyPlusForwardTranslator()
290
+ else:
291
+ idf_translator = openstudio.energyplus.ForwardTranslator()
292
+ workspace = idf_translator.translateModel(os_model)
293
+ workspace.save(os_path(idf), overwrite=True)
294
+ if strings_to_inject is not None and strings_to_inject != '':
295
+ with open(idf, 'a') as idf_file:
296
+ idf_file.write(strings_to_inject)
297
+
298
+ return osm, osw, idf
299
+
300
+
301
+ def from_gbxml_osw(gbxml_path, model_path=None, osw_directory=None):
302
+ """Create a .osw to translate gbXML to a HBJSON file.
303
+
304
+ Args:
305
+ gbxml_path: File path to the gbXML to be translated to HBJSON.
306
+ model_path: File path to where the Model HBJSON will be written. If None, it
307
+ will be output right next to the input file and given the same name.
308
+ osw_directory: The directory into which the .osw should be written. If None,
309
+ it will be written into the a temp folder in the default simulation folder.
310
+ """
311
+ return _import_model_osw(gbxml_path, 'gbxml', model_path, osw_directory)
312
+
313
+
314
+ def from_osm_osw(osm_path, model_path=None, osw_directory=None):
315
+ """Create a .osw to translate OSM to a HBJSON file.
316
+
317
+ Args:
318
+ osm_path: File path to the OSM to be translated to HBJSON.
319
+ model_path: File path to where the Model HBJSON will be written. If None, it
320
+ will be output right next to the input file and given the same name.
321
+ osw_directory: The directory into which the .osw should be written. If None,
322
+ it will be written into the a temp folder in the default simulation folder.
323
+ """
324
+ return _import_model_osw(osm_path, 'openstudio', model_path, osw_directory)
325
+
326
+
327
+ def from_idf_osw(idf_path, model_path=None, osw_directory=None):
328
+ """Create a .osw to translate IDF to a HBJSON file.
329
+
330
+ Args:
331
+ idf_path: File path to the IDF to be translated to HBJSON.
332
+ model_path: File path to where the Model HBJSON will be written. If None, it
333
+ will be output right next to the input file and given the same name.
334
+ osw_directory: The directory into which the .osw should be written. If None,
335
+ it will be written into the a temp folder in the default simulation folder.
336
+ """
337
+ return _import_model_osw(idf_path, 'idf', model_path, osw_directory)
338
+
339
+
340
+ def measure_compatible_model_json(
341
+ model_file_path, destination_directory=None, simplify_window_cons=False,
342
+ triangulate_sub_faces=True, triangulate_non_planar_orphaned=False,
343
+ enforce_rooms=False, use_geometry_names=False, use_resource_names=False):
344
+ """Convert a Model file to one that is compatible with the honeybee_openstudio_gem.
345
+
346
+ This includes the re-serialization of the Model to Python, which will
347
+ automatically ensure that all Apertures and Doors point in the same direction
348
+ as their parent Face. If the Model tolerance is non-zero and Rooms are closed
349
+ solids, this will also ensure that all Room Faces point outwards from their
350
+ parent's volume. If the Model units are not Meters, the model will be scaled
351
+ to be in Meters. Geometries with holes will have the vertices re-specified
352
+ in a manner that they are translated to EnergyPlus as a single list. Lastly,
353
+ apertures and doors with more than 4 vertices will be triangulated to ensure
354
+ EnergyPlus accepts them.
355
+
356
+ Args:
357
+ model_file_path: File path to a Honeybee Model as a HBJSON or HBpkl.
358
+ destination_directory: The directory into which the Model JSON that is
359
+ compatible with the honeybee_openstudio_gem should be written. If None,
360
+ this will be the same location as the input model_json_path. (Default: None).
361
+ simplify_window_cons: Boolean to note whether window constructions should
362
+ be simplified during the translation. This is useful when the ultimate
363
+ destination of the OSM is a format that does not supported layered
364
+ window constructions (like gbXML). (Default: False).
365
+ triangulate_sub_faces: Boolean to note whether sub-faces (including
366
+ Apertures and Doors) should be triangulated if they have more than
367
+ 4 sides (True) or whether they should be left as they are (False).
368
+ This triangulation is necessary when exporting directly to EnergyPlus
369
+ since it cannot accept sub-faces with more than 4 vertices. (Default: True).
370
+ triangulate_non_planar_orphaned: Boolean to note whether any non-planar
371
+ orphaned geometry in the model should be triangulated upon export.
372
+ This can be helpful because OpenStudio simply raises an error when
373
+ it encounters non-planar geometry, which would hinder the ability
374
+ to save gbXML files that are to be corrected in other
375
+ software. (Default: False).
376
+ enforce_rooms: Boolean to note whether this method should enforce the
377
+ presence of Rooms in the Model, which is as necessary prerequisite
378
+ for simulation in EnergyPlus. (Default: False).
379
+ use_geometry_names: Boolean to note whether a cleaned version of all
380
+ geometry display names should be used instead of identifiers when
381
+ translating the Model to OSM and IDF. Using this flag will affect
382
+ all Rooms, Faces, Apertures, Doors, and Shades. It will generally
383
+ result in more read-able names in the OSM and IDF but this means
384
+ that it will not be easy to map the EnergyPlus results back to the
385
+ input Honeybee Model. Cases of duplicate IDs resulting from
386
+ non-unique names will be resolved by adding integers to the ends
387
+ of the new IDs that are derived from the name. (Default: False).
388
+ use_resource_names: Boolean to note whether a cleaned version of all
389
+ resource display names should be used instead of identifiers when
390
+ translating the Model to OSM and IDF. Using this flag will affect
391
+ all Materials, Constructions, ConstructionSets, Schedules, Loads,
392
+ and ProgramTypes. It will generally result in more read-able names
393
+ for the resources in the OSM and IDF. Cases of duplicate IDs
394
+ resulting from non-unique names will be resolved by adding integers
395
+ to the ends of the new IDs that are derived from the name. (Default: False).
396
+
397
+ Returns:
398
+ The full file path to the new Model JSON written out by this method.
399
+ """
400
+ # check that the file is there
401
+ assert os.path.isfile(model_file_path), \
402
+ 'No file found at {}.'.format(model_file_path)
403
+
404
+ # get the directory and the file path for the new Model JSON
405
+ directory, _ = os.path.split(model_file_path)
406
+ dest_dir = directory if destination_directory is None else destination_directory
407
+ dest_file_path = os.path.join(dest_dir, 'in.hbjson')
408
+
409
+ # serialize the Model to Python
410
+ parsed_model = Model.from_file(model_file_path)
411
+ if enforce_rooms:
412
+ assert len(parsed_model.rooms) != 0, \
413
+ 'Model contains no Rooms and therefore cannot be simulated in EnergyPlus.'
414
+
415
+ # remove degenerate geometry within native E+ tolerance of 0.01 meters
416
+ original_model = parsed_model
417
+ parsed_model.convert_to_units('Meters')
418
+ try:
419
+ parsed_model.remove_degenerate_geometry(0.01)
420
+ except ValueError:
421
+ error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \
422
+ 'Is this correct?'.format(original_model.units)
423
+ raise ValueError(error)
424
+ if triangulate_non_planar_orphaned:
425
+ parsed_model.triangulate_non_planar_quads(0.01)
426
+
427
+ # remove the HVAC from any Rooms lacking setpoints
428
+ rem_msgs = parsed_model.properties.energy.remove_hvac_from_no_setpoints()
429
+ if len(rem_msgs) != 0:
430
+ print('\n'.join(rem_msgs))
431
+
432
+ # reset the IDs to be derived from the display_names if requested
433
+ if use_geometry_names:
434
+ id_map = parsed_model.reset_ids()
435
+ parsed_model.properties.energy.sync_detailed_hvac_ids(id_map['rooms'])
436
+ if use_resource_names:
437
+ parsed_model.properties.energy.reset_resource_ids()
438
+ if simplify_window_cons: # simpler format that cannot handle certain identifiers
439
+ for room in parsed_model.rooms:
440
+ if room.story is not None and room.story.startswith('-'):
441
+ room.story = 'neg{}'.format(room.story[1:])
442
+
443
+ # get the dictionary representation of the Model and add auto-calculated properties
444
+ model_dict = parsed_model.to_dict(triangulate_sub_faces=triangulate_sub_faces)
445
+ parsed_model.properties.energy.add_autocal_properties_to_dict(model_dict)
446
+ if simplify_window_cons:
447
+ parsed_model.properties.energy.simplify_window_constructions_in_dict(model_dict)
448
+
449
+ # write the dictionary into a file
450
+ preparedir(dest_dir, remove_content=False) # create the directory if it's not there
451
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
452
+ with open(dest_file_path, writemode) as fp:
453
+ model_str = json.dumps(model_dict, ensure_ascii=False)
454
+ fp.write(model_str.encode('utf-8'))
455
+ else:
456
+ with open(dest_file_path, writemode, encoding='utf-8') as fp:
457
+ json.dump(model_dict, fp, ensure_ascii=False)
458
+
459
+ return os.path.abspath(dest_file_path)
460
+
461
+
462
+ def trace_compatible_model_json(
463
+ model_file_path, destination_directory=None, single_window=True,
464
+ rect_sub_distance='0.15m', frame_merge_distance='0.2m'):
465
+ """Convert a Model to one that is compatible with exporting to TRANE TRACE 3D Plus.
466
+
467
+ The resulting HBJSON is intended to be serialized to gbXML for import into
468
+ TRACE 3D Plus. To handle TRACE's limitations, all rooms in the model will be
469
+ converted to extrusions with flat roofs and all Apertures will be converted
470
+ to simple rectangles. In this process, priority is given to preserving the volume
471
+ of the original detailed geometry and the original window area.
472
+
473
+ Args:
474
+ model_file_path: File path to a Honeybee Model as a HBJSON or HBpkl.
475
+ destination_directory: The directory into which the Model JSON that is
476
+ compatible with the honeybee_openstudio_gem should be written. If None,
477
+ this will be the same location as the input model_json_path. (Default: None).
478
+ single_window: A boolean for whether all windows within walls should be
479
+ converted to a single window with an area that matches the original
480
+ geometry. (Default: True).
481
+ rect_sub_distance: Text string of a number for the resolution at which
482
+ non-rectangular Apertures will be subdivided into smaller rectangular
483
+ units. This is required as TRACE 3D plus cannot model non-rectangular
484
+ geometries. This can include the units of the distance (eg. 0.5ft) or,
485
+ if no units are provided, the value will be interpreted in the
486
+ honeybee model units. (Default: 0.15m).
487
+ frame_merge_distance: Text string of a number for the maximum distance
488
+ between non-rectangular Apertures at which point the Apertures will
489
+ be merged into a single rectangular geometry. This is often helpful
490
+ when there are several triangular Apertures that together make a
491
+ rectangle when they are merged across their frames. This can include
492
+ the units of the distance (eg. 0.5ft) or, if no units are provided,
493
+ the value will be interpreted in the honeybee model units. (Default: 0.2m).
494
+
495
+ Returns:
496
+ The full file path to the new Model JSON written out by this method.
497
+ """
498
+ # check that the file is there
499
+ assert os.path.isfile(model_file_path), \
500
+ 'No file found at {}.'.format(model_file_path)
501
+
502
+ # get the directory and the file path for the new Model JSON
503
+ directory, _ = os.path.split(model_file_path)
504
+ dest_dir = directory if destination_directory is None else destination_directory
505
+ dest_file_path = os.path.join(dest_dir, 'in.hbjson')
506
+
507
+ # serialize the Model to Python
508
+ parsed_model = Model.from_file(model_file_path)
509
+
510
+ # make sure there are rooms and remove all shades and orphaned objects
511
+ assert len(parsed_model.rooms) != 0, \
512
+ 'Model contains no Rooms and therefore cannot be simulated in TRACE.'
513
+ parsed_model.remove_all_shades()
514
+ parsed_model.remove_faces()
515
+ parsed_model.remove_apertures()
516
+ parsed_model.remove_doors()
517
+
518
+ # remove degenerate geometry within native E+ tolerance of 0.01 meters
519
+ original_units = parsed_model.units
520
+ parsed_model.convert_to_units('Meters')
521
+ try:
522
+ parsed_model.remove_degenerate_geometry(0.01)
523
+ except ValueError:
524
+ error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \
525
+ 'Is this correct?'.format(original_units)
526
+ raise ValueError(error)
527
+ rect_sub_distance = parse_distance_string(rect_sub_distance, original_units)
528
+ frame_merge_distance = parse_distance_string(frame_merge_distance, original_units)
529
+ if original_units != 'Meters':
530
+ c_factor = conversion_factor_to_meters(original_units)
531
+ rect_sub_distance = rect_sub_distance * c_factor
532
+ frame_merge_distance = frame_merge_distance * c_factor
533
+
534
+ # remove all interior windows in the model
535
+ for room in parsed_model.rooms:
536
+ for face in room.faces:
537
+ if isinstance(face.boundary_condition, Surface):
538
+ face.remove_sub_faces()
539
+
540
+ # convert all rooms to extrusions and patch the resulting missing adjacencies
541
+ parsed_model.rooms_to_extrusions()
542
+ parsed_model.properties.energy.missing_adjacencies_to_adiabatic()
543
+
544
+ # convert windows in walls to a single geometry
545
+ if single_window:
546
+ for room in parsed_model.rooms:
547
+ for face in room.faces:
548
+ if isinstance(face.type, Wall) and face.has_sub_faces:
549
+ face.boundary_condition = boundary_conditions.outdoors
550
+ face.apertures_by_ratio(face.aperture_ratio, 0.01, rect_split=False)
551
+
552
+ # convert all of the Aperture geometries to rectangles so they can be translated
553
+ parsed_model.rectangularize_apertures(
554
+ subdivision_distance=rect_sub_distance, max_separation=frame_merge_distance,
555
+ merge_all=True, resolve_adjacency=False
556
+ )
557
+
558
+ # if there are still multiple windows in a given Face, ensure they do not touch
559
+ for room in parsed_model.rooms:
560
+ for face in room.faces:
561
+ if len(face.apertures) > 1:
562
+ face.offset_aperture_edges(-0.01, 0.01)
563
+
564
+ # re-solve adjacency given that all of the previous operations have messed with it
565
+ parsed_model.solve_adjacency(merge_coplanar=True, intersect=True, overwrite=True)
566
+
567
+ # reset all display_names so that they are unique (derived from reset identifiers)
568
+ parsed_model.reset_ids() # sets the identifiers based on the display_name
569
+ for room in parsed_model.rooms:
570
+ room.display_name = None
571
+ for face in room.faces:
572
+ face.display_name = None
573
+ for ap in face.apertures:
574
+ ap.display_name = None
575
+ for dr in face.apertures:
576
+ dr.display_name = None
577
+ if room.story is not None and room.story.startswith('-'):
578
+ room.story = 'neg{}'.format(room.story[1:])
579
+
580
+ # remove the HVAC from any Rooms lacking setpoints
581
+ rem_msgs = parsed_model.properties.energy.remove_hvac_from_no_setpoints()
582
+ if len(rem_msgs) != 0:
583
+ print('\n'.join(rem_msgs))
584
+
585
+ # get the dictionary representation of the Model and add auto-calculated properties
586
+ model_dict = parsed_model.to_dict()
587
+ parsed_model.properties.energy.add_autocal_properties_to_dict(
588
+ model_dict, exclude_hole_verts=True)
589
+ parsed_model.properties.energy.simplify_window_constructions_in_dict(model_dict)
590
+
591
+ # write the dictionary into a file
592
+ preparedir(dest_dir, remove_content=False) # create the directory if it's not there
593
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
594
+ with open(dest_file_path, writemode) as fp:
595
+ model_str = json.dumps(model_dict, ensure_ascii=False)
596
+ fp.write(model_str.encode('utf-8'))
597
+ else:
598
+ with open(dest_file_path, writemode, encoding='utf-8') as fp:
599
+ json.dump(model_dict, fp, ensure_ascii=False)
600
+
601
+ return os.path.abspath(dest_file_path)
602
+
603
+
604
+ def to_openstudio_osw(osw_directory, model_path, sim_par_json_path=None,
605
+ additional_measures=None, base_osw=None, epw_file=None,
606
+ schedule_directory=None, strings_to_inject=None,
607
+ report_units=None, viz_variables=None):
608
+ """Create a .osw to translate honeybee JSONs to an .osm file.
609
+
610
+ Args:
611
+ osw_directory: The directory into which the .osw should be written and the
612
+ .osm will eventually be written into.
613
+ model_path: File path to the Model as either a HBJSON or an OSM.
614
+ sim_par_json_path: Optional file path to the SimulationParameter JSON.
615
+ If None, the resulting OSM will not have everything it needs to be
616
+ simulate-able in EnergyPlus. (Default: None).
617
+ additional_measures: An optional array of honeybee-energy Measure objects
618
+ to be included in the output osw. These Measure objects must have
619
+ values for all required input arguments or an exception will be
620
+ raised while running this function. (Default: None).
621
+ base_osw: Optional file path to an existing OSW JSON be used as the base
622
+ for the output .osw. This is another way that outside measures
623
+ can be incorporated into the workflow. (Default: None).
624
+ epw_file: Optional file path to an EPW that should be associated with the
625
+ output energy model. If None, no EPW file will be written into the
626
+ OSW. (Default: None).
627
+ schedule_directory: An optional file directory to which all file-based
628
+ schedules should be written to. If None, all ScheduleFixedIntervals
629
+ will be translated to Schedule:Compact and written fully into the
630
+ IDF string instead of to Schedule:File. (Default: None).
631
+ strings_to_inject: An additional text string to get appended to the IDF
632
+ before simulation. The input should include complete EnergyPlus objects
633
+ as a single string following the IDF format.
634
+ report_units: A text value to set the units of the OpenStudio Results report
635
+ that can optionally be included in the OSW. If set to None, no report
636
+ will be produced. (Default: None). Choose from the following.
637
+
638
+ * si - all units will be in SI
639
+ * ip - all units will be in IP
640
+
641
+ viz_variables: An optional list of EnergyPlus output variable names to
642
+ be visualized on the geometry in an output view_data HTML report.
643
+ If None or an empty list, no view_data report is produced. See below
644
+ for an example.
645
+
646
+ .. code-block:: python
647
+
648
+ viz_variables = [
649
+ "Zone Air System Sensible Heating Rate",
650
+ "Zone Air System Sensible Cooling Rate"
651
+ ]
652
+
653
+ Returns:
654
+ The file path to the .osw written out by this method.
655
+ """
656
+ # create a dictionary representation of the .osw with steps to run
657
+ # the model measure and the simulation parameter measure
658
+ if base_osw is None:
659
+ osw_dict = {'steps': []}
660
+ else:
661
+ assert os.path.isfile(base_osw), 'No base OSW file found at {}.'.format(base_osw)
662
+ with open(base_osw, readmode) as base_file:
663
+ osw_dict = json.load(base_file)
664
+
665
+ # add the model json serialization into the steps
666
+ if model_path.lower().endswith('.osm'): # use the OSM as a seed file
667
+ osw_dict['seed_file'] = model_path
668
+ else: # assume that it is a HBJSON
669
+ model_measure_dict = {
670
+ 'arguments': {
671
+ 'model_json': model_path
672
+ },
673
+ 'measure_dir_name': 'from_honeybee_model'
674
+ }
675
+ if schedule_directory is not None:
676
+ model_measure_dict['arguments']['schedule_csv_dir'] = schedule_directory
677
+ osw_dict['steps'].insert(0, model_measure_dict)
678
+
679
+ # add a simulation parameter step if it is specified
680
+ if sim_par_json_path is not None:
681
+ sim_par_dict = {
682
+ 'arguments': {
683
+ 'simulation_parameter_json': sim_par_json_path
684
+ },
685
+ 'measure_dir_name': 'from_honeybee_simulation_parameter'
686
+ }
687
+ osw_dict['steps'].insert(0, sim_par_dict)
688
+
689
+ # assign the measure_paths to the osw_dict
690
+ if 'measure_paths' not in osw_dict:
691
+ osw_dict['measure_paths'] = []
692
+ if folders.honeybee_openstudio_gem_path: # include honeybee-openstudio measure path
693
+ measure_dir = os.path.join(folders.honeybee_openstudio_gem_path, 'measures')
694
+ osw_dict['measure_paths'].append(measure_dir)
695
+
696
+ # assign the schedule_directory to the file_paths if it is specified
697
+ if schedule_directory is not None:
698
+ if 'file_paths' not in osw_dict:
699
+ osw_dict['file_paths'] = [schedule_directory]
700
+ else:
701
+ osw_dict['file_paths'].append(schedule_directory)
702
+
703
+ # load the inject IDF measure if strings_to_inject have bee specified
704
+ if strings_to_inject is not None and strings_to_inject != '':
705
+ assert folders.inject_idf_measure_path is not None, \
706
+ 'Additional IDF strings input but the inject_idf measure is not installed.'
707
+ idf_measure = Measure(folders.inject_idf_measure_path)
708
+ inject_idf = os.path.join(osw_directory, 'inject.idf')
709
+ with open(inject_idf, "w") as idf_file:
710
+ idf_file.write(strings_to_inject)
711
+ units_arg = idf_measure.arguments[0]
712
+ units_arg.value = inject_idf
713
+ if additional_measures is None:
714
+ additional_measures = [idf_measure]
715
+ else:
716
+ additional_measures = list(additional_measures)
717
+ additional_measures.append(idf_measure)
718
+
719
+ # load the OpenStudio Results measure if report_units have bee specified
720
+ if report_units is not None and report_units.lower() != 'none':
721
+ assert folders.openstudio_results_measure_path is not None, 'OpenStudio report' \
722
+ ' requested but the openstudio_results measure is not installed.'
723
+ report_measure = Measure(folders.openstudio_results_measure_path)
724
+ units_arg = report_measure.arguments[0]
725
+ units_arg.value = report_units.upper()
726
+ if additional_measures is None:
727
+ additional_measures = [report_measure]
728
+ else:
729
+ additional_measures = list(additional_measures)
730
+ additional_measures.append(report_measure)
731
+
732
+ # load the OpenStudio view_data measure if outputs have been requested
733
+ if viz_variables is not None and len(viz_variables) != 0:
734
+ assert folders.view_data_measure_path is not None, 'A visualization variable' \
735
+ 'has been requested but the view_data measure is not installed.'
736
+ viz_measure = Measure(folders.view_data_measure_path)
737
+ if len(viz_variables) > 3:
738
+ viz_variables = viz_variables[:3]
739
+ for i, var in enumerate(viz_variables):
740
+ var_arg = viz_measure.arguments[i + 2]
741
+ var_arg.value = var
742
+ if additional_measures is None:
743
+ additional_measures = [viz_measure]
744
+ else:
745
+ additional_measures = list(additional_measures)
746
+ additional_measures.append(viz_measure)
747
+
748
+ # add any additional measures to the osw_dict
749
+ if additional_measures:
750
+ measure_paths = set() # set of all unique measure paths
751
+ # ensure measures are correctly ordered
752
+ m_dict = {'ModelMeasure': [], 'EnergyPlusMeasure': [], 'ReportingMeasure': []}
753
+ for measure in additional_measures:
754
+ m_dict[measure.type].append(measure)
755
+ sorted_measures = m_dict['ModelMeasure'] + m_dict['EnergyPlusMeasure'] + \
756
+ m_dict['ReportingMeasure']
757
+ # add the measures and the measure paths to the OSW
758
+ for measure in sorted_measures:
759
+ measure.validate() # ensure that all required arguments have values
760
+ measure_paths.add(os.path.dirname(measure.folder))
761
+ osw_dict['steps'].append(measure.to_osw_dict()) # add measure to workflow
762
+ for m_path in measure_paths:
763
+ osw_dict['measure_paths'].append(m_path)
764
+ # if there were reporting measures, add the ladybug adapter to get sim progress
765
+ adapter = folders.honeybee_adapter_path
766
+ if len(m_dict['ReportingMeasure']) != 0 and adapter is not None:
767
+ if 'run_options' not in osw_dict:
768
+ osw_dict['run_options'] = {}
769
+ osw_dict['run_options']['output_adapter'] = {
770
+ 'custom_file_name': adapter,
771
+ 'class_name': 'HoneybeeAdapter',
772
+ 'options': {}
773
+ }
774
+
775
+ # assign the epw_file to the osw if it is input
776
+ if epw_file is not None:
777
+ osw_dict['weather_file'] = epw_file
778
+
779
+ # write the dictionary to a workflow.osw
780
+ osw_json = os.path.join(osw_directory, 'workflow.osw')
781
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
782
+ with open(osw_json, writemode) as fp:
783
+ workflow_str = json.dumps(osw_dict, indent=4, ensure_ascii=False)
784
+ fp.write(workflow_str.encode('utf-8'))
785
+ else:
786
+ with open(osw_json, writemode, encoding='utf-8') as fp:
787
+ workflow_str = json.dump(osw_dict, fp, indent=4, ensure_ascii=False)
788
+
789
+ return os.path.abspath(osw_json)
790
+
791
+
792
+ def to_gbxml_osw(model_path, output_path=None, osw_directory=None):
793
+ """Create a .osw to translate HBJSON to a gbXML file.
794
+
795
+ Args:
796
+ model_path: File path to Honeybee Model (HBJSON).
797
+ output_path: File path to where the gbXML will be written. If None, it
798
+ will be output right next to the input file and given the same name.
799
+ osw_directory: The directory into which the .osw should be written. If None,
800
+ it will be written into the a temp folder in the default simulation folder.
801
+ """
802
+ # create the dictionary with the OSW steps
803
+ osw_dict = {'steps': []}
804
+ model_measure_dict = {
805
+ 'arguments': {
806
+ 'model_json': model_path
807
+ },
808
+ 'measure_dir_name': 'from_honeybee_model_to_gbxml'
809
+ }
810
+ if output_path is not None:
811
+ model_measure_dict['arguments']['output_file_path'] = output_path
812
+ osw_dict['steps'].append(model_measure_dict)
813
+
814
+ # add measure paths
815
+ osw_dict['measure_paths'] = []
816
+ if folders.honeybee_openstudio_gem_path: # include honeybee-openstudio measure path
817
+ measure_dir = os.path.join(folders.honeybee_openstudio_gem_path, 'measures')
818
+ osw_dict['measure_paths'].append(measure_dir)
819
+
820
+ # write the dictionary to a workflow.osw
821
+ if osw_directory is None:
822
+ osw_directory = os.path.join(
823
+ hb_folders.default_simulation_folder, 'temp_translate')
824
+ if not os.path.isdir(osw_directory):
825
+ os.mkdir(osw_directory)
826
+ osw_json = os.path.join(osw_directory, 'translate_honeybee.osw')
827
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
828
+ with open(osw_json, writemode) as fp:
829
+ workflow_str = json.dumps(osw_dict, indent=4, ensure_ascii=False)
830
+ fp.write(workflow_str.encode('utf-8'))
831
+ else:
832
+ with open(osw_json, writemode, encoding='utf-8') as fp:
833
+ workflow_str = json.dump(osw_dict, fp, indent=4, ensure_ascii=False)
834
+
835
+ return os.path.abspath(osw_json)
836
+
837
+
838
+ def to_sdd_osw(model_path, output_path=None, osw_directory=None):
839
+ """Create a .osw to translate HBJSON to a SDD file.
840
+
841
+ Args:
842
+ model_path: File path to Honeybee Model (HBJSON).
843
+ output_path: File path to where the SDD will be written. If None, it
844
+ will be output right next to the input file and given the same name.
845
+ osw_directory: The directory into which the .osw should be written. If None,
846
+ it will be written into the a temp folder in the default simulation folder.
847
+ """
848
+ # create the dictionary with the OSW steps
849
+ osw_dict = {'steps': []}
850
+ model_measure_dict = {
851
+ 'arguments': {
852
+ 'model_json': model_path
853
+ },
854
+ 'measure_dir_name': 'from_honeybee_model_to_sdd'
855
+ }
856
+ if output_path is not None:
857
+ model_measure_dict['arguments']['output_file_path'] = output_path
858
+ osw_dict['steps'].append(model_measure_dict)
859
+
860
+ # add measure paths
861
+ osw_dict['measure_paths'] = []
862
+ if folders.honeybee_openstudio_gem_path: # include honeybee-openstudio measure path
863
+ measure_dir = os.path.join(folders.honeybee_openstudio_gem_path, 'measures')
864
+ osw_dict['measure_paths'].append(measure_dir)
865
+
866
+ # write the dictionary to a workflow.osw
867
+ if osw_directory is None:
868
+ osw_directory = os.path.join(
869
+ hb_folders.default_simulation_folder, 'temp_translate')
870
+ if not os.path.isdir(osw_directory):
871
+ os.mkdir(osw_directory)
872
+ osw_json = os.path.join(osw_directory, 'translate_honeybee.osw')
873
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
874
+ with open(osw_json, writemode) as fp:
875
+ workflow_str = json.dumps(osw_dict, indent=4, ensure_ascii=False)
876
+ fp.write(workflow_str.encode('utf-8'))
877
+ else:
878
+ with open(osw_json, writemode, encoding='utf-8') as fp:
879
+ workflow_str = json.dump(osw_dict, fp, indent=4, ensure_ascii=False)
880
+
881
+ return os.path.abspath(osw_json)
882
+
883
+
884
+ def to_empty_osm_osw(osw_directory, sim_par_json_path, epw_file=None):
885
+ """Create a .osw to produce an empty .osm file with no building geometry.
886
+
887
+ This is useful for creating OpenStudio models to which a detailed Ironbug
888
+ system will be added. Such models with only Ironbug components can simulate
889
+ in EnergyPlus if they use the LoadProfile:Plant object to represent the
890
+ building loads. They are useful for evaluating the performance of such heating
891
+ and cooling plants and, by setting the simulation parameters and EPW file
892
+ with the inputs to this method, any sizing criteria for the plant components
893
+ can be set.
894
+
895
+ Args:
896
+ osw_directory: The directory into which the .osw should be written and the
897
+ .osm will eventually be written into.
898
+ sim_par_json_path: File path to a SimulationParameter JSON.
899
+ epw_file: Optional file path to an EPW that should be associated with the
900
+ output energy model. If None, no EPW file will be written into the
901
+ OSW. (Default: None).
902
+
903
+ Returns:
904
+ The file path to the .osw written out by this method.
905
+ """
906
+ # create the OSW dictionary with the simulation parameter step written
907
+ sim_par_dict = {
908
+ 'arguments': {
909
+ 'simulation_parameter_json': sim_par_json_path
910
+ },
911
+ 'measure_dir_name': 'from_honeybee_simulation_parameter'
912
+ }
913
+ osw_dict = {'steps': [sim_par_dict]}
914
+
915
+ # assign the measure_paths to the osw_dict
916
+ osw_dict['measure_paths'] = []
917
+ if folders.honeybee_openstudio_gem_path: # include honeybee-openstudio measure path
918
+ measure_dir = os.path.join(folders.honeybee_openstudio_gem_path, 'measures')
919
+ osw_dict['measure_paths'].append(measure_dir)
920
+
921
+ # assign the epw_file to the osw if it is input
922
+ if epw_file is not None:
923
+ osw_dict['weather_file'] = epw_file
924
+
925
+ # write the dictionary to a workflow.osw
926
+ osw_json = os.path.join(osw_directory, 'workflow.osw')
927
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
928
+ with open(osw_json, writemode) as fp:
929
+ workflow_str = json.dumps(osw_dict, indent=4, ensure_ascii=False)
930
+ fp.write(workflow_str.encode('utf-8'))
931
+ else:
932
+ with open(osw_json, writemode, encoding='utf-8') as fp:
933
+ workflow_str = json.dump(osw_dict, fp, indent=4, ensure_ascii=False)
934
+
935
+ return os.path.abspath(osw_json)
936
+
937
+
938
+ def run_osw(osw_json, measures_only=True, silent=False):
939
+ """Run a .osw file using the OpenStudio CLI on any operating system.
940
+
941
+ Args:
942
+ osw_json: File path to a OSW file to be run using OpenStudio CLI.
943
+ measures_only: Boolean to note whether only the measures should be
944
+ applied in the running of the OSW (True) or the resulting model
945
+ should be run through EnergyPlus after the measures are applied
946
+ to it (False). (Default: True).
947
+ silent: Boolean to note whether the OSW should be run silently.
948
+ This only has an effect on Windows simulations since Unix-based
949
+ simulations always use shell and are always silent (Default: False).
950
+
951
+ Returns:
952
+ The following files output from the CLI run
953
+
954
+ - osm -- Path to a .osm file representing the output model.
955
+ Will be None if no file exists.
956
+
957
+ - idf -- Path to a .idf file representing the model.
958
+ Will be None if no file exists.
959
+ """
960
+ # run the simulation
961
+ if os.name == 'nt': # we are on Windows
962
+ directory = _run_osw_windows(osw_json, measures_only, silent)
963
+ else: # we are on Mac, Linux, or some other unix-based system
964
+ directory = _run_osw_unix(osw_json, measures_only)
965
+
966
+ # output the simulation files
967
+ return _output_openstudio_files(directory)
968
+
969
+
970
+ def prepare_idf_for_simulation(idf_file_path, epw_file_path=None):
971
+ """Prepare an IDF file to be run through EnergyPlus.
972
+
973
+ This includes checking that the EPW file and IDF file exist and renaming the
974
+ IDF to in.idf (if it is not already named so). A check is also performed to
975
+ be sure that a valid EnergyPlus installation was found.
976
+
977
+ Args:
978
+ idf_file_path: The full path to an IDF file.
979
+ epw_file_path: The full path to an EPW file. Note that inputting None here
980
+ is only appropriate when the simulation is just for design days and has
981
+ no weather file run period. (Default: None).
982
+
983
+ Returns:
984
+ directory -- The folder in which the IDF exists and out of which the EnergyPlus
985
+ simulation will be run.
986
+ """
987
+ # check the energyplus directory
988
+ if not folders.energyplus_path:
989
+ raise OSError('No EnergyPlus installation was found on this machine.\n'
990
+ 'Install EnergyPlus to run energy simulations.')
991
+
992
+ # check the input files
993
+ assert os.path.isfile(idf_file_path), \
994
+ 'No IDF file found at {}.'.format(idf_file_path)
995
+ if epw_file_path is not None:
996
+ assert os.path.isfile(epw_file_path), \
997
+ 'No EPW file found at {}.'.format(epw_file_path)
998
+
999
+ # rename the idf file to in.idf if it isn't named that already
1000
+ directory = os.path.split(idf_file_path)[0]
1001
+ idf_file_name = os.path.split(idf_file_path)[-1]
1002
+ if idf_file_name != 'in.idf':
1003
+ old_file_name = os.path.join(directory, idf_file_name)
1004
+ new_file_name = os.path.join(directory, 'in.idf')
1005
+ # ensure that there isn't an in.idf file there already
1006
+ if os.path.isfile(new_file_name):
1007
+ os.remove(new_file_name)
1008
+ os.rename(old_file_name, new_file_name)
1009
+
1010
+ return directory
1011
+
1012
+
1013
+ def run_idf(idf_file_path, epw_file_path=None, expand_objects=True, silent=False):
1014
+ """Run an IDF file through energyplus on any operating system.
1015
+
1016
+ Args:
1017
+ idf_file_path: The full path to an IDF file.
1018
+ epw_file_path: The full path to an EPW file. Note that inputting None here
1019
+ is only appropriate when the simulation is just for design days and has
1020
+ no weather file run period. (Default: None).
1021
+ expand_objects: If True, the IDF run will include the expansion of any
1022
+ HVAC Template objects in the file before beginning the simulation.
1023
+ This is a necessary step whenever there are HVAC Template objects in
1024
+ the IDF but it is unnecessary extra time when they are not
1025
+ present. (Default: True).
1026
+ silent: Boolean to note whether the simulation should be run silently.
1027
+ This only has an effect on Windows simulations since Unix-based
1028
+ simulations always use shell and are always silent (Default: False).
1029
+
1030
+ Returns:
1031
+ A series of file paths to the simulation output files
1032
+
1033
+ - sql -- Path to a .sqlite file containing all simulation results.
1034
+ Will be None if no file exists.
1035
+
1036
+ - zsz -- Path to a .csv file containing detailed zone load information
1037
+ recorded over the course of the design days. Will be None if no
1038
+ file exists.
1039
+
1040
+ - rdd -- Path to a .rdd file containing all possible outputs that can be
1041
+ requested from the simulation. Will be None if no file exists.
1042
+
1043
+ - html -- Path to a .html file containing all summary reports.
1044
+ Will be None if no file exists.
1045
+
1046
+ - err -- Path to a .err file containing all errors and warnings from the
1047
+ simulation. Will be None if no file exists.
1048
+ """
1049
+ # rename the stat file to ensure EnergyPlus does not find it and error
1050
+ stat_file, renamed_stat = None, None
1051
+ if epw_file_path is not None:
1052
+ epw_folder = os.path.dirname(epw_file_path)
1053
+ for wf in os.listdir(epw_folder):
1054
+ if wf.endswith('.stat'):
1055
+ stat_file = os.path.join(epw_folder, wf)
1056
+ renamed_stat = os.path.join(epw_folder, wf.replace('.stat', '.hide'))
1057
+ try:
1058
+ os.rename(stat_file, renamed_stat)
1059
+ except Exception: # STAT file in restricted location (Program Files)
1060
+ stat_file = None # hope that it is not a OneBuilding EPW
1061
+ break
1062
+
1063
+ # run the simulation
1064
+ if os.name == 'nt': # we are on Windows
1065
+ directory = _run_idf_windows(
1066
+ idf_file_path, epw_file_path, expand_objects, silent)
1067
+ else: # we are on Mac, Linux, or some other unix-based system
1068
+ directory = _run_idf_unix(idf_file_path, epw_file_path, expand_objects)
1069
+
1070
+ # put back the .stat file
1071
+ if stat_file is not None:
1072
+ os.rename(renamed_stat, stat_file)
1073
+
1074
+ # output the simulation files
1075
+ return output_energyplus_files(directory)
1076
+
1077
+
1078
+ def output_energyplus_files(directory):
1079
+ """Get the paths to the EnergyPlus simulation output files given the idf directory.
1080
+
1081
+ Args:
1082
+ directory: The path to where the IDF was run.
1083
+
1084
+ Returns:
1085
+ A tuple with four elements
1086
+
1087
+ - sql -- Path to a .sqlite file containing all simulation results.
1088
+ Will be None if no file exists.
1089
+
1090
+ - zsz -- Path to a .csv file containing detailed zone load information
1091
+ recorded over the course of the design days. Will be None if no
1092
+ file exists.
1093
+
1094
+ - rdd -- Path to a .rdd file containing all possible outputs that can be
1095
+ requested from the simulation. Will be None if no file exists.
1096
+
1097
+ - html -- Path to a .html file containing all summary reports.
1098
+ Will be None if no file exists.
1099
+
1100
+ - err -- Path to a .err file containing all errors and warnings from the
1101
+ simulation. Will be None if no file exists.
1102
+ """
1103
+ # generate paths to the simulation files
1104
+ sql_file = os.path.join(directory, 'eplusout.sql')
1105
+ zsz_file = os.path.join(directory, 'epluszsz.csv')
1106
+ rdd_file = os.path.join(directory, 'eplusout.rdd')
1107
+ html_file = os.path.join(directory, 'eplustbl.htm')
1108
+ err_file = os.path.join(directory, 'eplusout.err')
1109
+
1110
+ # check that the simulation files exist
1111
+ sql = sql_file if os.path.isfile(sql_file) else None
1112
+ zsz = zsz_file if os.path.isfile(zsz_file) else None
1113
+ rdd = rdd_file if os.path.isfile(rdd_file) else None
1114
+ html = html_file if os.path.isfile(html_file) else None
1115
+ err = err_file if os.path.isfile(err_file) else None
1116
+
1117
+ return sql, zsz, rdd, html, err
1118
+
1119
+
1120
+ def _import_model_osw(model_path, extension, output_path=None, osw_directory=None):
1121
+ """Base function used for OSW translating from various formats to HBJSON.
1122
+
1123
+ Args:
1124
+ model_path: File path to the file to be translated to HBJSON.
1125
+ extension: Name of the file type to be translated (eg. gbxml).
1126
+ output_path: File path to where the Model HBJSON will be written.
1127
+ osw_directory: The directory into which the .osw should be written.
1128
+ """
1129
+ # create the dictionary with the OSW steps
1130
+ osw_dict = {'steps': []}
1131
+ model_measure_dict = {
1132
+ 'arguments': {
1133
+ '{}_model'.format(extension): model_path
1134
+ },
1135
+ 'measure_dir_name': 'from_{}_model'.format(extension)
1136
+ }
1137
+ if output_path is not None:
1138
+ model_measure_dict['arguments']['output_file_path'] = output_path
1139
+ osw_dict['steps'].append(model_measure_dict)
1140
+
1141
+ # add measure paths
1142
+ osw_dict['measure_paths'] = []
1143
+ if folders.honeybee_openstudio_gem_path: # include honeybee-openstudio measure path
1144
+ measure_dir = os.path.join(folders.honeybee_openstudio_gem_path, 'measures')
1145
+ osw_dict['measure_paths'].append(measure_dir)
1146
+
1147
+ # write the dictionary to a workflow.osw
1148
+ if osw_directory is None:
1149
+ osw_directory = os.path.join(
1150
+ hb_folders.default_simulation_folder, 'temp_translate')
1151
+ if not os.path.isdir(osw_directory):
1152
+ os.mkdir(osw_directory)
1153
+ osw_json = os.path.join(osw_directory, 'translate_{}.osw'.format(extension))
1154
+ if (sys.version_info < (3, 0)): # we need to manually encode it as UTF-8
1155
+ with open(osw_json, writemode) as fp:
1156
+ workflow_str = json.dumps(osw_dict, indent=4, ensure_ascii=False)
1157
+ fp.write(workflow_str.encode('utf-8'))
1158
+ else:
1159
+ with open(osw_json, writemode, encoding='utf-8') as fp:
1160
+ workflow_str = json.dump(osw_dict, fp, indent=4, ensure_ascii=False)
1161
+
1162
+ return os.path.abspath(osw_json)
1163
+
1164
+
1165
+ def _check_osw(osw_json):
1166
+ """Prepare an OSW file to be run through OpenStudio CLI.
1167
+
1168
+ This includes checking for a valid OpenStudio installation and ensuring the
1169
+ OSW file exists.
1170
+
1171
+ Args:
1172
+ osw_json: The full path to an OSW file.
1173
+ epw_file_path: The full path to an EPW file.
1174
+
1175
+ Returns:
1176
+ The folder in which the OSW exists and out of which the OpenStudio CLI
1177
+ will operate.
1178
+ """
1179
+ # check the openstudio directory
1180
+ if not folders.openstudio_path:
1181
+ raise OSError('No OpenStudio installation was found on this machine.\n'
1182
+ 'Install OpenStudio to run energy simulations.')
1183
+
1184
+ # check the input files
1185
+ assert os.path.isfile(osw_json), 'No OSW file found at {}.'.format(osw_json)
1186
+ return os.path.split(osw_json)[0]
1187
+
1188
+
1189
+ def _run_osw_windows(osw_json, measures_only=True, silent=False):
1190
+ """Run a .osw file using the OpenStudio CLI on a Windows-based operating system.
1191
+
1192
+ A batch file will be used to run the simulation.
1193
+
1194
+ Args:
1195
+ osw_json: File path to a OSW file to be run using OpenStudio CLI.
1196
+ measures_only: Boolean to note whether only the measures should be applied
1197
+ in the running of the OSW (True) or the resulting model should be run
1198
+ through EnergyPlus after the measures are applied to it (False).
1199
+ Default: True.
1200
+ silent: Boolean to note whether the OSW should be run silently (without
1201
+ the batch window). If so, the simulation will be run using subprocess
1202
+ with shell set to True. (Default: False).
1203
+
1204
+ Returns:
1205
+ Path to the folder out of which the OSW was run.
1206
+ """
1207
+ # check the input file
1208
+ directory = _check_osw(osw_json)
1209
+
1210
+ if not silent:
1211
+ # write a batch file to call OpenStudio CLI; useful for re-running the sim
1212
+ working_drive = directory[:2]
1213
+ measure_str = '-m ' if measures_only else ''
1214
+ batch = '{}\n"{}" -I "{}" run --show-stdout {}-w "{}"'.format(
1215
+ working_drive, folders.openstudio_exe, folders.honeybee_openstudio_gem_path,
1216
+ measure_str, osw_json)
1217
+ if all(ord(c) < 128 for c in batch): # just run the batch file as it is
1218
+ batch_file = os.path.join(directory, 'run_workflow.bat')
1219
+ write_to_file(batch_file, batch, True)
1220
+ os.system('"{}"'.format(batch_file)) # run the batch file
1221
+ return directory
1222
+ # given .bat file restrictions with non-ASCII characters, run the sim with subprocess
1223
+ cmds = [folders.openstudio_exe, '-I', folders.honeybee_openstudio_gem_path,
1224
+ 'run', '--show-stdout', '-w', osw_json]
1225
+ if measures_only:
1226
+ cmds.append('-m')
1227
+ process = subprocess.Popen(cmds, shell=silent)
1228
+ process.communicate() # prevents the script from running before command is done
1229
+
1230
+ return directory
1231
+
1232
+
1233
+ def _run_osw_unix(osw_json, measures_only=True):
1234
+ """Run a .osw file using the OpenStudio CLI on a Unix-based operating system.
1235
+
1236
+ This includes both Mac OS and Linux since a shell will be used to run
1237
+ the simulation.
1238
+
1239
+ Args:
1240
+ osw_json: File path to a OSW file to be run using OpenStudio CLI.
1241
+ measures_only: Boolean to note whether only the measures should be applied
1242
+ in the running of the OSW (True) or the resulting model should be run
1243
+ through EnergyPlus after the measures are applied to it (False).
1244
+ Default: True.
1245
+
1246
+ Returns:
1247
+ Path to the folder out of which the OSW was run.
1248
+ """
1249
+ # check the input file
1250
+ directory = _check_osw(osw_json)
1251
+
1252
+ # Write the shell script to call OpenStudio CLI
1253
+ measure_str = '-m ' if measures_only else ''
1254
+ shell = '#!/usr/bin/env bash\n"{}" -I "{}" run --show-stdout {}-w "{}"'.format(
1255
+ folders.openstudio_exe, folders.honeybee_openstudio_gem_path,
1256
+ measure_str, osw_json)
1257
+ shell_file = os.path.join(directory, 'run_workflow.sh')
1258
+ write_to_file(shell_file, shell, True)
1259
+
1260
+ # make the shell script executable using subprocess.check_call
1261
+ # this is more reliable than native Python chmod on Mac
1262
+ subprocess.check_call(['chmod', 'u+x', shell_file])
1263
+
1264
+ # run the shell script
1265
+ subprocess.call(shell_file)
1266
+
1267
+ return directory
1268
+
1269
+
1270
+ def _output_openstudio_files(directory):
1271
+ """Get the paths to the OpenStudio simulation output files given the osw directory.
1272
+
1273
+ Args:
1274
+ directory: The path to where the OSW was run.
1275
+
1276
+ Returns:
1277
+
1278
+ - osm -- Path to a .osm file containing all simulation results.
1279
+ Will be None if no file exists.
1280
+
1281
+ - idf -- Path to a .idf file containing properties of the model, including
1282
+ the size of HVAC objects. Will be None if no file exists.
1283
+ """
1284
+ # generate and check paths to the OSM and IDF files
1285
+ osm_file = os.path.join(directory, 'run', 'in.osm')
1286
+ osm = osm_file if os.path.isfile(osm_file) else None
1287
+
1288
+ # check the pre-process idf and replace the other in.idf with it
1289
+ idf_file_right = os.path.join(directory, 'run', 'pre-preprocess.idf')
1290
+ idf_file_wrong = os.path.join(directory, 'run', 'in.idf')
1291
+ if os.path.isfile(idf_file_right) and os.path.isfile(idf_file_wrong):
1292
+ os.remove(idf_file_wrong)
1293
+ os.rename(idf_file_right, idf_file_wrong)
1294
+ idf = idf_file_wrong
1295
+ else:
1296
+ idf = None
1297
+
1298
+ return osm, idf
1299
+
1300
+
1301
+ def _run_idf_windows(idf_file_path, epw_file_path=None, expand_objects=True,
1302
+ silent=False):
1303
+ """Run an IDF file through energyplus on a Windows-based operating system.
1304
+
1305
+ A batch file will be used to run the simulation.
1306
+
1307
+ Args:
1308
+ idf_file_path: The full path to an IDF file.
1309
+ epw_file_path: The full path to an EPW file. Note that inputting None here
1310
+ is only appropriate when the simulation is just for design days and has
1311
+ no weather file run period. (Default: None).
1312
+ expand_objects: If True, the IDF run will include the expansion of any
1313
+ HVAC Template objects in the file before beginning the simulation.
1314
+ This is a necessary step whenever there are HVAC Template objects in
1315
+ the IDF but it is unnecessary extra time when they are not
1316
+ present. (Default: True).
1317
+ silent: Boolean to note whether the simulation should be run silently
1318
+ (without the batch window). If so, the simulation will be run using
1319
+ subprocess with shell set to True. (Default: False).
1320
+
1321
+ Returns:
1322
+ Path to the folder out of which the simulation was run.
1323
+ """
1324
+ # check and prepare the input files
1325
+ directory = prepare_idf_for_simulation(idf_file_path, epw_file_path)
1326
+
1327
+ if not silent: # write a batch file; useful for re-running the sim
1328
+ # generate various arguments to pass to the energyplus command
1329
+ epw_str = '-w "{}"'.format(os.path.abspath(epw_file_path)) \
1330
+ if epw_file_path is not None else ''
1331
+ idd_str = '-i "{}"'.format(folders.energyplus_idd_path)
1332
+ expand_str = ' -x' if expand_objects else ''
1333
+ working_drive = directory[:2]
1334
+ # write the batch file
1335
+ batch = '{}\ncd "{}"\n"{}" {} {}{}'.format(
1336
+ working_drive, directory, folders.energyplus_exe, epw_str,
1337
+ idd_str, expand_str)
1338
+ if all(ord(c) < 128 for c in batch): # just run the batch file as it is
1339
+ batch_file = os.path.join(directory, 'in.bat')
1340
+ write_to_file(batch_file, batch, True)
1341
+ os.system('"{}"'.format(batch_file)) # run the batch file
1342
+ return directory
1343
+ # given .bat file restrictions with non-ASCII characters, run the sim with subprocess
1344
+ cmds = [folders.energyplus_exe, '-i', folders.energyplus_idd_path]
1345
+ if epw_file_path is not None:
1346
+ cmds.append('-w')
1347
+ cmds.append(os.path.abspath(epw_file_path))
1348
+ if expand_objects:
1349
+ cmds.append('-x')
1350
+ process = subprocess.Popen(cmds, cwd=directory, shell=silent)
1351
+ process.communicate() # prevents the script from running before command is done
1352
+
1353
+ return directory
1354
+
1355
+
1356
+ def _run_idf_unix(idf_file_path, epw_file_path=None, expand_objects=True):
1357
+ """Run an IDF file through energyplus on a Unix-based operating system.
1358
+
1359
+ This includes both Mac OS and Linux since a shell will be used to run
1360
+ the simulation.
1361
+
1362
+ Args:
1363
+ idf_file_path: The full path to an IDF file.
1364
+ epw_file_path: The full path to an EPW file. Note that inputting None here
1365
+ is only appropriate when the simulation is just for design days and has
1366
+ no weather file run period. (Default: None).
1367
+ expand_objects: If True, the IDF run will include the expansion of any
1368
+ HVAC Template objects in the file before beginning the simulation.
1369
+ This is a necessary step whenever there are HVAC Template objects in
1370
+ the IDF but it is unnecessary extra time when they are not present.
1371
+ Default: True.
1372
+
1373
+ Returns:
1374
+ Path to the folder out of which the simulation was run.
1375
+ """
1376
+ # check and prepare the input files
1377
+ directory = prepare_idf_for_simulation(idf_file_path, epw_file_path)
1378
+
1379
+ # generate various arguments to pass to the energyplus command
1380
+ epw_str = '-w "{}"'.format(os.path.abspath(epw_file_path))\
1381
+ if epw_file_path is not None else ''
1382
+ idd_str = '-i "{}"'.format(folders.energyplus_idd_path)
1383
+ expand_str = ' -x' if expand_objects else ''
1384
+
1385
+ # write a shell file
1386
+ shell = '#!/usr/bin/env bash\n\ncd "{}"\n"{}" {} {}{}'.format(
1387
+ directory, folders.energyplus_exe, epw_str, idd_str, expand_str)
1388
+ shell_file = os.path.join(directory, 'in.sh')
1389
+ write_to_file(shell_file, shell, True)
1390
+
1391
+ # make the shell script executable using subprocess.check_call
1392
+ # this is more reliable than native Python chmod on Mac
1393
+ subprocess.check_call(['chmod', 'u+x', shell_file])
1394
+
1395
+ # run the shell script
1396
+ subprocess.call(shell_file)
1397
+
1398
+ return directory
1399
+
1400
+
1401
+ def set_gbxml_floor_types(
1402
+ base_gbxml, interior_type=None, ground_type=None, new_gbxml=None):
1403
+ """Set certain Floor Faces of a base_gbxml to a single type.
1404
+
1405
+ This method helps account for the fact that the gbXML schema has several
1406
+ different types of floors that mean effectively the same thing in energy
1407
+ simulation but not all destination software interfaces are equipped to
1408
+ treat them as such.
1409
+
1410
+ Args:
1411
+ base_gbxml: A file path to a gbXML file that has been exported from the
1412
+ OpenStudio Forward Translator.
1413
+ interior_type: Text for the type to be used for all interior floor faces. If
1414
+ None, the interior types will be left as they are. (Default: None).
1415
+ Choose from the following.
1416
+
1417
+ * InteriorFloor
1418
+ * Ceiling
1419
+
1420
+ ground_type: Text for the type to be used for all ground-contact floor faces.
1421
+ If None, the ground floor types will be left as they are. (Default: None).
1422
+ Choose from the following.
1423
+
1424
+ * UndergroundSlab
1425
+ * SlabOnGrade
1426
+ * RaisedFloor
1427
+
1428
+ new_gbxml: Optional path to where the new gbXML will be written. If None,
1429
+ the original base_gbxml will be overwritten with a version that has
1430
+ the floor types overridden. (Default: None).
1431
+ """
1432
+ # read the file content
1433
+ with open(base_gbxml, 'r') as bxf:
1434
+ content = bxf.read()
1435
+
1436
+ # replace all interior floors with the specified type
1437
+ if interior_type == 'InteriorFloor':
1438
+ content = content.replace('="Ceiling"', '="InteriorFloor"')
1439
+ elif interior_type == 'Ceiling':
1440
+ content = content.replace('="InteriorFloor"', '="Ceiling"')
1441
+
1442
+ # replace all ground floors with the specified type
1443
+ if ground_type == 'UndergroundSlab':
1444
+ content = content.replace('="SlabOnGrade"', '="UndergroundSlab"')
1445
+ content = content.replace('="RaisedFloor"', '="UndergroundSlab"')
1446
+ elif ground_type == 'SlabOnGrade':
1447
+ content = content.replace('="UndergroundSlab"', '="SlabOnGrade"')
1448
+ content = content.replace('="RaisedFloor"', '="SlabOnGrade"')
1449
+ elif ground_type == 'RaisedFloor':
1450
+ content = content.replace('="UndergroundSlab"', '="RaisedFloor"')
1451
+ content = content.replace('="SlabOnGrade"', '="RaisedFloor"')
1452
+
1453
+ # write out the new XML
1454
+ new_xml = base_gbxml if new_gbxml is None else new_gbxml
1455
+ with open(new_xml, 'w') as nxf:
1456
+ nxf.write(content)
1457
+ return new_xml
1458
+
1459
+
1460
+ def add_gbxml_space_boundaries(base_gbxml, honeybee_model, new_gbxml=None):
1461
+ """Add the SpaceBoundary and ShellGeometry to a base_gbxml of a Honeybee model.
1462
+
1463
+ Note that these space boundary geometry definitions are optional within gbXML and
1464
+ are essentially a duplication of the required non-manifold geometry within a
1465
+ valid gbXML. The OpenStudio Forward Translator does not include such duplicated
1466
+ geometries (hence, the reason for this method). However, these closed-volume
1467
+ boundary geometries are used by certain interfaces and gbXML viewers.
1468
+
1469
+ Args:
1470
+ base_gbxml: A file path to a gbXML file that has been exported from the
1471
+ OpenStudio Forward Translator.
1472
+ honeybee_model: The honeybee Model object that was used to create the
1473
+ exported base_gbxml.
1474
+ new_gbxml: Optional path to where the new gbXML will be written. If None,
1475
+ the original base_gbxml will be overwritten with a version that has
1476
+ the SpaceBoundary included within it.
1477
+ """
1478
+ # get a dictionary of rooms in the model
1479
+ room_dict = {room.identifier: room for room in honeybee_model.rooms}
1480
+
1481
+ # register all of the namespaces within the OpenStudio-exported XML
1482
+ ET.register_namespace('', 'http://www.gbxml.org/schema')
1483
+ ET.register_namespace('xhtml', 'http://www.w3.org/1999/xhtml')
1484
+ ET.register_namespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
1485
+ ET.register_namespace('xsd', 'http://www.w3.org/2001/XMLSchema')
1486
+
1487
+ # parse the XML and get the building definition
1488
+ tree = ET.parse(base_gbxml)
1489
+ root = tree.getroot()
1490
+ gbxml_header = r'{http://www.gbxml.org/schema}'
1491
+ building = root[0][1]
1492
+
1493
+ # loop through surfaces in the gbXML so that we know the name of the interior ones
1494
+ surface_set = set()
1495
+ for room_element in root[0].findall(gbxml_header + 'Surface'):
1496
+ surface_set.add(room_element.get('id'))
1497
+
1498
+ # loop through the rooms in the XML and add them as space boundaries to the room
1499
+ for room_element in building.findall(gbxml_header + 'Space'):
1500
+ room_id = room_element.get('zoneIdRef')
1501
+ if room_id:
1502
+ room_id = room_element.get('id')
1503
+ shell_element = ET.Element('ShellGeometry')
1504
+ shell_element.set('id', '{}Shell'.format(room_id))
1505
+ shell_geo_element = ET.SubElement(shell_element, 'ClosedShell')
1506
+ hb_room = room_dict[room_id[:-6]] # remove '_Space' from the end
1507
+ for face in hb_room:
1508
+ face_xml, face_geo_xml = _face_to_gbxml_geo(face, surface_set)
1509
+ if face_xml is not None:
1510
+ room_element.append(face_xml)
1511
+ shell_geo_element.append(face_geo_xml)
1512
+ room_element.append(shell_element)
1513
+
1514
+ # write out the new XML
1515
+ new_xml = base_gbxml if new_gbxml is None else new_gbxml
1516
+ tree.write(new_xml, xml_declaration=True)
1517
+ return new_xml
1518
+
1519
+
1520
+ def _face_to_gbxml_geo(face, face_set):
1521
+ """Get an Element Tree of a gbXML SpaceBoundary for a Face.
1522
+
1523
+ Note that the resulting string is only meant to go under the "Space" tag and
1524
+ it is not a Surface tag with all of the construction and boundary condition
1525
+ properties assigned to it.
1526
+
1527
+ Args:
1528
+ face: A honeybee Face for which an gbXML representation will be returned.
1529
+ face_set: A set of surface identifiers in the model, used to evaluate whether
1530
+ the geometry must be associated with its boundary condition surface.
1531
+
1532
+ Returns:
1533
+ A tuple with two elements.
1534
+
1535
+ - face_element: The element tree for the SpaceBoundary definition of the Face.
1536
+
1537
+ - loop_element: The element tree for the PolyLoop definition of the Face,
1538
+ which is useful in defining the shell.
1539
+ """
1540
+ # create the face element and associate it with a surface in the model
1541
+ face_element = ET.Element('SpaceBoundary')
1542
+ face_element.set('isSecondLevelBoundary', 'false')
1543
+ obj_id = None
1544
+ if face.identifier in face_set:
1545
+ obj_id = face.identifier
1546
+ elif isinstance(face.boundary_condition, Surface):
1547
+ bc_obj = face.boundary_condition.boundary_condition_object
1548
+ if bc_obj in face_set:
1549
+ obj_id = bc_obj
1550
+ if obj_id is None:
1551
+ return None, None
1552
+ face_element.set('surfaceIdRef', obj_id)
1553
+
1554
+ # write the geometry of the face
1555
+ geo_element = ET.SubElement(face_element, 'PlanarGeometry')
1556
+ loop_element = ET.SubElement(geo_element, 'PolyLoop')
1557
+ for pt in face.vertices:
1558
+ pt_element = ET.SubElement(loop_element, 'CartesianPoint')
1559
+ for coord in pt:
1560
+ coord_element = ET.SubElement(pt_element, 'Coordinate')
1561
+ coord_element.text = str(coord)
1562
+ return face_element, loop_element
1563
+
1564
+
1565
+ def _parse_os_cli_failure(directory):
1566
+ """Parse the failure log of OpenStudio CLI.
1567
+
1568
+ Args:
1569
+ directory: Path to the directory out of which the simulation is run.
1570
+ """
1571
+ log_osw = OSW(os.path.join(directory, 'out.osw'))
1572
+ raise Exception(
1573
+ 'Failed to run OpenStudio CLI:\n{}'.format('\n'.join(log_osw.errors)))