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
@@ -0,0 +1,2621 @@
1
+ # coding=utf-8
2
+ """Model Energy Properties."""
3
+ try:
4
+ from itertools import izip as zip # python 2
5
+ except ImportError:
6
+ pass # python 3
7
+
8
+ from ladybug_geometry.geometry2d import Vector2D
9
+ from ladybug_geometry.geometry3d import Point3D
10
+ from honeybee.boundarycondition import Outdoors, Surface, boundary_conditions
11
+ from honeybee.facetype import AirBoundary, face_types
12
+ from honeybee.extensionutil import model_extension_dicts
13
+ from honeybee.checkdup import check_duplicate_identifiers
14
+ from honeybee.units import conversion_factor_to_meters, parse_distance_string
15
+ from honeybee.typing import invalid_dict_error, clean_ep_string, \
16
+ clean_and_id_ep_string, clean_and_number_ep_string
17
+ from honeybee.face import Face
18
+ from honeybee.room import Room
19
+ from honeybee.model import Model
20
+
21
+ from ..material.dictutil import dict_to_material
22
+ from ..construction.dictutil import CONSTRUCTION_TYPES, dict_to_construction, \
23
+ dict_abridged_to_construction
24
+ from ..construction.opaque import OpaqueConstruction
25
+ from ..construction.window import WindowConstruction
26
+ from ..construction.windowshade import WindowConstructionShade
27
+ from ..construction.dynamic import WindowConstructionDynamic
28
+ from ..construction.air import AirBoundaryConstruction
29
+ from ..constructionset import ConstructionSet
30
+ from ..material.opaque import EnergyMaterialVegetation
31
+ from ..schedule.typelimit import ScheduleTypeLimit
32
+ from ..schedule.ruleset import ScheduleRuleset
33
+ from ..schedule.dictutil import SCHEDULE_TYPES, dict_to_schedule, \
34
+ dict_abridged_to_schedule
35
+ from ..programtype import ProgramType
36
+ from ..hvac.detailed import DetailedHVAC
37
+ from ..hvac import HVAC_TYPES_DICT
38
+ from ..shw import SHWSystem
39
+ from ..ventcool.simulation import VentilationSimulationControl
40
+ from ..generator.loadcenter import ElectricLoadCenter
41
+
42
+ from ..config import folders
43
+ from ..lib.constructions import generic_context
44
+ from ..lib.constructionsets import generic_construction_set
45
+ from ..lib.schedules import always_on
46
+ from ..lib.scheduletypelimits import fractional
47
+
48
+
49
+ class ModelEnergyProperties(object):
50
+ """Energy Properties for Honeybee Model.
51
+
52
+ Args:
53
+ host: A honeybee_core Model object that hosts these properties.
54
+ ventilation_simulation_control: A VentilationSimulationControl object that
55
+ defines global parameters for ventilation simulation.
56
+ electric_load_center: A ElectricLoadCenter object that defines the properties
57
+ of the model's electric loads center.
58
+
59
+ Properties:
60
+ * host
61
+ * materials
62
+ * constructions
63
+ * room_constructions
64
+ * face_constructions
65
+ * shade_constructions
66
+ * construction_sets
67
+ * global_construction_set
68
+ * schedule_type_limits
69
+ * schedules
70
+ * shade_schedules
71
+ * room_schedules
72
+ * program_type_schedules
73
+ * hvac_schedules
74
+ * orphaned_trans_schedules
75
+ * program_types
76
+ * hvacs
77
+ * shws
78
+ * ventilation_simulation_control
79
+ * electric_load_center
80
+ """
81
+
82
+ def __init__(
83
+ self, host, ventilation_simulation_control=None, electric_load_center=None):
84
+ """Initialize Model energy properties."""
85
+ self._host = host
86
+ self.ventilation_simulation_control = ventilation_simulation_control
87
+ self.electric_load_center = electric_load_center
88
+
89
+ @property
90
+ def host(self):
91
+ """Get the Model object hosting these properties."""
92
+ return self._host
93
+
94
+ @property
95
+ def materials(self):
96
+ """Get a list of all unique materials contained within the model.
97
+
98
+ This includes materials across all Faces, Apertures, Doors and Room
99
+ ConstructionSets but it does NOT include the Honeybee generic default
100
+ construction set.
101
+ """
102
+ materials = []
103
+ for constr in self.constructions:
104
+ try:
105
+ materials.extend(constr.materials)
106
+ if constr.has_frame:
107
+ materials.append(constr.frame)
108
+ if isinstance(constr, WindowConstructionShade):
109
+ if constr.is_switchable_glazing:
110
+ materials.append(constr.switched_glass_material)
111
+ if constr.shade_location == 'Between':
112
+ materials.append(constr.window_construction.materials[-2])
113
+ except AttributeError:
114
+ pass # ShadeConstruction or AirBoundaryConstruction
115
+ return list(set(materials))
116
+
117
+ @property
118
+ def constructions(self):
119
+ """Get a list of all unique constructions in the model.
120
+
121
+ This includes constructions across all Faces, Apertures, Doors, Shades,
122
+ and Room ConstructionSets but it does NOT include the Honeybee generic
123
+ default construction set.
124
+ """
125
+ all_constrs = self.room_constructions + self.face_constructions + \
126
+ self.shade_constructions
127
+ return list(set(all_constrs))
128
+
129
+ @property
130
+ def room_constructions(self):
131
+ """Get a list of all unique constructions assigned to Room ConstructionSets.
132
+
133
+ This also includes the constructions assigned to Room InternalMasses.
134
+ """
135
+ room_constrs = []
136
+ for cnstr_set in self.construction_sets:
137
+ room_constrs.extend(cnstr_set.modified_constructions_unique)
138
+ for room in self.host.rooms:
139
+ for int_mass in room.properties.energy._internal_masses:
140
+ constr = int_mass.construction
141
+ if not self._instance_in_array(constr, room_constrs):
142
+ room_constrs.append(constr)
143
+ return list(set(room_constrs))
144
+
145
+ @property
146
+ def face_constructions(self):
147
+ """Get a list of all unique constructions assigned to Faces, Apertures and Doors.
148
+ """
149
+ constructions = []
150
+ for face in self.host.faces:
151
+ self._check_and_add_obj_construction(face, constructions)
152
+ for ap in face.apertures:
153
+ self._check_and_add_obj_construction(ap, constructions)
154
+ for dr in face.doors:
155
+ self._check_and_add_obj_construction(dr, constructions)
156
+ for ap in self.host.orphaned_apertures:
157
+ self._check_and_add_obj_construction(ap, constructions)
158
+ for dr in self.host.orphaned_doors:
159
+ self._check_and_add_obj_construction(dr, constructions)
160
+ return list(set(constructions))
161
+
162
+ @property
163
+ def shade_constructions(self):
164
+ """Get a list of all unique constructions assigned to Shades in the model."""
165
+ constructions = []
166
+ for shade in self.host.shades:
167
+ self._check_and_add_obj_construction(shade, constructions)
168
+ for sm in self.host.shade_meshes: # check all ShadeMesh modifiers
169
+ self._check_and_add_obj_construction(sm, constructions)
170
+ return list(set(constructions))
171
+
172
+ @property
173
+ def construction_sets(self):
174
+ """Get a list of all unique Room-Assigned ConstructionSets in the Model."""
175
+ construction_sets = []
176
+ for room in self.host.rooms:
177
+ if room.properties.energy._construction_set is not None:
178
+ if not self._instance_in_array(room.properties.energy._construction_set,
179
+ construction_sets):
180
+ construction_sets.append(room.properties.energy._construction_set)
181
+ return list(set(construction_sets)) # catch equivalent construction sets
182
+
183
+ @property
184
+ def global_construction_set(self):
185
+ """The global energy construction set.
186
+
187
+ This is what is used whenever no construction has been assigned to a given
188
+ Face/Aperture/Door/Shade and there is no construction_set assigned to the
189
+ parent Room.
190
+ """
191
+ return generic_construction_set
192
+
193
+ @property
194
+ def schedule_type_limits(self):
195
+ """Get a list of all unique schedule type limits contained within the model.
196
+
197
+ This includes schedules across all Shades and Rooms.
198
+ """
199
+ type_limits = []
200
+ for sched in self.schedules:
201
+ t_lim = sched.schedule_type_limit
202
+ if t_lim is not None and not self._instance_in_array(t_lim, type_limits):
203
+ type_limits.append(t_lim)
204
+ return list(set(type_limits))
205
+
206
+ @property
207
+ def schedules(self):
208
+ """Get a list of all unique schedules directly assigned to objects in the model.
209
+
210
+ This includes schedules across all ProgramTypes, HVACs, Rooms and Shades.
211
+ However, it does not include any of the orphaned_trans_schedules as these
212
+ are not directly assigned to objects but rather generated from their
213
+ constructions.
214
+ """
215
+ all_scheds = self.program_type_schedules + self.hvac_schedules + \
216
+ self.room_schedules + self.shade_schedules + self.construction_schedules
217
+ return list(set(all_scheds))
218
+
219
+ @property
220
+ def construction_schedules(self):
221
+ """Get a list of all unique schedules assigned to constructions in the model.
222
+
223
+ This includes schedules on al AirBoundaryConstructions.
224
+ """
225
+ schedules = []
226
+ for constr in self.constructions:
227
+ if isinstance(constr, AirBoundaryConstruction):
228
+ self._check_and_add_schedule(constr.air_mixing_schedule, schedules)
229
+ elif isinstance(constr, WindowConstructionShade):
230
+ if constr.schedule is not None:
231
+ self._check_and_add_schedule(constr.schedule, schedules)
232
+ elif isinstance(constr, WindowConstructionDynamic):
233
+ self._check_and_add_schedule(constr.schedule, schedules)
234
+ return list(set(schedules))
235
+
236
+ @property
237
+ def shade_schedules(self):
238
+ """Get a list of unique transmittance schedules assigned to Shades in the model.
239
+ """
240
+ schedules = []
241
+ for shade in self.host.orphaned_shades:
242
+ self._check_and_add_shade_schedule(shade, schedules)
243
+ for room in self.host.rooms: # check all Room Shade schedules
244
+ for shade in room.shades:
245
+ self._check_and_add_shade_schedule(shade, schedules)
246
+ for face in room.faces: # check all Face Shade schedules
247
+ for shade in face.shades:
248
+ self._check_and_add_shade_schedule(shade, schedules)
249
+ for ap in face.apertures: # check all Aperture Shade schedules
250
+ for shade in ap.shades:
251
+ self._check_and_add_shade_schedule(shade, schedules)
252
+ for dr in face.doors: # check all Door Shade schedules
253
+ for shade in dr.shades:
254
+ self._check_and_add_shade_schedule(shade, schedules)
255
+ for sm in self.host.shade_meshes:
256
+ self._check_and_add_shade_schedule(sm, schedules)
257
+ return list(set(schedules))
258
+
259
+ @property
260
+ def room_schedules(self):
261
+ """Get a list of all unique schedules assigned directly to Rooms in the model.
262
+
263
+ Note that this does not include schedules from ProgramTypes assigned to the
264
+ rooms. For this, use the program_type_schedules property.
265
+ """
266
+ scheds = []
267
+ for room in self.host.rooms:
268
+ people = room.properties.energy._people
269
+ lighting = room.properties.energy._lighting
270
+ electric_equipment = room.properties.energy._electric_equipment
271
+ gas_equipment = room.properties.energy._gas_equipment
272
+ shw = room.properties.energy._service_hot_water
273
+ infiltration = room.properties.energy._infiltration
274
+ ventilation = room.properties.energy._ventilation
275
+ setpoint = room.properties.energy._setpoint
276
+ window_vent = room.properties.energy._window_vent_control
277
+ processes = room.properties.energy._process_loads
278
+ fans = room.properties.energy._fans
279
+ if people is not None:
280
+ self._check_and_add_schedule(people.occupancy_schedule, scheds)
281
+ self._check_and_add_schedule(people.activity_schedule, scheds)
282
+ if lighting is not None:
283
+ self._check_and_add_schedule(lighting.schedule, scheds)
284
+ if electric_equipment is not None:
285
+ self._check_and_add_schedule(electric_equipment.schedule, scheds)
286
+ if gas_equipment is not None:
287
+ self._check_and_add_schedule(gas_equipment.schedule, scheds)
288
+ if shw is not None:
289
+ self._check_and_add_schedule(shw.schedule, scheds)
290
+ if infiltration is not None:
291
+ self._check_and_add_schedule(infiltration.schedule, scheds)
292
+ if ventilation is not None and ventilation.schedule is not None:
293
+ self._check_and_add_schedule(ventilation.schedule, scheds)
294
+ if setpoint is not None:
295
+ self._check_and_add_schedule(setpoint.heating_schedule, scheds)
296
+ self._check_and_add_schedule(setpoint.cooling_schedule, scheds)
297
+ if setpoint.humidifying_schedule is not None:
298
+ self._check_and_add_schedule(
299
+ setpoint.humidifying_schedule, scheds)
300
+ self._check_and_add_schedule(
301
+ setpoint.dehumidifying_schedule, scheds)
302
+ if window_vent is not None:
303
+ self._check_and_add_schedule(window_vent.schedule, scheds)
304
+ if len(processes) != 0:
305
+ for process in processes:
306
+ self._check_and_add_schedule(process.schedule, scheds)
307
+ if len(fans) != 0:
308
+ for fan in fans:
309
+ self._check_and_add_schedule(fan.control.schedule, scheds)
310
+ return list(set(scheds))
311
+
312
+ @property
313
+ def program_type_schedules(self):
314
+ """Get a list of all unique schedules assigned to ProgramTypes in the model."""
315
+ schedules = []
316
+ for p_type in self.program_types:
317
+ for sched in p_type.schedules:
318
+ self._check_and_add_schedule(sched, schedules)
319
+ return list(set(schedules))
320
+
321
+ @property
322
+ def hvac_schedules(self):
323
+ """Get a list of all unique HVAC-assigned schedules in the model."""
324
+ schedules = []
325
+ for hvac in self.hvacs:
326
+ for sched in hvac.schedules:
327
+ self._check_and_add_schedule(sched, schedules)
328
+ return list(set(schedules))
329
+
330
+ @property
331
+ def orphaned_trans_schedules(self):
332
+ """Get a list of constant transmittance schedules for transparent orphaned objs.
333
+
334
+ These schedules are not directly assigned to any honeybee objects but
335
+ they are automatically generated from the constructions of orphaned objects.
336
+ They are intended to be assigned to shade representations of the orphaned
337
+ objects in the simulation in order to account for their transparency.
338
+ """
339
+ # collect all unique transmittances
340
+ transmittances = set()
341
+ for face in self.host.orphaned_faces:
342
+ for ap in face.apertures:
343
+ self._check_and_add_obj_transmit(ap, transmittances)
344
+ for dr in face.doors:
345
+ self._check_and_add_obj_transmit(dr, transmittances)
346
+ for ap in self.host.orphaned_apertures:
347
+ self._check_and_add_obj_transmit(ap, transmittances)
348
+ for dr in self.host.orphaned_doors:
349
+ if dr.is_glass:
350
+ self._check_and_add_obj_transmit(dr, transmittances)
351
+ # create the schedules from the transmittances
352
+ schedules = []
353
+ for trans in transmittances:
354
+ sch_name = 'Constant %.3f Transmittance' % trans
355
+ sch = ScheduleRuleset.from_constant_value(sch_name, trans, fractional)
356
+ schedules.append(sch)
357
+ return schedules
358
+
359
+ @property
360
+ def program_types(self):
361
+ """Get a list of all unique ProgramTypes in the Model."""
362
+ program_types = []
363
+ for room in self.host.rooms:
364
+ if room.properties.energy._program_type is not None:
365
+ if not self._instance_in_array(room.properties.energy._program_type,
366
+ program_types):
367
+ program_types.append(room.properties.energy._program_type)
368
+ return list(set(program_types)) # catch equivalent program types
369
+
370
+ @property
371
+ def hvacs(self):
372
+ """Get a list of all unique HVAC systems in the Model."""
373
+ hvacs = []
374
+ for room in self.host.rooms:
375
+ if room.properties.energy._hvac is not None:
376
+ if not self._instance_in_array(room.properties.energy._hvac, hvacs):
377
+ hvacs.append(room.properties.energy._hvac)
378
+ return hvacs
379
+
380
+ @property
381
+ def shws(self):
382
+ """Get a list of all unique SHW systems in the Model."""
383
+ shws = []
384
+ for room in self.host.rooms:
385
+ if room.properties.energy._shw is not None:
386
+ if not self._instance_in_array(room.properties.energy._shw, shws):
387
+ shws.append(room.properties.energy._shw)
388
+ return shws
389
+
390
+ @property
391
+ def electric_load_center(self):
392
+ """Get or set global parameters for ventilation cooling simulation."""
393
+ return self._electric_load_center
394
+
395
+ @electric_load_center.setter
396
+ def electric_load_center(self, value):
397
+ if value is None:
398
+ value = ElectricLoadCenter()
399
+ else:
400
+ assert isinstance(value, ElectricLoadCenter), \
401
+ 'electric_load_center must be a ' \
402
+ 'ElectricLoadCenter object. Got: {}.'.format(value)
403
+ self._electric_load_center = value
404
+
405
+ @property
406
+ def ventilation_simulation_control(self):
407
+ """Get or set global parameters for ventilation cooling simulation."""
408
+ return self._ventilation_simulation_control
409
+
410
+ @ventilation_simulation_control.setter
411
+ def ventilation_simulation_control(self, value):
412
+ if value is None:
413
+ value = VentilationSimulationControl()
414
+ else:
415
+ assert isinstance(value, VentilationSimulationControl), \
416
+ 'ventilation_simulation_control must be a ' \
417
+ 'VentilationSimulationControl object. Got: {}.'.format(value)
418
+ self._ventilation_simulation_control = value
419
+
420
+ def aperture_constructions(self, room_assigned_only=True):
421
+ """Get only the constructions assigned to Apertures in the Model.
422
+
423
+ Args:
424
+ room_assigned_only: Boolean to note whether only the constructions that
425
+ are a part of Room-assigned Apertures should be returned (True) or
426
+ constructions assigned to all Apertures should be included (False).
427
+ """
428
+ if room_assigned_only:
429
+ aps_to_search = []
430
+ for room in self.host.rooms:
431
+ for face in room.faces:
432
+ aps_to_search.extend(face.apertures)
433
+ else:
434
+ aps_to_search = self.host.apertures
435
+ constructions = []
436
+ for ap in aps_to_search:
437
+ self._check_and_add_obj_construction_inc_parent(ap, constructions)
438
+ return list(set(constructions))
439
+
440
+ def door_constructions(self, room_assigned_only=True):
441
+ """Get only the constructions assigned to Doors in the Model.
442
+
443
+ Args:
444
+ room_assigned_only: Boolean to note whether only the constructions that
445
+ are a part of Room-assigned Doors should be returned (True) or
446
+ constructions assigned to all Doors should be included (False).
447
+ """
448
+ if room_assigned_only:
449
+ doors_to_search = []
450
+ for room in self.host.rooms:
451
+ for face in room.faces:
452
+ doors_to_search.extend(face.doors)
453
+ else:
454
+ doors_to_search = self.host.doors
455
+ constructions = []
456
+ for dr in doors_to_search:
457
+ self._check_and_add_obj_construction_inc_parent(dr, constructions)
458
+ return list(set(constructions))
459
+
460
+ def autocalculate_ventilation_simulation_control(self):
461
+ """Set geometry properties of ventilation_simulation_control with Model's rooms.
462
+
463
+ The room geometry of the host Model will be used to assign the aspect_ratio,
464
+ long_axis_angle, and the building_type. Note that these properties are only
465
+ meaningful for simulations using the AirflowNetwork.
466
+ """
467
+ self.ventilation_simulation_control.assign_geometry_properties_from_rooms(
468
+ self.host.rooms)
469
+
470
+ def remove_child_constructions(self):
471
+ """Remove constructions assigned to Faces, Apertures, Doors and Shades.
472
+
473
+ This means that all constructions of the Mode's rooms will be assigned
474
+ by the Rooms' construction_set (or the Honeybee default ConstructionSet
475
+ if Rooms have no construction set).
476
+ """
477
+ for room in self._host.rooms:
478
+ room.properties.energy.remove_child_constructions()
479
+
480
+ def window_construction_by_orientation(
481
+ self, construction, orientation=0, offset=45, north_vector=Vector2D(0, 1)):
482
+ """Set the construction of exterior Apertures facing a given orientation.
483
+
484
+ This is useful for testing orientation-specific energy conservation
485
+ strategies or creating ASHRAE baseline buildings.
486
+
487
+ Args:
488
+ construction: A WindowConstruction that will be assigned to all of the
489
+ room's Apertures in Walls that are facing a certain orientation.
490
+ orientation: A number between 0 and 360 that represents the orientation
491
+ in degrees to which the construction will be assigned. 0 = North,
492
+ 90 = East, 180 = South, 270 = West. (Default: 0 for North).
493
+ offset: A number between 0 and 180 that represents the offset from the
494
+ orientation in degrees for which the construction will be assigned.
495
+ For example, a value of 45 indicates that any Apertures falling
496
+ in the 90 degree range around the orientation will get the input
497
+ construction. (Default: 45).
498
+ north_vector: A ladybug_geometry Vector3D for the north direction.
499
+ Default is the Y-axis (0, 1).
500
+ """
501
+ for room in self._host.rooms:
502
+ room.properties.energy.window_construction_by_orientation(
503
+ construction, orientation, offset, north_vector)
504
+
505
+ def remove_hvac_from_no_setpoints(self):
506
+ """Remove any HVAC systems assigned to Rooms that have no thermostat setpoints.
507
+
508
+ This will ensure that EnergyPlus does not fail when it tries to simulate
509
+ a HVAC for which there are no criteria to meet.
510
+
511
+ Returns:
512
+ A list of text strings for each room that had an HVAC removed because
513
+ of a lack of setpoints. This can be printed to logs or registered as
514
+ a warning in the interface.
515
+ """
516
+ removed_msgs = []
517
+ for room in self._host.rooms:
518
+ if room.properties.energy.hvac is not None \
519
+ and room.properties.energy.setpoint is None:
520
+ hvac_name = room.properties.energy.hvac.display_name
521
+ room.properties.energy.hvac = None
522
+ msg = 'Room "{}" has an HVAC assigned to it but does not have any ' \
523
+ 'thermostat setpoints.\nThe HVAC "{}" will not be translated ' \
524
+ 'for this room.'.format(room.display_name, hvac_name)
525
+ removed_msgs.append(msg)
526
+ return removed_msgs
527
+
528
+ def missing_adjacencies_to_adiabatic(self):
529
+ """Set any Faces with missing adjacencies in the model to adiabatic.
530
+
531
+ If any of the Faces with missing adjacencies have sub-faces, these will be
532
+ removed in order to accommodate the adiabatic condition. Similarly, if the
533
+ Face is an AirBoundary, the type will be set to a Wall.
534
+
535
+ Note that this method assumes all of the Surface boundary conditions
536
+ are set up correctly with the last boundary_condition_object being
537
+ the adjacent room.
538
+ """
539
+ room_ids = set()
540
+ for room in self.host._rooms:
541
+ room_ids.add(room.identifier)
542
+ for room in self.host._rooms:
543
+ for face in room._faces:
544
+ if isinstance(face.boundary_condition, Surface):
545
+ bc_room = face.boundary_condition.boundary_condition_objects[-1]
546
+ if bc_room not in room_ids:
547
+ face.remove_sub_faces()
548
+ if isinstance(face.type, AirBoundary):
549
+ face.type = face_types.wall
550
+ face.boundary_condition = boundary_conditions.adiabatic
551
+ elif isinstance(face.type, AirBoundary): # assume it's Surface
552
+ face.type = face_types.wall
553
+ face.boundary_condition = boundary_conditions.adiabatic
554
+
555
+ def assign_radiance_solar_interior(self):
556
+ """Assign honeybee Radiance modifiers based on interior solar properties."""
557
+ mod_sets = {}
558
+ for constr_set in self.construction_sets + [generic_construction_set]:
559
+ mod_sets[constr_set.identifier] = constr_set.to_radiance_solar_interior()
560
+ self._assign_room_modifier_sets(mod_sets)
561
+ mods = {}
562
+ for con in self.face_constructions + self.shade_constructions:
563
+ mods[con.identifier] = con.to_radiance_solar_interior() \
564
+ if isinstance(con, OpaqueConstruction) else con.to_radiance_solar()
565
+ self._assign_face_modifiers(mods)
566
+
567
+ def assign_radiance_visible_interior(self):
568
+ """Assign honeybee Radiance modifiers based on interior visible properties."""
569
+ mod_sets = {}
570
+ for constr_set in self.construction_sets + [generic_construction_set]:
571
+ mod_sets[constr_set.identifier] = constr_set.to_radiance_visible_interior()
572
+ self._assign_room_modifier_sets(mod_sets)
573
+ mods = {}
574
+ for con in self.face_constructions + self.shade_constructions:
575
+ mods[con.identifier] = con.to_radiance_visible_interior() \
576
+ if isinstance(con, OpaqueConstruction) else con.to_radiance_visible()
577
+ self._assign_face_modifiers(mods)
578
+
579
+ def assign_radiance_solar_exterior(self):
580
+ """Assign honeybee Radiance modifiers based on exterior solar properties."""
581
+ mod_sets = {}
582
+ for constr_set in self.construction_sets + [generic_construction_set]:
583
+ mod_sets[constr_set.identifier] = constr_set.to_radiance_solar_exterior()
584
+ self._assign_room_modifier_sets(mod_sets)
585
+ mods = {}
586
+ for con in self.face_constructions + self.shade_constructions:
587
+ mods[con.identifier] = con.to_radiance_solar_exterior() \
588
+ if isinstance(con, OpaqueConstruction) else con.to_radiance_solar()
589
+ self._assign_face_modifiers(mods)
590
+
591
+ def assign_radiance_visible_exterior(self):
592
+ """Assign honeybee Radiance modifiers based on exterior visible properties."""
593
+ mod_sets = {}
594
+ for constr_set in self.construction_sets + [generic_construction_set]:
595
+ mod_sets[constr_set.identifier] = constr_set.to_radiance_visible_exterior()
596
+ self._assign_room_modifier_sets(mod_sets)
597
+ mods = {}
598
+ for con in self.face_constructions + self.shade_constructions:
599
+ mods[con.identifier] = con.to_radiance_visible_exterior() \
600
+ if isinstance(con, OpaqueConstruction) else con.to_radiance_visible()
601
+ self._assign_face_modifiers(mods)
602
+
603
+ def offset_and_assign_exterior_face_modifiers(
604
+ self, reflectance_type='Solar', offset=0.02):
605
+ """Offset all exterior Faces and assign them a modifier based on exterior layer.
606
+
607
+ This is often useful in conjunction with the assign_radiance_solar_interior
608
+ or the assign_radiance_visible_interior to make a radiance model that
609
+ accounts for both the interior and exterior material layers.
610
+
611
+ Note that this method will add the offset faces as orphaned faces and so the
612
+ model will not be simulate-able in EnergyPlus after running this method
613
+ (it is only intended to be simulated within Radiance).
614
+
615
+ Args:
616
+ reflectance_type: Text for the type of reflectance to be used in the
617
+ assigned modifier. Must be either Solar or Visible. (Default: Solar).
618
+ offset: A number for the distance at which the exterior Faces should
619
+ be offset. (Default: 0.02, suitable for models in meters).
620
+ """
621
+ # collect all of the unique exterior face constructions
622
+ constructions = []
623
+ for room in self.host.rooms:
624
+ for face in room.faces: # check all Face constructions
625
+ if isinstance(face.boundary_condition, Outdoors):
626
+ constr = face.properties.energy.construction
627
+ if not self._instance_in_array(constr, constructions):
628
+ constructions.append(constr)
629
+ constructions = set(constructions)
630
+ # convert constructions into modifiers
631
+ mods = {}
632
+ for con in constructions:
633
+ mods[con.identifier] = con.to_radiance_visible_exterior() \
634
+ if reflectance_type == 'Visible' else con.to_radiance_solar_exterior()
635
+ # loop through the faces and create new offset exterior ones
636
+ new_faces = []
637
+ for room in self._host.rooms:
638
+ for face in room.faces:
639
+ if isinstance(face.boundary_condition, Outdoors):
640
+ new_geo = face.punched_geometry.move(face.normal * offset)
641
+ new_id = '{}_ext'.format(face.identifier)
642
+ new_face = Face(
643
+ new_id, new_geo, face.type, face.boundary_condition)
644
+ new_face.properties.radiance.modifier = \
645
+ mods[face.properties.energy.construction.identifier]
646
+ new_faces.append(new_face)
647
+ # add the new faces to the host model
648
+ for face in new_faces:
649
+ self._host.add_face(face)
650
+
651
+ def assign_dynamic_aperture_groups(self):
652
+ """Assign aperture groups to all Apertures with dynamic and shaded constructions.
653
+
654
+ Note that this method will only add two groups for each dynamic aperture.
655
+ The first group will be completely transparent while the second group
656
+ will be a 100% transmittance perfectly diffusing modifier. This is done
657
+ with the assumption that EnergyPlus transmittance results will be used to
658
+ appropriately account for the transmittance of states in the results.
659
+ """
660
+ # import dependencies and set up reused variables
661
+ try:
662
+ from honeybee_radiance.modifier.material import Trans
663
+ from honeybee_radiance.dynamic.state import RadianceSubFaceState
664
+ except ImportError as e:
665
+ raise ImportError('honeybee_radiance library must be installed to use '
666
+ 'assign_dynamic_aperture_groups() method. {}'.format(e))
667
+ all_spec = Trans('complete_spec', 1, 1, 1, 0, 0, 1, 1)
668
+ all_diff = Trans('complete_diff', 1, 1, 1, 0, 0, 1, 0)
669
+
670
+ # establish groups based on similar constructions, orientations and controls
671
+ group_dict = {}
672
+ for room in self.host.rooms:
673
+ for face in room.faces:
674
+ for ap in face.apertures:
675
+ u_id = None
676
+ con = ap.properties.energy.construction
677
+ if isinstance(con, WindowConstructionDynamic):
678
+ orient = '{}_{}'.format(int(ap.azimuth), int(ap.altitude))
679
+ u_id = '{}_{}'.format(con.identifier, orient)
680
+ elif isinstance(con, WindowConstructionShade):
681
+ orient = '{}_{}'.format(int(ap.azimuth), int(ap.altitude))
682
+ if con.is_groupable:
683
+ u_id = '{}_{}'.format(con.identifier, orient)
684
+ elif con.is_room_groupable:
685
+ u_id = '{}_{}_{}'.format(
686
+ con.identifier, room.identifier, orient)
687
+ else:
688
+ u_id = ap.identifier
689
+ if u_id is not None:
690
+ try:
691
+ group_dict[u_id].append(ap)
692
+ except KeyError:
693
+ group_dict[u_id] = [ap]
694
+
695
+ # create the actual aperture groups and assign states
696
+ for group in group_dict.values():
697
+ grp_name = group[0].identifier
698
+ for ap in group:
699
+ ap.properties.radiance.dynamic_group_identifier = grp_name
700
+ spec_state = RadianceSubFaceState(all_spec)
701
+ diff_state = RadianceSubFaceState(all_diff)
702
+ ap.properties.radiance.states = [spec_state, diff_state]
703
+
704
+ def generate_ground_room(self, soil_construction):
705
+ """Generate and add a Room to the Model that represents the ground.
706
+
707
+ The Room will be added such that it exists below all of the other geometry
708
+ of the model and covers the full XY extents of the model.
709
+
710
+ This is useful when it is desirable to track the ground surface temperature
711
+ or when the model needs a simple Room to be able to simulate in EnergyPlus.
712
+
713
+ Args:
714
+ soil_construction: An OpaqueConstruction that reflects the soil type of
715
+ the ground. If a multi-layered construction is input, the multiple
716
+ layers will only be used for the roof Face of the Room and all other
717
+ Faces will get a construction with the inner-most layer assigned.
718
+ If the outer-most material is an EnergyMaterialVegetation and there
719
+ are no other layers in the construction, the vegetation's soil
720
+ material will be used for all other Faces.
721
+ """
722
+ # create the room geometry from the min and max points
723
+ min_pt, max_pt = self.host.min, self.host.max
724
+ room_height = 1 / conversion_factor_to_meters(self.host.units)
725
+ rm_origin = Point3D(min_pt.x, min_pt.y, min_pt.z - room_height)
726
+ ground = Room.from_box(
727
+ 'Ground_Room', max_pt.x - min_pt.x, max_pt.y - min_pt.y, room_height,
728
+ origin=rm_origin)
729
+ # turn the room into a ground with an appropriate construction
730
+ ground.properties.energy.make_ground(soil_construction)
731
+ self.host.add_room(ground)
732
+ return ground
733
+
734
+ def check_for_extension(self, raise_exception=True, detailed=False):
735
+ """Check that the Model is valid for EnergyPlus simulation.
736
+
737
+ This process includes all relevant honeybee-core checks as well as checks
738
+ that apply only for EnergyPlus.
739
+
740
+ Args:
741
+ raise_exception: Boolean to note whether a ValueError should be raised
742
+ if any errors are found. If False, this method will simply
743
+ return a text string with all errors that were found. (Default: True).
744
+ detailed: Boolean for whether the returned object is a detailed list of
745
+ dicts with error info or a string with a message. (Default: False).
746
+
747
+ Returns:
748
+ A text string with all errors that were found or a list if detailed is True.
749
+ This string (or list) will be empty if no errors were found.
750
+ """
751
+ # set up defaults to ensure the method runs correctly
752
+ detailed = False if raise_exception else detailed
753
+ msgs = []
754
+ tol = self.host.tolerance
755
+ ang_tol = self.host.angle_tolerance
756
+ e_tol = parse_distance_string('1cm', self.host.units)
757
+
758
+ # perform checks for duplicate identifiers, which might mess with other checks
759
+ msgs.append(self.host.check_all_duplicate_identifiers(False, detailed))
760
+
761
+ # perform several checks for the Honeybee schema geometry rules
762
+ msgs.append(self.host.check_planar(tol, False, detailed))
763
+ msgs.append(self.host.check_self_intersecting(tol, False, detailed))
764
+ msgs.append(self.host.check_degenerate_rooms(e_tol, False, detailed))
765
+
766
+ # perform geometry checks related to parent-child relationships
767
+ msgs.append(self.host.check_sub_faces_valid(tol, ang_tol, False, detailed))
768
+ msgs.append(self.host.check_sub_faces_overlapping(tol, False, detailed))
769
+ msgs.append(self.host.check_rooms_solid(tol, ang_tol, False, detailed))
770
+ msgs.append(self.host.check_upside_down_faces(ang_tol, False, detailed))
771
+
772
+ # perform checks related to adjacency relationships
773
+ msgs.append(self.host.check_room_volume_collisions(tol, False, detailed))
774
+ msgs.append(self.host.check_missing_adjacencies(False, detailed))
775
+ msgs.append(self.host.check_matching_adjacent_areas(tol, False, detailed))
776
+ msgs.append(self.host.check_all_air_boundaries_adjacent(False, detailed))
777
+
778
+ # perform checks for specific energy simulation rules
779
+ msgs.append(self.check_all_zones_have_one_hvac(False, detailed))
780
+ msgs.append(self.check_detailed_hvac_rooms(False, detailed))
781
+ msgs.append(self.check_shw_rooms_in_model(False, detailed))
782
+ msgs.append(self.check_maximum_elevation(1000, False, detailed))
783
+ msgs.append(self.check_one_vegetation_material(False, detailed))
784
+ msgs.append(self.check_interior_constructions_reversed(False, detailed))
785
+
786
+ # output a final report of errors or raise an exception
787
+ full_msgs = [msg for msg in msgs if msg]
788
+ if detailed:
789
+ return [m for msg in full_msgs for m in msg]
790
+ full_msg = '\n'.join(full_msgs)
791
+ if raise_exception and len(full_msgs) != 0:
792
+ raise ValueError(full_msg)
793
+ return full_msg
794
+
795
+ def check_generic(self, raise_exception=True, detailed=False):
796
+ """Check generic of the aspects of the Model energy properties.
797
+
798
+ This includes checks for everything except duplicate identifiers for
799
+ constructions, schedules, etc. Typically, these checks just add to the
800
+ validation time without providing useful information since extension
801
+ objects with duplicate IDs are lost during HBJSON serialization.
802
+
803
+ Args:
804
+ raise_exception: Boolean to note whether a ValueError should be raised
805
+ if any errors are found. If False, this method will simply
806
+ return a text string with all errors that were found.
807
+ detailed: Boolean for whether the returned object is a detailed list of
808
+ dicts with error info or a string with a message. (Default: False).
809
+
810
+ Returns:
811
+ A text string with all errors that were found or a list if detailed is True.
812
+ This string (or list) will be empty if no errors were found.
813
+ """
814
+ # set up defaults to ensure the method runs correctly
815
+ detailed = False if raise_exception else detailed
816
+ msgs = []
817
+ # perform checks for specific energy simulation rules
818
+ msgs.append(self.check_all_zones_have_one_hvac(False, detailed))
819
+ msgs.append(self.check_detailed_hvac_rooms(False, detailed))
820
+ msgs.append(self.check_shw_rooms_in_model(False, detailed))
821
+ msgs.append(self.check_maximum_elevation(1000, False, detailed))
822
+ msgs.append(self.check_one_vegetation_material(False, detailed))
823
+ msgs.append(self.check_interior_constructions_reversed(False, detailed))
824
+ # output a final report of errors or raise an exception
825
+ full_msgs = [msg for msg in msgs if msg]
826
+ if detailed:
827
+ return [m for msg in full_msgs for m in msg]
828
+ full_msg = '\n'.join(full_msgs)
829
+ if raise_exception and len(full_msgs) != 0:
830
+ raise ValueError(full_msg)
831
+ return full_msg
832
+
833
+ def check_all(self, raise_exception=True, detailed=False):
834
+ """Check all of the aspects of the Model energy properties.
835
+
836
+ Args:
837
+ raise_exception: Boolean to note whether a ValueError should be raised
838
+ if any errors are found. If False, this method will simply
839
+ return a text string with all errors that were found. (Default: True).
840
+ detailed: Boolean for whether the returned object is a detailed list of
841
+ dicts with error info or a string with a message. (Default: False).
842
+
843
+ Returns:
844
+ A text string with all errors that were found or a list if detailed is True.
845
+ This string (or list) will be empty if no errors were found.
846
+ """
847
+ # set up defaults to ensure the method runs correctly
848
+ detailed = False if raise_exception else detailed
849
+ msgs = []
850
+ # perform checks for duplicate identifiers
851
+ msgs.append(self.check_all_duplicate_identifiers(False, detailed))
852
+ # perform checks for specific energy simulation rules
853
+ msgs.append(self.check_all_zones_have_one_hvac(False, detailed))
854
+ msgs.append(self.check_detailed_hvac_rooms(False, detailed))
855
+ msgs.append(self.check_shw_rooms_in_model(False, detailed))
856
+ msgs.append(self.check_maximum_elevation(1000, False, detailed))
857
+ msgs.append(self.check_one_vegetation_material(False, detailed))
858
+ msgs.append(self.check_interior_constructions_reversed(False, detailed))
859
+ # output a final report of errors or raise an exception
860
+ full_msgs = [msg for msg in msgs if msg]
861
+ if detailed:
862
+ return [m for msg in full_msgs for m in msg]
863
+ full_msg = '\n'.join(full_msgs)
864
+ if raise_exception and len(full_msgs) != 0:
865
+ raise ValueError(full_msg)
866
+ return full_msg
867
+
868
+ def check_all_duplicate_identifiers(self, raise_exception=True, detailed=False):
869
+ """Check that there are no duplicate identifiers for any energy objects.
870
+
871
+ This includes Materials, Constructions, ConstructionSets, Schedules,
872
+ Programs, HVACs, and SHWs.
873
+
874
+ Args:
875
+ raise_exception: Boolean to note whether a ValueError should be raised
876
+ if any duplicate identifiers are found. If False, this method will simply
877
+ return a text string with all errors that were found. (Default: True).
878
+ detailed: Boolean for whether the returned object is a detailed list of
879
+ dicts with error info or a string with a message. (Default: False).
880
+
881
+ Returns:
882
+ A text string with all errors that were found or a list if detailed is True.
883
+ This string (or list) will be empty if no errors were found.
884
+ """
885
+ # set up defaults to ensure the method runs correctly
886
+ detailed = False if raise_exception else detailed
887
+ msgs = []
888
+ # perform checks for duplicate identifiers
889
+ msgs.append(self.check_duplicate_material_identifiers(False, detailed))
890
+ msgs.append(self.check_duplicate_construction_identifiers(False, detailed))
891
+ msgs.append(self.check_duplicate_construction_set_identifiers(False, detailed))
892
+ stl_msgs = self.check_duplicate_schedule_type_limit_identifiers(False, detailed)
893
+ msgs.append(stl_msgs)
894
+ msgs.append(self.check_duplicate_schedule_identifiers(False, detailed))
895
+ msgs.append(self.check_duplicate_program_type_identifiers(False, detailed))
896
+ msgs.append(self.check_duplicate_hvac_identifiers(False, detailed))
897
+ msgs.append(self.check_duplicate_shw_identifiers(False, detailed))
898
+ # output a final report of errors or raise an exception
899
+ full_msgs = [msg for msg in msgs if msg]
900
+ if detailed:
901
+ return [m for msg in full_msgs for m in msg]
902
+ full_msg = '\n'.join(full_msgs)
903
+ if raise_exception and len(full_msgs) != 0:
904
+ raise ValueError(full_msg)
905
+ return full_msg
906
+
907
+ def check_duplicate_material_identifiers(self, raise_exception=True, detailed=False):
908
+ """Check that there are no duplicate Material identifiers in the model.
909
+
910
+ Args:
911
+ raise_exception: Boolean to note whether a ValueError should be raised
912
+ if duplicate identifiers are found. (Default: True).
913
+ detailed: Boolean for whether the returned object is a detailed list of
914
+ dicts with error info or a string with a message. (Default: False).
915
+
916
+ Returns:
917
+ A string with the message or a list with a dictionary if detailed is True.
918
+ """
919
+ return check_duplicate_identifiers(
920
+ self.materials, raise_exception, 'Material',
921
+ detailed, '020001', 'Energy', error_type='Duplicate Material Identifier')
922
+
923
+ def check_duplicate_construction_identifiers(
924
+ self, raise_exception=True, detailed=False):
925
+ """Check that there are no duplicate Construction identifiers in the model.
926
+
927
+ Args:
928
+ raise_exception: Boolean to note whether a ValueError should be raised
929
+ if duplicate identifiers are found. (Default: True).
930
+ detailed: Boolean for whether the returned object is a detailed list of
931
+ dicts with error info or a string with a message. (Default: False).
932
+
933
+ Returns:
934
+ A string with the message or a list with a dictionary if detailed is True.
935
+ """
936
+ return check_duplicate_identifiers(
937
+ self.constructions, raise_exception, 'Construction',
938
+ detailed, '020002', 'Energy', error_type='Duplicate Construction Identifier')
939
+
940
+ def check_duplicate_construction_set_identifiers(
941
+ self, raise_exception=True, detailed=False):
942
+ """Check that there are no duplicate ConstructionSet identifiers in the model.
943
+
944
+ Args:
945
+ raise_exception: Boolean to note whether a ValueError should be raised
946
+ if duplicate identifiers are found. (Default: True).
947
+ detailed: Boolean for whether the returned object is a detailed list of
948
+ dicts with error info or a string with a message. (Default: False).
949
+
950
+ Returns:
951
+ A string with the message or a list with a dictionary if detailed is True.
952
+ """
953
+ return check_duplicate_identifiers(
954
+ self.construction_sets, raise_exception, 'ConstructionSet',
955
+ detailed, '020003', 'Energy',
956
+ error_type='Duplicate ConstructionSet Identifier')
957
+
958
+ def check_duplicate_schedule_type_limit_identifiers(
959
+ self, raise_exception=True, detailed=False):
960
+ """Check that there are no duplicate ScheduleTypeLimit identifiers in the model.
961
+
962
+ Args:
963
+ raise_exception: Boolean to note whether a ValueError should be raised
964
+ if duplicate identifiers are found. (Default: True).
965
+ detailed: Boolean for whether the returned object is a detailed list of
966
+ dicts with error info or a string with a message. (Default: False).
967
+
968
+ Returns:
969
+ A string with the message or a list with a dictionary if detailed is True.
970
+ """
971
+ return check_duplicate_identifiers(
972
+ self.schedule_type_limits, raise_exception, 'ScheduleTypeLimit',
973
+ detailed, '020004', 'Energy',
974
+ error_type='Duplicate ScheduleTypeLimit Identifier')
975
+
976
+ def check_duplicate_schedule_identifiers(self, raise_exception=True, detailed=False):
977
+ """Check that there are no duplicate Schedule identifiers in the model.
978
+
979
+ Args:
980
+ raise_exception: Boolean to note whether a ValueError should be raised
981
+ if duplicate identifiers are found. (Default: True).
982
+ detailed: Boolean for whether the returned object is a detailed list of
983
+ dicts with error info or a string with a message. (Default: False).
984
+
985
+ Returns:
986
+ A string with the message or a list with a dictionary if detailed is True.
987
+ """
988
+ return check_duplicate_identifiers(
989
+ self.schedules, raise_exception, 'Schedule', detailed, '020005', 'Energy',
990
+ error_type='Duplicate Schedule Identifier')
991
+
992
+ def check_duplicate_program_type_identifiers(
993
+ self, raise_exception=True, detailed=False):
994
+ """Check that there are no duplicate ProgramType identifiers in the model.
995
+
996
+ Args:
997
+ raise_exception: Boolean to note whether a ValueError should be raised
998
+ if duplicate identifiers are found. (Default: True).
999
+ detailed: Boolean for whether the returned object is a detailed list of
1000
+ dicts with error info or a string with a message. (Default: False).
1001
+
1002
+ Returns:
1003
+ A string with the message or a list with a dictionary if detailed is True.
1004
+ """
1005
+ return check_duplicate_identifiers(
1006
+ self.program_types, raise_exception, 'ProgramType',
1007
+ detailed, '020006', 'Energy', error_type='Duplicate ProgramType Identifier')
1008
+
1009
+ def check_duplicate_hvac_identifiers(self, raise_exception=True, detailed=False):
1010
+ """Check that there are no duplicate HVAC identifiers in the model.
1011
+
1012
+ Args:
1013
+ raise_exception: Boolean to note whether a ValueError should be raised
1014
+ if duplicate identifiers are found. (Default: True).
1015
+ detailed: Boolean for whether the returned object is a detailed list of
1016
+ dicts with error info or a string with a message. (Default: False).
1017
+
1018
+ Returns:
1019
+ A string with the message or a list with a dictionary if detailed is True.
1020
+ """
1021
+ return check_duplicate_identifiers(
1022
+ self.hvacs, raise_exception, 'HVAC', detailed, '020007', 'Energy',
1023
+ error_type='Duplicate HVAC Identifier')
1024
+
1025
+ def check_duplicate_shw_identifiers(self, raise_exception=True, detailed=False):
1026
+ """Check that there are no duplicate SHW identifiers in the model.
1027
+
1028
+ Args:
1029
+ raise_exception: Boolean to note whether a ValueError should be raised
1030
+ if duplicate identifiers are found. (Default: True).
1031
+ detailed: Boolean for whether the returned object is a detailed list of
1032
+ dicts with error info or a string with a message. (Default: False).
1033
+
1034
+ Returns:
1035
+ A string with the message or a list with a dictionary if detailed is True.
1036
+ """
1037
+ return check_duplicate_identifiers(
1038
+ self.shws, raise_exception, 'SHW', detailed, '020008', 'Energy',
1039
+ error_type='Duplicate SHW Identifier')
1040
+
1041
+ def check_all_zones_have_one_hvac(self, raise_exception=True, detailed=False):
1042
+ """Check that all rooms within each zone have only one HVAC assigned to them.
1043
+
1044
+ Multiple HVAC systems serving one zone typically causes EnergyPlus simulation
1045
+ failures and is often a mistake that results from changing zoning strategies
1046
+ without changing the HVAC to be coordinated with the new zones.
1047
+
1048
+ Note that having some Rooms in the zone referencing the HVAC and others
1049
+ with no HVAC is considered permissible since this just implies that the
1050
+ thermostat or zone equipment may be in one of the rooms but the whole
1051
+ zone is conditioned by this equipment.
1052
+
1053
+ Args:
1054
+ raise_exception: Boolean to note whether a ValueError should be raised if
1055
+ there are rooms within zones that have different HVAC systems
1056
+ assigned to them. (Default: True).
1057
+ detailed: Boolean for whether the returned object is a detailed list of
1058
+ dicts with error info or a string with a message. (Default: False).
1059
+
1060
+ Returns:
1061
+ A string with the message or a list with a dictionary if detailed is True.
1062
+ """
1063
+ detailed = False if raise_exception else detailed
1064
+ # gather a list of all the zones with multiple HVACs
1065
+ invalid_zones = {}
1066
+ for zone, rooms in self.host.zone_dict.items():
1067
+ if len(rooms) == 1:
1068
+ continue
1069
+ hvacs = [r.properties.energy.hvac for r in rooms
1070
+ if r.properties.energy.hvac is not None]
1071
+ if len(hvacs) > 1:
1072
+ invalid_zones[zone] = [rooms, hvacs]
1073
+
1074
+ # if multiple HVACs in the same zone were found, then report the issue
1075
+ if len(invalid_zones) != 0:
1076
+ if detailed:
1077
+ all_err = []
1078
+ for zone, data in invalid_zones.items():
1079
+ rooms, hvacs = data
1080
+ hvac_names = [h.display_name for h in hvacs]
1081
+ msg = 'Zone "{}" is served by the following different HVAC ' \
1082
+ 'systems:\n{}'.format(zone, '\n'.join(hvac_names))
1083
+ error_dict = {
1084
+ 'type': 'ValidationError',
1085
+ 'code': '020014',
1086
+ 'error_type': 'Zone with Different Room HVACs',
1087
+ 'extension_type': 'Energy',
1088
+ 'element_type': 'Room',
1089
+ 'element_id': [r.identifier for r in rooms],
1090
+ 'element_name': [r.display_name for r in rooms],
1091
+ 'message': msg
1092
+ }
1093
+ all_err.append(error_dict)
1094
+ return all_err
1095
+ else:
1096
+ err_zones = []
1097
+ for zone, data in invalid_zones.items():
1098
+ rooms, hvacs = data
1099
+ hvac_names = [h.display_name for h in hvacs]
1100
+ err_zn = ' {} - [HVACS: {}]'.format(zone, ', '.join(hvac_names))
1101
+ err_zones.append(err_zn)
1102
+ msg = 'The model has the following invalid zones served by different ' \
1103
+ 'HVAC systems:\n{}'.format('\n'.join(err_zones))
1104
+ if raise_exception:
1105
+ raise ValueError(msg)
1106
+ return msg
1107
+ return [] if detailed else ''
1108
+
1109
+ def check_shw_rooms_in_model(self, raise_exception=True, detailed=False):
1110
+ """Check that the room_identifiers of SHWSystems are in the model.
1111
+
1112
+ Args:
1113
+ raise_exception: Boolean to note whether a ValueError should be raised if
1114
+ SHWSystems reference Rooms that are not in the Model. (Default: True).
1115
+ detailed: Boolean for whether the returned object is a detailed list of
1116
+ dicts with error info or a string with a message. (Default: False).
1117
+
1118
+ Returns:
1119
+ A string with the message or a list with a dictionary if detailed is True.
1120
+ """
1121
+ detailed = False if raise_exception else detailed
1122
+ # gather a list of all the missing rooms
1123
+ shw_ids = [(shw_sys, shw_sys.ambient_condition) for shw_sys in self.shws
1124
+ if isinstance(shw_sys.ambient_condition, str)]
1125
+ room_ids = set(room.identifier for room in self.host.rooms)
1126
+ missing_rooms = [] if detailed else set()
1127
+ for shw_sys in shw_ids:
1128
+ if shw_sys[1] not in room_ids:
1129
+ if detailed:
1130
+ missing_rooms.append(shw_sys[0])
1131
+ else:
1132
+ missing_rooms.add(shw_sys[1])
1133
+ # if missing rooms were found, then report the issue
1134
+ if len(missing_rooms) != 0:
1135
+ if detailed:
1136
+ all_err = []
1137
+ for shw_sys in missing_rooms:
1138
+ msg = 'SHWSystem "{}" has a ambient_condition referencing ' \
1139
+ 'a Room that is not in the ' \
1140
+ 'Model: "{}"'.format(shw_sys.identifier, shw_sys.room_identifier)
1141
+ error_dict = {
1142
+ 'type': 'ValidationError',
1143
+ 'code': '020009',
1144
+ 'error_type': 'SHWSystem Room Not In Model',
1145
+ 'extension_type': 'Energy',
1146
+ 'element_type': 'SHW',
1147
+ 'element_id': [shw_sys.identifier],
1148
+ 'element_name': [shw_sys.display_name],
1149
+ 'message': msg
1150
+ }
1151
+ all_err.append(error_dict)
1152
+ return all_err
1153
+ else:
1154
+ msg = 'The model has the following missing rooms referenced by SHW ' \
1155
+ 'Systems:\n{}'.format('\n'.join(missing_rooms))
1156
+ if raise_exception:
1157
+ raise ValueError(msg)
1158
+ return msg
1159
+ return [] if detailed else ''
1160
+
1161
+ def check_detailed_hvac_rooms(self, raise_exception=True, detailed=False):
1162
+ """Check that any rooms referenced within a DetailedHVAC exist in the model.
1163
+
1164
+ This method will also check to make sure that two detailed HVACs do not
1165
+ reference the same room and that all rooms referencing a detailed HVAC
1166
+ have setpoints.
1167
+
1168
+ Args:
1169
+ raise_exception: Boolean to note whether a ValueError should be raised if
1170
+ DetailedHVAC reference Rooms that are not in the Model. (Default: True).
1171
+ detailed: Boolean for whether the returned object is a detailed list of
1172
+ dicts with error info or a string with a message. (Default: False).
1173
+
1174
+ Returns:
1175
+ A string with the message or a list with a dictionary if detailed is True.
1176
+ """
1177
+ detailed = False if raise_exception else detailed
1178
+ all_err = []
1179
+
1180
+ # get all of the HVACs and check Rooms for setpoints in the process
1181
+ hvacs = []
1182
+ for room in self.host.rooms:
1183
+ hvac = room.properties.energy._hvac
1184
+ if hvac is not None and isinstance(hvac, DetailedHVAC):
1185
+ if not self._instance_in_array(hvac, hvacs):
1186
+ hvacs.append(hvac)
1187
+ if room.properties.energy.setpoint is None:
1188
+ msg = 'Detailed HVAC "{}" is assigned to Room {}, which lacks a ' \
1189
+ 'thermostat setpoint specification.\nThis makes the model ' \
1190
+ 'un-simulate-able in EnergyPlus/OpenStudio.'.format(
1191
+ hvac.display_name, room.full_id)
1192
+ if detailed:
1193
+ error_dict = {
1194
+ 'type': 'ValidationError',
1195
+ 'code': '020011',
1196
+ 'error_type': 'Room With HVAC Lacks Setpoint',
1197
+ 'extension_type': 'Energy',
1198
+ 'element_type': 'Room',
1199
+ 'element_id': [room.identifier],
1200
+ 'element_name': [room.display_name],
1201
+ 'message': msg
1202
+ }
1203
+ all_err.append(error_dict)
1204
+ else:
1205
+ all_err.append(msg)
1206
+
1207
+ # gather a list of all the rooms and evaluate it against the HVACs
1208
+ room_ids = set(room.zone for room in self.host.rooms)
1209
+ rooms_with_hvac = set()
1210
+ problem_hvacs, problem_rooms = [], []
1211
+ for hvac in hvacs:
1212
+ missing_rooms = []
1213
+ for zone in hvac.thermal_zones:
1214
+ if zone not in room_ids:
1215
+ missing_rooms.append(zone)
1216
+ if zone in rooms_with_hvac:
1217
+ problem_rooms.append(zone)
1218
+ rooms_with_hvac.add(zone)
1219
+ if len(missing_rooms) != 0:
1220
+ problem_hvacs.append((hvac, missing_rooms))
1221
+
1222
+ # if missing room references were found, report them
1223
+ if len(problem_hvacs) != 0:
1224
+ for bad_hvac, missing_rooms in problem_hvacs:
1225
+ msg = 'DetailedHVAC "{}" is referencing the following Rooms that ' \
1226
+ 'are not in the Model:\n{}'.format(
1227
+ bad_hvac.display_name, '\n'.join(missing_rooms))
1228
+ if detailed:
1229
+ error_dict = {
1230
+ 'type': 'ValidationError',
1231
+ 'code': '020012',
1232
+ 'error_type': 'DetailedHVAC Rooms Not In Model',
1233
+ 'extension_type': 'Energy',
1234
+ 'element_type': 'HVAC',
1235
+ 'element_id': [bad_hvac.identifier],
1236
+ 'element_name': [bad_hvac.display_name],
1237
+ 'message': msg
1238
+ }
1239
+ all_err.append(error_dict)
1240
+ else:
1241
+ all_err.append(msg)
1242
+
1243
+ # if rooms were found with multiple HVAC references, report them
1244
+ if len(problem_rooms) != 0:
1245
+ room_objs = [room for room in self.host.rooms
1246
+ if room.identifier in problem_rooms]
1247
+ for mult_hvac_room in room_objs:
1248
+ msg = 'Room {} is referenced by more than one detailed HVAC.'.format(
1249
+ mult_hvac_room.full_id)
1250
+ if detailed:
1251
+ error_dict = {
1252
+ 'type': 'ValidationError',
1253
+ 'code': '020013',
1254
+ 'error_type': 'Room Referenced by Multiple Detailed HVAC',
1255
+ 'extension_type': 'Energy',
1256
+ 'element_type': 'Room',
1257
+ 'element_id': [mult_hvac_room.identifier],
1258
+ 'element_name': [mult_hvac_room.display_name],
1259
+ 'message': msg
1260
+ }
1261
+ all_err.append(error_dict)
1262
+ else:
1263
+ all_err.append(msg)
1264
+
1265
+ # return any of the errors that were discovered
1266
+ if len(all_err) != 0:
1267
+ if raise_exception:
1268
+ raise ValueError('\n'.join(all_err))
1269
+ return all_err if detailed else '\n'.join(all_err)
1270
+ return [] if detailed else ''
1271
+
1272
+ def check_one_vegetation_material(self, raise_exception=True, detailed=False):
1273
+ """Check that there no more than one EnergyMaterialVegetation in the model.
1274
+
1275
+ It is a limitation of EnergyPlus that it can only simulate a single
1276
+ eco roof per model. This should probably be addressed at some point
1277
+ so that we don't always have to check for it.
1278
+
1279
+ Args:
1280
+ raise_exception: Boolean for whether a ValueError should be raised if there's
1281
+ more than one EnergyMaterialVegetation in the Model. (Default: True).
1282
+ detailed: Boolean for whether the returned object is a detailed list of
1283
+ dicts with error info or a string with a message. (Default: False).
1284
+
1285
+ Returns:
1286
+ A string with the message or a list with a dictionary if detailed is True.
1287
+ """
1288
+ detailed = False if raise_exception else detailed
1289
+ # first see if there's more than one vegetation material
1290
+ all_constrs = self.room_constructions + self.face_constructions
1291
+ materials = []
1292
+ for constr in all_constrs:
1293
+ try:
1294
+ materials.extend(constr.materials)
1295
+ except AttributeError:
1296
+ pass # ShadeConstruction
1297
+ all_mats = list(set(materials))
1298
+ veg_mats = [m for m in all_mats if isinstance(m, EnergyMaterialVegetation)]
1299
+
1300
+ # if more than one vegetation material was found, then report the issue
1301
+ if len(veg_mats) > 1:
1302
+ if detailed:
1303
+ all_err = []
1304
+ for v_mat in veg_mats:
1305
+ msg = 'EnergyMaterialVegetation "{}" is one of several vegetation ' \
1306
+ 'materials in the model.\nThis is not allowed by ' \
1307
+ 'EnergyPlus.'.format(v_mat.identifier)
1308
+ error_dict = {
1309
+ 'type': 'ValidationError',
1310
+ 'code': '020010',
1311
+ 'error_type': 'Multiple Vegetation Materials',
1312
+ 'extension_type': 'Energy',
1313
+ 'element_type': 'Material',
1314
+ 'element_id': [v_mat.identifier],
1315
+ 'element_name': [v_mat.display_name],
1316
+ 'message': msg
1317
+ }
1318
+ all_err.append(error_dict)
1319
+ return all_err
1320
+ else:
1321
+ veg_mats_ids = [v_mat.identifier for v_mat in veg_mats]
1322
+ msg = 'The model has multiple vegetation materials. This is not ' \
1323
+ 'allowed by EnergyPlus:\n{}'.format('\n'.join(veg_mats_ids))
1324
+ if raise_exception:
1325
+ raise ValueError(msg)
1326
+ return msg
1327
+ return [] if detailed else ''
1328
+
1329
+ def check_maximum_elevation(self, max_elevation=1000, raise_exception=True,
1330
+ detailed=False):
1331
+ """Check that no Rooms of the model are above a certain elevation.
1332
+
1333
+ EnergyPlus computes wind speeds, air pressures, and adjusts outdoor
1334
+ temperatures to account for the height above the ground using the Z values
1335
+ of the geometry coordinates. This is an important consideration when modeling
1336
+ skyscrapers but it can be detrimental when a building has been modeled
1337
+ with its coordinates at the height above sea level and the location
1338
+ is significantly above sea level (eg. Denver, Colorado).
1339
+
1340
+ This validation check is intended to catch such cases and make the user
1341
+ aware of the repercussions.
1342
+
1343
+ Args:
1344
+ max_elevation: A number for the maximum elevation in Meters that the
1345
+ model's rooms are permitted to be at before a ValidationError is
1346
+ reported. While EnergyPlus technically still simulates with models that
1347
+ are 12 kilometers above the origin, better practice is to set this
1348
+ value at the maximum height above the ground that any human-made
1349
+ structure can reasonably obtain. For this reason, the default is
1350
+ set to 1000 meters or roughly the height of the Burj tower.
1351
+ raise_exception: Boolean to note whether a ValueError should be raised
1352
+ if a Room composed entirely of AirBoundaries is found. (Default: True).
1353
+ detailed: Boolean for whether the returned object is a detailed list of
1354
+ dicts with error info or a string with a message. (Default: False).
1355
+
1356
+ Returns:
1357
+ A string with the message or a list with a dictionary if detailed is True.
1358
+ """
1359
+ detailed = False if raise_exception else detailed
1360
+ # get the maximum elevation of all the rooms
1361
+ conv_fac = conversion_factor_to_meters(self.host.units)
1362
+ max_elev_model = max_elevation / conv_fac
1363
+ room_elevations = tuple(room.max.z for room in self.host.rooms)
1364
+ max_bldg_elev = max(room_elevations) if len(room_elevations) != 0 else 0
1365
+
1366
+ # if the maximum elevation was exceeded, then report the issue
1367
+ if max_bldg_elev > max_elev_model:
1368
+ msg = 'The building height is currently {} meters above the ground ' \
1369
+ 'given the Z values of the coordinates.\nThis is above the ' \
1370
+ 'maximum recommended height of {} meters.'.format(
1371
+ int(max_bldg_elev * conv_fac), max_elevation)
1372
+ if detailed:
1373
+ error_dict = {
1374
+ 'type': 'ValidationError',
1375
+ 'code': '020101',
1376
+ 'error_type': 'Building Height Exceeds Max Elevation',
1377
+ 'extension_type': 'Energy',
1378
+ 'element_type': 'Building',
1379
+ 'element_id': [self.host.identifier],
1380
+ 'element_name': [self.host.display_name],
1381
+ 'message': msg
1382
+ }
1383
+ return [error_dict]
1384
+ else:
1385
+ if raise_exception:
1386
+ raise ValueError(msg)
1387
+ return msg
1388
+ return [] if detailed else ''
1389
+
1390
+ def check_interior_constructions_reversed(
1391
+ self, raise_exception=True, detailed=False):
1392
+ """Check that all interior constructions are in reversed order for paired faces.
1393
+
1394
+ Note that, if there are missing adjacencies in the model, the message from
1395
+ this method will simply note this fact without reporting on mis-matched layers.
1396
+
1397
+ Args:
1398
+ raise_exception: Boolean to note whether a ValueError should be raised
1399
+ if mis-matched interior construction layers are found. (Default: True).
1400
+ detailed: Boolean for whether the returned object is a detailed list of
1401
+ dicts with error info or a string with a message. (Default: False).
1402
+
1403
+ Returns:
1404
+ A string with the message or a list with a dictionary if detailed is True.
1405
+ """
1406
+ detailed = False if raise_exception else detailed
1407
+ # first gather all interior faces in the model and their adjacent object
1408
+ adj_constr, base_objs, adj_ids = [], [], []
1409
+ for face in self.host.faces:
1410
+ if isinstance(face.boundary_condition, Surface):
1411
+ const = face.properties.energy.construction
1412
+ if not isinstance(const, AirBoundaryConstruction):
1413
+ adj_constr.append(face.properties.energy.construction)
1414
+ base_objs.append(face)
1415
+ adj_ids.append(face.boundary_condition.boundary_condition_object)
1416
+ # next, get the adjacent objects
1417
+ try:
1418
+ adj_faces = self.host.faces_by_identifier(adj_ids)
1419
+ except ValueError as e: # the model has missing adjacencies
1420
+ if detailed: # the user will get a more detailed error in honeybee-core
1421
+ return []
1422
+ else:
1423
+ msg = 'Matching adjacent constructions could not be verified because ' \
1424
+ 'of missing adjacencies in the model. \n{}'.format(e)
1425
+ if raise_exception:
1426
+ raise ValueError(msg)
1427
+ return msg
1428
+ # loop through the adjacent face pairs and report if materials are not matched
1429
+ full_msgs, reported_items = [], set()
1430
+ for adj_c, base_f, adj_f in zip(adj_constr, base_objs, adj_faces):
1431
+ if (base_f.identifier, adj_f.identifier) in reported_items:
1432
+ continue
1433
+ try:
1434
+ rev_mat = tuple(reversed(adj_f.properties.energy.construction.materials))
1435
+ except AttributeError:
1436
+ rev_mat = None
1437
+ if not adj_c.materials == rev_mat:
1438
+ f_msg = 'Face "{}" with construction "{}" does not have material ' \
1439
+ 'layers matching in reversed order with its adjacent pair "{}" '\
1440
+ 'with construction "{}".'.format(
1441
+ base_f.full_id,
1442
+ base_f.properties.energy.construction.identifier,
1443
+ adj_f.full_id,
1444
+ adj_f.properties.energy.construction.identifier
1445
+ )
1446
+ f_msg = self.host._validation_message_child(
1447
+ f_msg, base_f, detailed, '020201', 'Energy',
1448
+ error_type='Mismatched Adjacent Constructions')
1449
+ if detailed:
1450
+ f_msg['element_id'].append(adj_f.identifier)
1451
+ f_msg['element_name'].append(adj_f.display_name)
1452
+ parents = []
1453
+ rel_obj = adj_f
1454
+ while getattr(rel_obj, '_parent', None) is not None:
1455
+ rel_obj = getattr(rel_obj, '_parent')
1456
+ par_dict = {
1457
+ 'parent_type': rel_obj.__class__.__name__,
1458
+ 'id': rel_obj.identifier,
1459
+ 'name': rel_obj.display_name
1460
+ }
1461
+ parents.append(par_dict)
1462
+ f_msg['parents'].append(parents)
1463
+ full_msgs.append(f_msg)
1464
+ reported_items.add((adj_f.identifier, base_f.identifier))
1465
+ full_msg = full_msgs if detailed else '\n'.join(full_msgs)
1466
+ if raise_exception and len(full_msgs) != 0:
1467
+ raise ValueError(full_msg)
1468
+ return full_msg
1469
+
1470
+ def resolve_zones(self):
1471
+ """Resolve properties of Rooms across each zone such that E+ can simulate them.
1472
+
1473
+ This method is intended as a pre-step before translating the model to EnergyPlus
1474
+ or OpenStudio but it will mutate the Rooms of the model. So it is often
1475
+ best to duplicate() the model before running this method such that existing
1476
+ room and zone information is not lost. After running this method, the
1477
+ following will be true.
1478
+
1479
+ 1. All Zone names in the model will use only ASCII characters that are
1480
+ accepted by EnergyPlus.
1481
+
1482
+ 2. If not all Rooms of a zone have the same thermostat setpoint, these will
1483
+ be edited such that the strictest setpoint of the rooms at each timestep
1484
+ governs the thermostat setpoint of the whole zone. So all Rooms in each
1485
+ zone will have their setpoint property re-assigned.
1486
+
1487
+ 3. If not all Rooms of a zone have the same ventilation requirements, these
1488
+ will be edited such that the ventilation requirement of the zone is equal
1489
+ to the sum of the individual room ventilation requirements. To total outdoor
1490
+ flow rates are added across the Rooms, flow-per-floor area gets recomputed using
1491
+ the floor area of each Room, ACH flow rates get recomputed using the volume
1492
+ of each Room, and the flow-per-person is set to the highest value of
1493
+ the Rooms in the zone. If all Rooms have ventilation schedules, then these
1494
+ are recomputed such that the highest value governs at each timestep.
1495
+
1496
+ 4. If not all of the Rooms of the zone have the same multiplier, then the
1497
+ highest multiplier of the rooms in the zone sets the multiplier of the zone.
1498
+
1499
+ 5. If not all Rooms of the zone have the same exclude_floor_area value,
1500
+ then the rooms with excluded floor area will be pulled out into separate
1501
+ zones from the ones that have included floor area, which will retain the
1502
+ original zone name.
1503
+
1504
+ Returns:
1505
+ A tuple with two elements.
1506
+
1507
+ - single_zones -- A list of all rooms in the model that have their
1508
+ room identifier equal to their zone identifier, in which case
1509
+ they can be translated without the need to distinguish spaces
1510
+ from zones.
1511
+
1512
+ - zone_dict -- A dictionary with (clean ASCII) identifiers of zones
1513
+ as keys a list of properties defining the zone as values, including
1514
+ the Room objects, zone setpoints, and zone ventilation. These
1515
+ can be used to write zones into the destination format.
1516
+ """
1517
+ # set up variables to be returned from this method
1518
+ single_zones, zone_dict = [], {}
1519
+ zone_ids = {}
1520
+
1521
+ # adjust setpoints, ventilation, multipliers and exclude_floor_area
1522
+ for zone_name, rooms in self.host.zone_dict.items():
1523
+ if len(rooms) == 1: # simple case of one room in the zone
1524
+ room = rooms[0]
1525
+ if rooms[0].identifier == zone_name:
1526
+ single_zones.append(rooms[0])
1527
+ continue # room can be written without the need for space vs. zone
1528
+ zone_id = clean_and_number_ep_string(zone_name, zone_ids)
1529
+ room.zone = zone_id
1530
+ ceil_hgt = room.geometry.max.z - room.geometry.min.z
1531
+ inc_flr = 'No' if room.exclude_floor_area else 'Yes'
1532
+ z_prop = (room.multiplier, ceil_hgt, room.volume, room.floor_area, inc_flr)
1533
+ set_pt = room.properties.energy.setpoint
1534
+ vent = room.properties.energy.ventilation
1535
+ else: # resolve properties across the rooms of the zone
1536
+ zone_id = clean_and_number_ep_string(zone_name, zone_ids)
1537
+ # first determine whether zone must be split for excluded floor areas
1538
+ if all(not r.exclude_floor_area for r in rooms):
1539
+ inc_flr = 'Yes'
1540
+ elif all(r.exclude_floor_area for r in rooms):
1541
+ inc_flr = 'No'
1542
+ else: # split off excluded rooms into separate zones
1543
+ inc_flr, ex_r_i = 'Yes', []
1544
+ for i, r in enumerate(rooms):
1545
+ if r.exclude_floor_area:
1546
+ ex_r_i.append(i)
1547
+ r.zone = None # remove the room from the zone
1548
+ single_zones.append(r)
1549
+ for ex_i in reversed(ex_r_i):
1550
+ rooms.pop(ex_i)
1551
+ # determine the other zone geometry properties
1552
+ min_z = min(r.min.z for r in rooms)
1553
+ max_z = max(r.max.z for r in rooms)
1554
+ ceil_hgt = max_z - min_z
1555
+ mult = max(r.multiplier for r in rooms)
1556
+ vol = sum(r.volume for r in rooms)
1557
+ flr_area = sum(r.floor_area for r in rooms)
1558
+ z_prop = (mult, ceil_hgt, vol, flr_area, inc_flr)
1559
+ # determine the setpoint
1560
+ setpoints = [r.properties.energy.setpoint for r in rooms]
1561
+ setpoints = [s for s in setpoints if s is not None]
1562
+ if len(setpoints) == 0:
1563
+ set_pt = None # no setpoint object to be created
1564
+ elif len(setpoints) == 1:
1565
+ set_pt = setpoints[0] # no need to create a new setpoint object
1566
+ else:
1567
+ setpoints = list(set(setpoints))
1568
+ if len(setpoints) == 1:
1569
+ set_pt = setpoints[0] # no need to create a new setpoint object
1570
+ else:
1571
+ set_pt = setpoints[0].strictest(
1572
+ '{}_SetPt'.format(zone_id), setpoints)
1573
+ # determine the ventilation
1574
+ new_vent = False
1575
+ vents = [r.properties.energy.ventilation for r in rooms]
1576
+ if all(v is None for v in vents):
1577
+ vent = None
1578
+ elif len(set(vents)) == 1 and vents[0].flow_per_zone == 0.0:
1579
+ vent = vents[0] # no need to make a new custom ventilation object
1580
+ else:
1581
+ new_vent = True
1582
+ v_obj = [v for v in vents if v is not None][0]
1583
+ vent = v_obj.combine_room_ventilations(
1584
+ '{}_Vent'.format(zone_id), rooms)
1585
+ # edit the rooms so that the setpoint and ventilation are consistent
1586
+ for i, room in enumerate(rooms):
1587
+ room.zone = zone_id
1588
+ room.properties.energy.setpoint = set_pt
1589
+ if new_vent:
1590
+ room.properties.energy.ventilation = vent
1591
+ if room.identifier == zone_id:
1592
+ room.identifier = '{}_Space{}'.format(room.identifier, i)
1593
+
1594
+ # add to the dictionary of zone objects to be created
1595
+ zone_dict[zone_id] = (rooms, z_prop, set_pt, vent)
1596
+
1597
+ # return the list of single zones and the zone_dict
1598
+ return single_zones, zone_dict
1599
+
1600
+ def sync_detailed_hvac_ids(self, room_map):
1601
+ """Sync room identifiers in DetailedHVAC with rooms that had their IDs changed.
1602
+
1603
+ This is useful after running the Model.reset_ids() command to ensure that
1604
+ the bi-directional Room references between DetailedHVAC and Honeybee Rooms
1605
+ is correct.
1606
+
1607
+ Args:
1608
+ room_map: A dictionary that relates the original Rooms identifiers (keys)
1609
+ to the new identifiers (values) of the Rooms in the Model.
1610
+ """
1611
+ for hvac in self.hvacs:
1612
+ if isinstance(hvac, DetailedHVAC):
1613
+ hvac.sync_room_ids(room_map)
1614
+
1615
+ def reset_resource_ids(
1616
+ self, reset_materials=True, reset_constructions=True,
1617
+ reset_construction_sets=True, reset_schedules=True, reset_programs=True):
1618
+ """Reset the identifiers of energy resource objects in this Model.
1619
+
1620
+ Note that this method may have unintended consequences if the resources
1621
+ assigned to this Model instance are also being used by another Model
1622
+ instance that exists in the current Python session. In this case,
1623
+ running this method will result in the resource identifiers of the
1624
+ other Model also being reset.
1625
+
1626
+ This method is useful when human-readable names are needed when the model
1627
+ is exported to other formats like IDF and OSM. Cases of duplicate IDs
1628
+ resulting from non-unique names will be resolved by adding integers
1629
+ to the ends of the new IDs that are derived from the name.
1630
+
1631
+ Args:
1632
+ reset_materials: Boolean to note whether the IDs of all materials in
1633
+ the model should be reset or kept. (Default: True).
1634
+ reset_constructions: Boolean to note whether the IDs of all constructions
1635
+ in the model should be reset or kept. (Default: True).
1636
+ reset_construction_sets: Boolean to note whether the IDs of all construction
1637
+ sets in the model should be reset or kept. (Default: True).
1638
+ reset_schedules: Boolean to note whether the IDs of all schedules
1639
+ in the model should be reset or kept. (Default: True).
1640
+ reset_programs: Boolean to note whether the IDs of all program
1641
+ types in the model should be reset or kept. (Default: True).
1642
+
1643
+ Returns:
1644
+ A dictionary with the original identifiers of resources as keys and the
1645
+ edited resource objects as values. This can be used to set the identifiers
1646
+ of the objects back to the original value after this method has been
1647
+ run and any other routines have been performed. This will help prevent
1648
+ unintended consequences of changing the resource identifiers in the
1649
+ global resource library or in other Models that may be using the same
1650
+ resource object instances.
1651
+ """
1652
+ # set up the dictionaries used to check for uniqueness
1653
+ res_func = clean_and_number_ep_string
1654
+ mat_dict, con_dict, con_set_dict = {}, {}, {}
1655
+ sch_dict, sch_day_dict, prog_dict = {}, {}, {}
1656
+ ppl_dict, lgt_dict, equip_dict, hw_dict, inf_dict, vent_dict, spt_dict = \
1657
+ {}, {}, {}, {}, {}, {}, {}
1658
+ resource_map = {}
1659
+
1660
+ # change the identifiers of the materials
1661
+ if reset_materials:
1662
+ for mat in self.materials:
1663
+ mat.unlock()
1664
+ resource_map[mat.identifier] = mat
1665
+ mat.identifier = res_func(mat.display_name, mat_dict)
1666
+ mat.lock()
1667
+
1668
+ # change the identifiers of the constructions
1669
+ if reset_constructions:
1670
+ for con in self.constructions:
1671
+ con.unlock()
1672
+ resource_map[con.identifier] = con
1673
+ con.identifier = res_func(con.display_name, con_dict)
1674
+ con.lock()
1675
+
1676
+ # change the identifiers of the construction_sets
1677
+ if reset_construction_sets:
1678
+ for cs in self.construction_sets:
1679
+ cs.unlock()
1680
+ resource_map[cs.identifier] = cs
1681
+ cs.identifier = res_func(cs.display_name, con_set_dict)
1682
+ cs.lock()
1683
+
1684
+ # change the identifiers of the schedules
1685
+ if reset_schedules:
1686
+ sch_skip = ('Seated Adult Activity', 'HumidNoLimit', 'DeHumidNoLimit')
1687
+ for sch in self.schedules:
1688
+ if sch.identifier in sch_skip:
1689
+ continue
1690
+ sch.unlock()
1691
+ sch.identifier = res_func(sch.display_name, sch_dict)
1692
+ resource_map[sch.identifier] = sch
1693
+ if isinstance(sch, ScheduleRuleset):
1694
+ for day_sch in sch.day_schedules:
1695
+ day_sch.unlock()
1696
+ resource_map[day_sch.identifier] = day_sch
1697
+ day_sch.identifier = res_func(day_sch.display_name, sch_day_dict)
1698
+ day_sch.lock()
1699
+ sch.lock()
1700
+
1701
+ # change the identifiers of the program
1702
+ if reset_programs:
1703
+ for prg in self.program_types:
1704
+ prg.unlock()
1705
+ resource_map[prg.identifier] = prg
1706
+ prg.identifier = res_func(prg.display_name, prog_dict)
1707
+ if prg.people is not None:
1708
+ resource_map[prg.people.identifier] = prg.people
1709
+ prg.people.identifier = res_func(prg.people.display_name, ppl_dict)
1710
+ if prg.lighting is not None:
1711
+ resource_map[prg.lighting.identifier] = prg.lighting
1712
+ prg.lighting.identifier = \
1713
+ res_func(prg.lighting.display_name, lgt_dict)
1714
+ if prg.electric_equipment is not None:
1715
+ resource_map[prg.electric_equipment.identifier] = \
1716
+ prg.electric_equipment
1717
+ prg.electric_equipment.identifier = \
1718
+ res_func(prg.electric_equipment.display_name, equip_dict)
1719
+ if prg.gas_equipment is not None:
1720
+ resource_map[prg.gas_equipment.identifier] = \
1721
+ prg.gas_equipment
1722
+ prg.gas_equipment.identifier = \
1723
+ res_func(prg.gas_equipment.display_name, equip_dict)
1724
+ if prg.service_hot_water is not None:
1725
+ resource_map[prg.service_hot_water.identifier] = \
1726
+ prg.service_hot_water
1727
+ prg.service_hot_water.identifier = \
1728
+ res_func(prg.service_hot_water.display_name, hw_dict)
1729
+ if prg.infiltration is not None:
1730
+ resource_map[prg.infiltration.identifier] = prg.infiltration
1731
+ prg.infiltration.identifier = \
1732
+ res_func(prg.infiltration.display_name, inf_dict)
1733
+ if prg.ventilation is not None:
1734
+ resource_map[prg.ventilation.identifier] = prg.ventilation
1735
+ prg.ventilation.identifier = \
1736
+ res_func(prg.ventilation.display_name, vent_dict)
1737
+ if prg.setpoint is not None:
1738
+ resource_map[prg.setpoint.identifier] = prg.setpoint
1739
+ prg.setpoint.identifier = \
1740
+ res_func(prg.setpoint.display_name, spt_dict)
1741
+ prg.lock()
1742
+
1743
+ return resource_map
1744
+
1745
+ @staticmethod
1746
+ def restore_resource_ids(resource_map):
1747
+ """Restore the identifiers of resource objects after resetting them.
1748
+
1749
+ This can be used to set the identifiers of the objects back to the original
1750
+ value after the reset_resource_ids() method was called. This will help prevent
1751
+ unintended consequences of changing the resource identifiers in the
1752
+ global resource library or in other Models that may be using the same
1753
+ resource object instances.
1754
+
1755
+ Args:
1756
+ resource_map: A dictionary with the original identifiers of resources
1757
+ as keys and the edited resource objects as values. This type of
1758
+ dictionary is output from the reset_resource_ids method.
1759
+ """
1760
+ for orignal_id, res_obj in resource_map.items():
1761
+ res_obj.unlock()
1762
+ res_obj.identifier = orignal_id
1763
+ res_obj.lock()
1764
+
1765
+ def apply_properties_from_dict(self, data):
1766
+ """Apply the energy properties of a dictionary to the host Model of this object.
1767
+
1768
+ Args:
1769
+ data: A dictionary representation of an entire honeybee-core Model.
1770
+ Note that this dictionary must have ModelEnergyProperties in order
1771
+ for this method to successfully apply the energy properties.
1772
+ """
1773
+ assert 'energy' in data['properties'], \
1774
+ 'Dictionary possesses no ModelEnergyProperties.'
1775
+ _, constructions, construction_sets, _, schedules, program_types, hvacs, shws = \
1776
+ self.load_properties_from_dict(data)
1777
+
1778
+ # collect lists of energy property dictionaries
1779
+ room_e_dicts, face_e_dicts, shd_e_dicts, ap_e_dicts, dr_e_dicts = \
1780
+ model_extension_dicts(data, 'energy', [], [], [], [], [])
1781
+
1782
+ # apply energy properties to objects using the energy property dictionaries
1783
+ for room, r_dict in zip(self.host.rooms, room_e_dicts):
1784
+ if r_dict is not None:
1785
+ room.properties.energy.apply_properties_from_dict(
1786
+ r_dict, construction_sets, program_types, hvacs, shws,
1787
+ schedules, constructions)
1788
+ for face, f_dict in zip(self.host.faces, face_e_dicts):
1789
+ if f_dict is not None:
1790
+ face.properties.energy.apply_properties_from_dict(f_dict, constructions)
1791
+ for aperture, a_dict in zip(self.host.apertures, ap_e_dicts):
1792
+ if a_dict is not None:
1793
+ aperture.properties.energy.apply_properties_from_dict(
1794
+ a_dict, constructions)
1795
+ for door, d_dict in zip(self.host.doors, dr_e_dicts):
1796
+ if d_dict is not None:
1797
+ door.properties.energy.apply_properties_from_dict(
1798
+ d_dict, constructions)
1799
+ all_shades = self.host.shades + self.host._shade_meshes
1800
+ for shade, s_dict in zip(all_shades, shd_e_dicts):
1801
+ if s_dict is not None:
1802
+ shade.properties.energy.apply_properties_from_dict(
1803
+ s_dict, constructions, schedules)
1804
+
1805
+ energy_prop = data['properties']['energy']
1806
+ # re-serialize the ventilation_simulation_control
1807
+ if 'ventilation_simulation_control' in energy_prop and \
1808
+ energy_prop['ventilation_simulation_control'] is not None:
1809
+ self.ventilation_simulation_control = \
1810
+ VentilationSimulationControl.from_dict(
1811
+ energy_prop['ventilation_simulation_control'])
1812
+ # re-serialize the electric_load_center
1813
+ if 'electric_load_center' in energy_prop and \
1814
+ energy_prop['electric_load_center'] is not None:
1815
+ self.electric_load_center = \
1816
+ ElectricLoadCenter.from_dict(energy_prop['electric_load_center'])
1817
+
1818
+ def to_dict(self):
1819
+ """Return Model energy properties as a dictionary."""
1820
+ base = {'energy': {'type': 'ModelEnergyProperties'}}
1821
+
1822
+ # add all materials, constructions and construction sets to the dictionary
1823
+ schs = self._add_constr_type_objs_to_dict(base)
1824
+
1825
+ # add all schedule type limits, schedules, program types, hvacs, shws to the dict
1826
+ self._add_sched_type_objs_to_dict(base, schs)
1827
+
1828
+ # add ventilation_simulation_control
1829
+ base['energy']['ventilation_simulation_control'] = \
1830
+ self.ventilation_simulation_control.to_dict()
1831
+ # add electric_load_center
1832
+ base['energy']['electric_load_center'] = self.electric_load_center.to_dict()
1833
+
1834
+ return base
1835
+
1836
+ def add_autocal_properties_to_dict(self, data, exclude_hole_verts=False):
1837
+ """Add auto-calculated energy properties to a Model dictionary.
1838
+
1839
+ This includes Room volumes, ceiling heights, and (in the case of Faces
1840
+ and Shades with holes) vertices. These properties are used
1841
+ in translation from Honeybee to OpenStudio to ensure that the properties
1842
+ in Honeybee are aligned with those in OSM, IDF, and gbXML.
1843
+
1844
+ Note that the host model must be in Meters in order for the
1845
+ added properties to be in the correct units system.
1846
+
1847
+ Args:
1848
+ data: A dictionary representation of an entire honeybee-core Model.
1849
+ exclude_hole_verts: A boolean to note whether hole vertices should
1850
+ be excluded from the translation. This is useful only for
1851
+ very particular interfaces that do not support holes like
1852
+ TRACE. (Default: False).
1853
+ """
1854
+ # check that the host model is in meters and add geometry properties
1855
+ assert self.host.units == 'Meters', 'Host model units must be Meters to use ' \
1856
+ 'add_autocal_properties_to_dict. Not {}.'.format(self.host.units)
1857
+ # process rooms to add volume, ceiling height, and faces with holes
1858
+ if len(self.host.rooms) != 0:
1859
+ for room, room_dict in zip(self.host.rooms, data['rooms']):
1860
+ room_dict['ceiling_height'] = room.geometry.max.z - room.geometry.min.z
1861
+ room_dict['volume'] = room.volume
1862
+ self._add_shade_vertices(room, room_dict)
1863
+ for face, face_dict in zip(room._faces, room_dict['faces']):
1864
+ self._add_shade_vertices(face, face_dict)
1865
+ if face.geometry.has_holes and not exclude_hole_verts:
1866
+ ul_verts = face.upper_left_vertices
1867
+ if isinstance(face.boundary_condition, Surface):
1868
+ # check if the first vertex is the upper-left vertex
1869
+ pt1, found_i = ul_verts[0], False
1870
+ for pt in ul_verts[1:]:
1871
+ if pt == pt1:
1872
+ found_i = True
1873
+ break
1874
+ if found_i: # reorder the vertices to have boundary first
1875
+ ul_verts = reversed(ul_verts)
1876
+ face_dict['geometry']['vertices'] = \
1877
+ [[round(v, 3) for v in pt] for pt in ul_verts]
1878
+ if len(face._apertures) != 0:
1879
+ for ap, ap_dict in zip(face._apertures, face_dict['apertures']):
1880
+ self._add_shade_vertices(ap, ap_dict)
1881
+ if ap.properties.energy.construction.has_frame:
1882
+ ap_dict['properties']['energy']['frame'] = \
1883
+ ap.properties.energy.construction.frame.identifier
1884
+ if len(face._doors) != 0:
1885
+ for dr, dr_dict in zip(face._doors, face_dict['doors']):
1886
+ self._add_shade_vertices(dr, dr_dict)
1887
+ if dr.properties.energy.construction.has_frame:
1888
+ dr_dict['properties']['energy']['frame'] = \
1889
+ dr.properties.energy.construction.frame.identifier
1890
+ # process orphaned shades to add geometries with holes
1891
+ if len(self.host._orphaned_shades) != 0:
1892
+ for shd, shd_d in zip(self.host._orphaned_shades, data['orphaned_shades']):
1893
+ if shd.geometry.has_holes:
1894
+ shd_d['geometry']['vertices'] = \
1895
+ [pt.to_array() for pt in shd.upper_left_vertices]
1896
+ # process orphaned faces for punched geometry and aperture transmittance
1897
+ trans_orphaned = False
1898
+ if len(self.host._orphaned_faces) != 0:
1899
+ for shd, shd_d in zip(self.host._orphaned_faces, data['orphaned_faces']):
1900
+ shd_d['geometry']['vertices'] = \
1901
+ [pt.to_array() for pt in shd.punched_vertices]
1902
+ if 'apertures' in shd_d:
1903
+ for ap, ap_d in zip(shd._apertures, shd_d['apertures']):
1904
+ ap_con = ap.properties.energy.construction
1905
+ ap_d['transmit'] = str(round(ap_con.solar_transmittance, 3))
1906
+ trans_orphaned = True
1907
+ if 'doors' in shd_d:
1908
+ for dr, dr_d in zip(shd._doors, shd_d['doors']):
1909
+ if dr.is_glass:
1910
+ dr_con = dr.properties.energy.construction
1911
+ dr_d['transmit'] = str(round(dr_con.solar_transmittance, 3))
1912
+ trans_orphaned = True
1913
+ # add auto-generated transmittance schedules for transparent orphaned objects
1914
+ if len(self.host._orphaned_apertures) != 0:
1915
+ trans_orphaned = True
1916
+ ap_iter = zip(self.host._orphaned_apertures, data['orphaned_apertures'])
1917
+ for ap, ap_d in ap_iter:
1918
+ ap_con = ap.properties.energy.construction
1919
+ ap_d['transmit'] = str(round(ap_con.solar_transmittance, 3))
1920
+ if len(self.host._orphaned_doors) != 0:
1921
+ for dr, dr_d in zip(self.host._orphaned_doors, data['orphaned_doors']):
1922
+ if dr.is_glass:
1923
+ dr_con = dr.properties.energy.construction
1924
+ dr_d['transmit'] = str(round(dr_con.solar_transmittance, 3))
1925
+ trans_orphaned = True
1926
+ # add transmittance schedules to the model if needed
1927
+ if trans_orphaned:
1928
+ for sched in self.orphaned_trans_schedules:
1929
+ data['properties']['energy']['schedules'].append(
1930
+ sched.to_dict(abridged=True))
1931
+ # if there are any detailed HVACs, add the path to the ironbug installation
1932
+ # this is the only reliable way to pass the path to the honeybee-openstudio gem
1933
+ if folders.ironbug_exe is not None:
1934
+ for hvac_dict in data['properties']['energy']['hvacs']:
1935
+ if hvac_dict['type'] == 'DetailedHVAC':
1936
+ hvac_dict['ironbug_exe'] = folders.ironbug_exe
1937
+ # CWM: there is a very weird bug in OpenStudio
1938
+ # program types cannot have the same name as the model (Building) in OpenStudio
1939
+ model_id = data['identifier']
1940
+ for prog in data['properties']['energy']['program_types']:
1941
+ if prog['identifier'] == model_id:
1942
+ data['identifier'] = '{}1'.format(data['identifier'])
1943
+ if 'display_name' in data and data['display_name'] is not None:
1944
+ data['display_name'] = '{}1'.format(data['display_name'])
1945
+ break
1946
+
1947
+ @staticmethod
1948
+ def _add_shade_vertices(obj, obj_dict):
1949
+ if len(obj._outdoor_shades) != 0:
1950
+ for shd, shd_dict in zip(obj._outdoor_shades, obj_dict['outdoor_shades']):
1951
+ if shd.geometry.has_holes:
1952
+ shd_dict['geometry']['vertices'] = \
1953
+ [pt.to_array() for pt in shd.upper_left_vertices]
1954
+
1955
+ def simplify_window_constructions_in_dict(self, data):
1956
+ """Convert all window constructions in a model dictionary to SimpleGlazSys.
1957
+
1958
+ This is useful when translating to gbXML and other formats that do not
1959
+ support layered window constructions.
1960
+
1961
+ Args:
1962
+ data: A dictionary representation of an entire honeybee-core Model.
1963
+ """
1964
+ # add the window construction u-factors
1965
+ mat_dicts = data['properties']['energy']['materials']
1966
+ con_dicts = data['properties']['energy']['constructions']
1967
+ w_cons = (WindowConstruction, WindowConstructionShade, WindowConstructionDynamic)
1968
+ for i, con in enumerate(self.constructions):
1969
+ if isinstance(con, WindowConstruction):
1970
+ new_con = con.to_simple_construction()
1971
+ elif isinstance(con, WindowConstructionShade):
1972
+ new_con = con.window_construction.to_simple_construction()
1973
+ elif isinstance(con, WindowConstructionDynamic):
1974
+ new_con = con.constructions[0].to_simple_construction()
1975
+ if isinstance(con, w_cons):
1976
+ con_dicts[i] = new_con.to_dict(abridged=True)
1977
+ mat_dicts.append(new_con.materials[0].to_dict())
1978
+
1979
+ def duplicate(self, new_host=None):
1980
+ """Get a copy of this object.
1981
+
1982
+ Args:
1983
+ new_host: A new Model object that hosts these properties.
1984
+ If None, the properties will be duplicated with the same host.
1985
+ """
1986
+ _host = new_host or self._host
1987
+ return ModelEnergyProperties(
1988
+ _host, self._ventilation_simulation_control.duplicate(),
1989
+ self.electric_load_center.duplicate())
1990
+
1991
+ @staticmethod
1992
+ def load_properties_from_dict(data, skip_invalid=False):
1993
+ """Load model energy properties of a dictionary to Python objects.
1994
+
1995
+ Loaded objects include Materials, Constructions, ConstructionSets,
1996
+ ScheduleTypeLimits, Schedules, and ProgramTypes.
1997
+
1998
+ The function is called when re-serializing a Model object from a dictionary
1999
+ to load honeybee_energy objects into their Python object form before
2000
+ applying them to the Model geometry.
2001
+
2002
+ Args:
2003
+ data: A dictionary representation of an entire honeybee-core Model.
2004
+ Note that this dictionary must have ModelEnergyProperties in order
2005
+ for this method to successfully load the energy properties.
2006
+ skip_invalid: A boolean to note whether objects that cannot be loaded
2007
+ should be ignored (True) or whether an exception should be raised
2008
+ about the invalid object (False). (Default: False).
2009
+
2010
+ Returns:
2011
+ A tuple with eight elements
2012
+
2013
+ - materials -- A dictionary with identifiers of materials as keys
2014
+ and Python material objects as values.
2015
+
2016
+ - constructions -- A dictionary with identifiers of constructions
2017
+ as keys and Python construction objects as values.
2018
+
2019
+ - construction_sets -- A dictionary with identifiers of construction
2020
+ sets as keys and Python construction set objects as values.
2021
+
2022
+ - schedule_type_limits -- A dictionary with identifiers of schedule
2023
+ type limits as keys and Python schedule type limit objects as values.
2024
+
2025
+ - schedules -- A dictionary with identifiers of schedules as keys
2026
+ and Python schedule objects as values.
2027
+
2028
+ - program_types -- A dictionary with identifiers of program types
2029
+ as keys and Python program type objects as values.
2030
+
2031
+ - hvacs -- A dictionary with identifiers of HVAC systems as keys
2032
+ and Python HVACSystem objects as values.
2033
+
2034
+ - shws -- A dictionary with identifiers of SHW systems as keys
2035
+ and Python SHWSystem objects as values.
2036
+ """
2037
+ assert 'energy' in data['properties'], \
2038
+ 'Dictionary possesses no ModelEnergyProperties.'
2039
+
2040
+ # process all schedule type limits in the ModelEnergyProperties dictionary
2041
+ schedule_type_limits = {}
2042
+ if 'schedule_type_limits' in data['properties']['energy'] and \
2043
+ data['properties']['energy']['schedule_type_limits'] is not None:
2044
+ for t_lim in data['properties']['energy']['schedule_type_limits']:
2045
+ try:
2046
+ schedule_type_limits[t_lim['identifier']] = \
2047
+ ScheduleTypeLimit.from_dict(t_lim)
2048
+ except Exception as e:
2049
+ if not skip_invalid:
2050
+ invalid_dict_error(t_lim, e)
2051
+
2052
+ # process all schedules in the ModelEnergyProperties dictionary
2053
+ schedules = {}
2054
+ if 'schedules' in data['properties']['energy'] and \
2055
+ data['properties']['energy']['schedules'] is not None:
2056
+ for sched in data['properties']['energy']['schedules']:
2057
+ try:
2058
+ if sched['type'] in SCHEDULE_TYPES:
2059
+ schedules[sched['identifier']] = dict_to_schedule(sched)
2060
+ else:
2061
+ schedules[sched['identifier']] = dict_abridged_to_schedule(
2062
+ sched, schedule_type_limits)
2063
+ except Exception as e:
2064
+ if not skip_invalid:
2065
+ invalid_dict_error(sched, e)
2066
+
2067
+ # process all materials in the ModelEnergyProperties dictionary
2068
+ materials = {}
2069
+ if 'materials' in data['properties']['energy'] and \
2070
+ data['properties']['energy']['materials'] is not None:
2071
+ for mat in data['properties']['energy']['materials']:
2072
+ try:
2073
+ materials[mat['identifier']] = dict_to_material(mat)
2074
+ except Exception as e:
2075
+ if not skip_invalid:
2076
+ invalid_dict_error(mat, e)
2077
+
2078
+ # process all constructions in the ModelEnergyProperties dictionary
2079
+ constructions = {}
2080
+ if 'constructions' in data['properties']['energy'] and \
2081
+ data['properties']['energy']['constructions'] is not None:
2082
+ for cnstr in data['properties']['energy']['constructions']:
2083
+ try:
2084
+ if cnstr['type'] in CONSTRUCTION_TYPES:
2085
+ constructions[cnstr['identifier']] = dict_to_construction(cnstr)
2086
+ else:
2087
+ constructions[cnstr['identifier']] = \
2088
+ dict_abridged_to_construction(cnstr, materials, schedules)
2089
+ except Exception as e:
2090
+ if not skip_invalid:
2091
+ invalid_dict_error(cnstr, e)
2092
+
2093
+ # process all construction sets in the ModelEnergyProperties dictionary
2094
+ construction_sets = {}
2095
+ if 'construction_sets' in data['properties']['energy'] and \
2096
+ data['properties']['energy']['construction_sets'] is not None:
2097
+ for c_set in data['properties']['energy']['construction_sets']:
2098
+ try:
2099
+ if c_set['type'] == 'ConstructionSet':
2100
+ construction_sets[c_set['identifier']] = \
2101
+ ConstructionSet.from_dict(c_set)
2102
+ else:
2103
+ construction_sets[c_set['identifier']] = \
2104
+ ConstructionSet.from_dict_abridged(c_set, constructions)
2105
+ except Exception as e:
2106
+ if not skip_invalid:
2107
+ invalid_dict_error(c_set, e)
2108
+
2109
+ # process all ProgramType in the ModelEnergyProperties dictionary
2110
+ program_types = {}
2111
+ if 'program_types' in data['properties']['energy'] and \
2112
+ data['properties']['energy']['program_types'] is not None:
2113
+ for p_typ in data['properties']['energy']['program_types']:
2114
+ try:
2115
+ if p_typ['type'] == 'ProgramType':
2116
+ program_types[p_typ['identifier']] = ProgramType.from_dict(p_typ)
2117
+ else:
2118
+ program_types[p_typ['identifier']] = \
2119
+ ProgramType.from_dict_abridged(p_typ, schedules)
2120
+ except Exception as e:
2121
+ if not skip_invalid:
2122
+ invalid_dict_error(p_typ, e)
2123
+
2124
+ # process all HVAC systems in the ModelEnergyProperties dictionary
2125
+ hvacs = {}
2126
+ if 'hvacs' in data['properties']['energy'] and \
2127
+ data['properties']['energy']['hvacs'] is not None:
2128
+ for hvac in data['properties']['energy']['hvacs']:
2129
+ hvac_class = HVAC_TYPES_DICT[hvac['type'].replace('Abridged', '')]
2130
+ try:
2131
+ hvacs[hvac['identifier']] = \
2132
+ hvac_class.from_dict_abridged(hvac, schedules)
2133
+ except Exception as e:
2134
+ if not skip_invalid:
2135
+ invalid_dict_error(hvac, e)
2136
+
2137
+ # process all SHW systems in the ModelEnergyProperties dictionary
2138
+ shws = {}
2139
+ if 'shws' in data['properties']['energy'] and \
2140
+ data['properties']['energy']['shws'] is not None:
2141
+ for shw in data['properties']['energy']['shws']:
2142
+ try:
2143
+ shws[shw['identifier']] = SHWSystem.from_dict(shw)
2144
+ except Exception as e:
2145
+ if not skip_invalid:
2146
+ invalid_dict_error(shw, e)
2147
+
2148
+ return materials, constructions, construction_sets, schedule_type_limits, \
2149
+ schedules, program_types, hvacs, shws
2150
+
2151
+ @staticmethod
2152
+ def dump_properties_to_dict(
2153
+ materials=None, constructions=None, construction_sets=None,
2154
+ schedule_type_limits=None, schedules=None, program_types=None,
2155
+ hvacs=None, shws=None):
2156
+ """Get a ModelEnergyProperties dictionary from arrays of Python objects.
2157
+
2158
+ Args:
2159
+ materials: A list or tuple of energy material objects.
2160
+ constructions: A list or tuple of construction objects.
2161
+ construction_sets: A list or tuple of construction set objects.
2162
+ schedule_type_limits: A list or tuple of schedule type limit objects.
2163
+ schedules: A list or tuple of schedule objects.
2164
+ program_types: A list or tuple of program type objects.
2165
+ hvacs: A list or tuple of HVACSystem objects.
2166
+ shws: A list or tuple of SHWSystem objects.
2167
+
2168
+ Returns:
2169
+ data: A dictionary representation of ModelEnergyProperties. Note that
2170
+ all objects in this dictionary will follow the abridged schema.
2171
+ """
2172
+ # process the input schedules and type limits
2173
+ type_lim = [] if schedule_type_limits is None else list(schedule_type_limits)
2174
+ scheds = [] if schedules is None else list(schedules)
2175
+
2176
+ # process the program types
2177
+ all_progs, misc_p_scheds = [], []
2178
+ if program_types is not None:
2179
+ for program in program_types:
2180
+ all_progs.append(program)
2181
+ misc_p_scheds.extend(program.schedules)
2182
+
2183
+ # process the materials, constructions, and construction sets
2184
+ all_m = [] if materials is None else list(materials)
2185
+ all_c = [] if constructions is None else list(constructions)
2186
+ all_con_sets = [] if construction_sets is None else list(construction_sets)
2187
+ for con_set in all_con_sets:
2188
+ all_c.extend(con_set.modified_constructions)
2189
+
2190
+ # get schedules assigned in constructions
2191
+ misc_c_scheds = []
2192
+ for constr in all_c:
2193
+ if isinstance(constr, (WindowConstructionShade, WindowConstructionDynamic)):
2194
+ misc_c_scheds.append(constr.schedule)
2195
+ elif isinstance(constr, AirBoundaryConstruction):
2196
+ misc_c_scheds.append(constr.air_mixing_schedule)
2197
+
2198
+ # process the HVAC and SHW systems
2199
+ all_hvac = [] if hvacs is None else list(hvacs)
2200
+ all_shw = [] if shws is None else list(shws)
2201
+ misc_hvac_scheds = []
2202
+ for hvac_obj in all_hvac:
2203
+ misc_hvac_scheds.extend(hvac_obj.schedules)
2204
+
2205
+ # get sets of unique objects
2206
+ all_scheds = set(scheds + misc_p_scheds + misc_c_scheds + misc_hvac_scheds)
2207
+ sched_tl = [sch.schedule_type_limit for sch in all_scheds
2208
+ if sch.schedule_type_limit is not None]
2209
+ all_typ_lim = set(type_lim + sched_tl)
2210
+ all_cons = set(all_c)
2211
+ misc_c_mats = []
2212
+ for constr in all_cons:
2213
+ try:
2214
+ misc_c_mats.extend(constr.materials)
2215
+ if constr.has_frame:
2216
+ misc_c_mats.append(constr.frame)
2217
+ if constr.has_shade:
2218
+ if constr.is_switchable_glazing:
2219
+ misc_c_mats.append(constr.switched_glass_material)
2220
+ if constr.shade_location == 'Between':
2221
+ materials.append(constr.window_construction.materials[-2])
2222
+ except AttributeError: # not a construction with materials
2223
+ pass
2224
+ all_mats = set(all_m + misc_c_mats)
2225
+
2226
+ # add all object dictionaries into one object
2227
+ data = {'type': 'ModelEnergyProperties'}
2228
+ data['schedule_type_limits'] = [tl.to_dict() for tl in all_typ_lim]
2229
+ data['schedules'] = [sch.to_dict(abridged=True) for sch in all_scheds]
2230
+ data['program_types'] = [pro.to_dict(abridged=True) for pro in all_progs]
2231
+ data['materials'] = [m.to_dict() for m in all_mats]
2232
+ data['constructions'] = []
2233
+ for con in all_cons:
2234
+ try:
2235
+ data['constructions'].append(con.to_dict(abridged=True))
2236
+ except TypeError: # no abridged option
2237
+ data['constructions'].append(con.to_dict())
2238
+ data['construction_sets'] = [cs.to_dict(abridged=True) for cs in all_con_sets]
2239
+ data['hvacs'] = [hv.to_dict(abridged=True) for hv in all_hvac]
2240
+ data['shws'] = [sw.to_dict(abridged=True) for sw in all_shw]
2241
+ return data
2242
+
2243
+ @staticmethod
2244
+ def reset_resource_ids_in_dict(
2245
+ data, add_uuid=False, reset_materials=True, reset_constructions=True,
2246
+ reset_construction_sets=True, reset_schedules=True, reset_programs=True):
2247
+ """Reset the identifiers of energy resource objects in a Model dictionary.
2248
+
2249
+ This is useful when human-readable names are needed when the model is
2250
+ exported to other formats like IDF and OSM and the uniqueness of the
2251
+ identifiers is less of a concern.
2252
+
2253
+ Args:
2254
+ data: A dictionary representation of an entire honeybee-core Model.
2255
+ Note that this dictionary must have ModelEnergyProperties in order
2256
+ for this method to successfully edit the energy properties.
2257
+ add_uuid: Boolean to note whether newly-generated resource object IDs
2258
+ should be derived only from a cleaned display_name (False) or
2259
+ whether this new ID should also have a unique set of 8 characters
2260
+ appended to it to guarantee uniqueness. (Default: False).
2261
+ reset_materials: Boolean to note whether the IDs of all materials in
2262
+ the model should be reset or kept. (Default: True).
2263
+ reset_constructions: Boolean to note whether the IDs of all constructions
2264
+ in the model should be reset or kept. (Default: True).
2265
+ reset_construction_sets: Boolean to note whether the IDs of all construction
2266
+ sets in the model should be reset or kept. (Default: True).
2267
+ reset_schedules: Boolean to note whether the IDs of all schedules
2268
+ in the model should be reset or kept. (Default: True).
2269
+ reset_programs: Boolean to note whether the IDs of all program
2270
+ types in the model should be reset or kept. (Default: True).
2271
+
2272
+ Returns:
2273
+ A new Model dictionary with the resource identifiers reset. All references
2274
+ to the reset resources will be correct and valid in the resulting dictionary,
2275
+ assuming that the input is valid.
2276
+ """
2277
+ model = Model.from_dict(data)
2278
+ materials, constructions, construction_sets, schedule_type_limits, \
2279
+ schedules, program_types, _, _ = \
2280
+ model.properties.energy.load_properties_from_dict(data)
2281
+ res_func = clean_and_id_ep_string if add_uuid else clean_ep_string
2282
+
2283
+ # change the identifiers of the materials
2284
+ if reset_materials:
2285
+ model_mats = set()
2286
+ for mat in model.properties.energy.materials:
2287
+ mat.unlock()
2288
+ old_id, new_id = mat.identifier, res_func(mat.display_name)
2289
+ mat.identifier = new_id
2290
+ materials[old_id].unlock()
2291
+ materials[old_id].identifier = new_id
2292
+ model_mats.add(old_id)
2293
+ for old_id, mat in materials.items():
2294
+ if old_id not in model_mats:
2295
+ mat.unlock()
2296
+ mat.identifier = res_func(mat.display_name)
2297
+
2298
+ # change the identifiers of the constructions
2299
+ if reset_constructions:
2300
+ model_cons = set()
2301
+ for con in model.properties.energy.constructions:
2302
+ con.unlock()
2303
+ old_id, new_id = con.identifier, res_func(con.display_name)
2304
+ con.identifier = new_id
2305
+ constructions[old_id].unlock()
2306
+ constructions[old_id].identifier = new_id
2307
+ model_cons.add(old_id)
2308
+ for old_id, con in constructions.items():
2309
+ if old_id not in model_cons:
2310
+ con.unlock()
2311
+ con.identifier = res_func(con.display_name)
2312
+
2313
+ # change the identifiers of the construction_sets
2314
+ if reset_construction_sets:
2315
+ model_cs = set()
2316
+ for cs in model.properties.energy.construction_sets:
2317
+ cs.unlock()
2318
+ old_id, new_id = cs.identifier, res_func(cs.display_name)
2319
+ cs.identifier = new_id
2320
+ construction_sets[old_id].unlock()
2321
+ construction_sets[old_id].identifier = new_id
2322
+ model_cs.add(old_id)
2323
+ for old_id, cs in construction_sets.items():
2324
+ if old_id not in model_cs:
2325
+ cs.unlock()
2326
+ cs.identifier = res_func(cs.display_name)
2327
+
2328
+ # change the identifiers of the schedules
2329
+ if reset_schedules:
2330
+ sch_skip = ('Seated Adult Activity', 'HumidNoLimit', 'DeHumidNoLimit')
2331
+ model_sch = set()
2332
+ for sch in model.properties.energy.schedules:
2333
+ if sch.identifier in sch_skip:
2334
+ schedules[sch.identifier] = sch
2335
+ model_sch.add(sch.identifier)
2336
+ continue
2337
+ sch.unlock()
2338
+ old_id, new_id = sch.identifier, res_func(sch.display_name)
2339
+ sch.identifier = new_id
2340
+ schedules[old_id].unlock()
2341
+ schedules[old_id].identifier = new_id
2342
+ model_sch.add(old_id)
2343
+ if isinstance(sch, ScheduleRuleset):
2344
+ for day_sch in sch.day_schedules:
2345
+ day_sch.identifier = res_func(day_sch.display_name)
2346
+ for day_sch in schedules[old_id].day_schedules:
2347
+ day_sch.identifier = res_func(day_sch.display_name)
2348
+ for old_id, sch in schedules.items():
2349
+ if old_id not in model_sch:
2350
+ sch.unlock()
2351
+ sch.identifier = res_func(sch.display_name)
2352
+ if isinstance(sch, ScheduleRuleset):
2353
+ for day_sch in schedules[old_id].day_schedules:
2354
+ day_sch.identifier = res_func(day_sch.display_name)
2355
+
2356
+ # change the identifiers of the program
2357
+ if reset_programs:
2358
+ model_prg = set()
2359
+ for prg in model.properties.energy.program_types:
2360
+ prg.unlock()
2361
+ old_id, new_id = prg.identifier, res_func(prg.display_name)
2362
+ prg.identifier = new_id
2363
+ program_types[old_id].unlock()
2364
+ program_types[old_id].identifier = new_id
2365
+ model_prg.add(old_id)
2366
+ for old_id, prg in program_types.items():
2367
+ if old_id not in model_prg:
2368
+ prg.unlock()
2369
+ prg.identifier = res_func(prg.display_name)
2370
+
2371
+ # create the model dictionary and update any unreferenced resources
2372
+ model_dict = model.to_dict()
2373
+ me_props = model_dict['properties']['energy']
2374
+ me_props['materials'] = [mat.to_dict() for mat in materials.values()]
2375
+ me_props['constructions'] = []
2376
+ for cnst in constructions.values():
2377
+ try:
2378
+ me_props['constructions'].append(cnst.to_dict(abridged=True))
2379
+ except TypeError: # ShadeConstruction
2380
+ me_props['constructions'].append(cnst.to_dict())
2381
+ me_props['construction_sets'] = \
2382
+ [cs.to_dict(abridged=True) for cs in construction_sets.values()]
2383
+ me_props['schedule_type_limits'] = \
2384
+ [stl.to_dict() for stl in schedule_type_limits.values()]
2385
+ me_props['schedules'] = [sc.to_dict(abridged=True) for sc in schedules.values()]
2386
+ me_props['program_types'] = \
2387
+ [p.to_dict(abridged=True) for p in program_types.values()]
2388
+ return model_dict
2389
+
2390
+ def _add_constr_type_objs_to_dict(self, base):
2391
+ """Add materials, constructions and construction sets to a base dictionary.
2392
+
2393
+ Args:
2394
+ base: A base dictionary for a Honeybee Model.
2395
+
2396
+ Returns:
2397
+ A list of of schedules that are assigned to the constructions.
2398
+ """
2399
+ # add the global construction set to the dictionary
2400
+ gs = self.global_construction_set.to_dict(abridged=True, none_for_defaults=False)
2401
+ gs['type'] = 'GlobalConstructionSet'
2402
+ del gs['identifier']
2403
+ g_constr = self.global_construction_set.constructions_unique
2404
+ g_materials = []
2405
+ for constr in g_constr:
2406
+ try:
2407
+ g_materials.extend(constr.materials)
2408
+ except AttributeError:
2409
+ pass # ShadeConstruction or AirBoundaryConstruction
2410
+ gs['context_construction'] = generic_context.identifier
2411
+ gs['constructions'] = [generic_context.to_dict()]
2412
+ for cnst in g_constr:
2413
+ try:
2414
+ gs['constructions'].append(cnst.to_dict(abridged=True))
2415
+ except TypeError: # ShadeConstruction
2416
+ gs['constructions'].append(cnst.to_dict())
2417
+ gs['materials'] = [mat.to_dict() for mat in set(g_materials)]
2418
+ base['energy']['global_construction_set'] = gs
2419
+
2420
+ # add all ConstructionSets to the dictionary
2421
+ base['energy']['construction_sets'] = []
2422
+ construction_sets = self.construction_sets
2423
+ for cnstr_set in construction_sets:
2424
+ base['energy']['construction_sets'].append(cnstr_set.to_dict(abridged=True))
2425
+
2426
+ # add all unique Constructions to the dictionary
2427
+ room_constrs = []
2428
+ for cnstr_set in construction_sets:
2429
+ room_constrs.extend(cnstr_set.modified_constructions_unique)
2430
+ mass_constrs = []
2431
+ for room in self.host.rooms:
2432
+ for int_mass in room.properties.energy._internal_masses:
2433
+ constr = int_mass.construction
2434
+ if not self._instance_in_array(constr, mass_constrs):
2435
+ mass_constrs.append(constr)
2436
+ all_constrs = room_constrs + mass_constrs + \
2437
+ self.face_constructions + self.shade_constructions
2438
+ constructions = list(set(all_constrs))
2439
+ base['energy']['constructions'] = []
2440
+ for cnst in constructions:
2441
+ try:
2442
+ base['energy']['constructions'].append(cnst.to_dict(abridged=True))
2443
+ except TypeError: # ShadeConstruction
2444
+ base['energy']['constructions'].append(cnst.to_dict())
2445
+
2446
+ # add all unique Materials to the dictionary
2447
+ materials = []
2448
+ for cnstr in constructions:
2449
+ try:
2450
+ materials.extend(cnstr.materials)
2451
+ if cnstr.has_frame:
2452
+ materials.append(cnstr.frame)
2453
+ if isinstance(cnstr, WindowConstructionShade):
2454
+ if cnstr.is_switchable_glazing:
2455
+ materials.append(cnstr.switched_glass_material)
2456
+ if cnstr.shade_location == 'Between':
2457
+ materials.append(cnstr.window_construction.materials[-2])
2458
+ except AttributeError:
2459
+ pass # ShadeConstruction
2460
+ base['energy']['materials'] = [mat.to_dict() for mat in set(materials)]
2461
+
2462
+ # extract all of the schedules from the constructions
2463
+ schedules = []
2464
+ for constr in constructions:
2465
+ if isinstance(constr, AirBoundaryConstruction):
2466
+ self._check_and_add_schedule(constr.air_mixing_schedule, schedules)
2467
+ elif isinstance(constr, WindowConstructionShade):
2468
+ if constr.schedule is not None:
2469
+ self._check_and_add_schedule(constr.schedule, schedules)
2470
+ elif isinstance(constr, WindowConstructionDynamic):
2471
+ self._check_and_add_schedule(constr.schedule, schedules)
2472
+ return schedules
2473
+
2474
+ def _add_sched_type_objs_to_dict(self, base, schs):
2475
+ """Add type limits, schedules, program types, hvacs, shws to a base dictionary.
2476
+
2477
+ Args:
2478
+ base: A base dictionary for a Honeybee Model.
2479
+ schs: A list of additional schedules to be serialized to the
2480
+ base dictionary.
2481
+ """
2482
+ # add all unique hvacs to the dictionary
2483
+ hvacs = self.hvacs
2484
+ base['energy']['hvacs'] = []
2485
+ for hvac in hvacs:
2486
+ base['energy']['hvacs'].append(hvac.to_dict(abridged=True))
2487
+
2488
+ # add all unique shws to the dictionary
2489
+ base['energy']['shws'] = [shw.to_dict() for shw in self.shws]
2490
+
2491
+ # add all unique program types to the dictionary
2492
+ program_types = self.program_types
2493
+ base['energy']['program_types'] = []
2494
+ for p_type in program_types:
2495
+ base['energy']['program_types'].append(p_type.to_dict(abridged=True))
2496
+
2497
+ # add all unique Schedules to the dictionary
2498
+ p_type_scheds = []
2499
+ for p_type in program_types:
2500
+ for sched in p_type.schedules:
2501
+ self._check_and_add_schedule(sched, p_type_scheds)
2502
+ hvac_scheds = []
2503
+ for hvac in hvacs:
2504
+ for sched in hvac.schedules:
2505
+ self._check_and_add_schedule(sched, hvac_scheds)
2506
+ all_scheds = hvac_scheds + p_type_scheds + self.room_schedules + \
2507
+ self.shade_schedules + schs
2508
+ schedules = list(set(all_scheds))
2509
+ base['energy']['schedules'] = []
2510
+ for sched in schedules:
2511
+ base['energy']['schedules'].append(sched.to_dict(abridged=True))
2512
+
2513
+ # add all unique ScheduleTypeLimits to the dictionary
2514
+ type_limits = []
2515
+ for sched in schedules:
2516
+ t_lim = sched.schedule_type_limit
2517
+ if t_lim is not None and not self._instance_in_array(t_lim, type_limits):
2518
+ type_limits.append(t_lim)
2519
+ base['energy']['schedule_type_limits'] = \
2520
+ [s_typ.to_dict() for s_typ in set(type_limits)]
2521
+
2522
+ def _check_and_add_obj_construction(self, obj, constructions):
2523
+ """Check if a construction is assigned to an object and add it to a list."""
2524
+ constr = obj.properties.energy._construction
2525
+ if constr is not None:
2526
+ if not self._instance_in_array(constr, constructions):
2527
+ constructions.append(constr)
2528
+
2529
+ def _check_and_add_obj_construction_inc_parent(self, obj, constructions):
2530
+ """Check if a construction is assigned to an object and add it to a list."""
2531
+ constr = obj.properties.energy.construction
2532
+ if not self._instance_in_array(constr, constructions):
2533
+ constructions.append(constr)
2534
+
2535
+ def _check_and_add_shade_schedule(self, obj, schedules):
2536
+ """Check if a schedule is assigned to a shade and add it to a list."""
2537
+ sched = obj.properties.energy._transmittance_schedule
2538
+ if sched is not None:
2539
+ if not self._instance_in_array(sched, schedules):
2540
+ schedules.append(sched)
2541
+
2542
+ def _check_and_add_schedule(self, sched, schedules):
2543
+ """Check if a schedule is in a list and add it if not."""
2544
+ if not self._instance_in_array(sched, schedules):
2545
+ schedules.append(sched)
2546
+
2547
+ def _check_and_add_obj_transmit(self, hb_obj, transmittances):
2548
+ """Check and add the transmittance of a honeybee object to a set."""
2549
+ constr = hb_obj.properties.energy.construction
2550
+ transmittances.add(round(constr.solar_transmittance, 3))
2551
+
2552
+ def _assign_room_modifier_sets(self, unique_mod_sets):
2553
+ """Assign modifier sets to rooms using a dictionary of unique modifier sets."""
2554
+ for room in self._host.rooms:
2555
+ room.properties.radiance.modifier_set = \
2556
+ unique_mod_sets[room.properties.energy.construction_set.identifier]
2557
+
2558
+ def _assign_face_modifiers(self, unique_mods):
2559
+ """Assign modifiers to faces, apertures, doors and shades using a dictionary."""
2560
+ for room in self._host.rooms:
2561
+ for face in room.faces: # check Face constructions
2562
+ self._assign_obj_modifier_shade(face, unique_mods)
2563
+ for ap in face.apertures: # check Aperture constructions
2564
+ self._assign_obj_modifier_shade(ap, unique_mods)
2565
+ for dr in face.doors: # check Door constructions
2566
+ self._assign_obj_modifier_shade(dr, unique_mods)
2567
+ for shade in room.shades:
2568
+ self._assign_obj_modifier(shade, unique_mods)
2569
+ for face in self.host.orphaned_faces: # check orphaned Face constructions
2570
+ self._assign_obj_modifier_shade(face, unique_mods)
2571
+ for ap in face.apertures: # check Aperture constructions
2572
+ self._assign_obj_modifier_shade(ap, unique_mods)
2573
+ for dr in face.doors: # check Door constructions
2574
+ self._assign_obj_modifier_shade(dr, unique_mods)
2575
+ for ap in self.host.orphaned_apertures: # check orphaned Aperture constructions
2576
+ self._assign_obj_modifier_shade(ap, unique_mods)
2577
+ for dr in self.host.orphaned_doors: # check orphaned Door constructions
2578
+ self._assign_obj_modifier_shade(dr, unique_mods)
2579
+ for shade in self.host.orphaned_shades:
2580
+ self._assign_obj_modifier(shade, unique_mods)
2581
+ for sm in self.host.shade_meshes:
2582
+ self._assign_obj_modifier(sm, unique_mods)
2583
+
2584
+ def _assign_obj_modifier_shade(self, obj, unique_mods):
2585
+ """Check if an object or child shades have a unique constr and assign a modifier.
2586
+ """
2587
+ self._assign_obj_modifier(obj, unique_mods)
2588
+ for shade in obj.shades:
2589
+ self._assign_obj_modifier(shade, unique_mods)
2590
+
2591
+ @staticmethod
2592
+ def _always_on_schedule():
2593
+ """Get the Always On schedule."""
2594
+ return always_on
2595
+
2596
+ @staticmethod
2597
+ def _assign_obj_modifier(obj, unique_mods):
2598
+ """Check if an object has a unique construction and assign a modifier."""
2599
+ if obj.properties.energy._construction is not None:
2600
+ obj.properties.radiance.modifier = \
2601
+ unique_mods[obj.properties.energy._construction.identifier]
2602
+
2603
+ @staticmethod
2604
+ def _instance_in_array(object_instance, object_array):
2605
+ """Check if a specific object instance is already in an array.
2606
+
2607
+ This can be much faster than `if object_instance in object_array`
2608
+ when you expect to be testing a lot of the same instance of an object for
2609
+ inclusion in an array since the builtin method uses an == operator to
2610
+ test inclusion.
2611
+ """
2612
+ for val in object_array:
2613
+ if val is object_instance:
2614
+ return True
2615
+ return False
2616
+
2617
+ def ToString(self):
2618
+ return self.__repr__()
2619
+
2620
+ def __repr__(self):
2621
+ return 'Model Energy Properties: [host: {}]'.format(self.host.display_name)