honeybee-energy 1.116.106__py3-none-any.whl

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