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