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,497 @@
1
+ # coding=utf-8
2
+ """Complete definition of people in a simulation, including schedule and load."""
3
+ from __future__ import division
4
+
5
+ from honeybee._lockable import lockable
6
+ from honeybee.typing import float_in_range, float_positive, clean_and_id_ep_string
7
+ from honeybee.altnumber import autocalculate
8
+
9
+ from ._base import _LoadBase
10
+ from ..schedule.ruleset import ScheduleRuleset
11
+ from ..schedule.fixedinterval import ScheduleFixedInterval
12
+ from ..reader import parse_idf_string
13
+ from ..writer import generate_idf_string
14
+ from ..properties.extension import PeopleProperties
15
+
16
+ import honeybee_energy.lib.schedules as _sched_lib
17
+
18
+
19
+ @lockable
20
+ class People(_LoadBase):
21
+ """A complete definition of people, including schedules and load.
22
+
23
+ Args:
24
+ identifier: Text string for a unique People ID. Must be < 100 characters
25
+ and not contain any EnergyPlus special characters. This will be used to
26
+ identify the object across a model and in the exported IDF.
27
+ people_per_area: A numerical value for the number of people per square
28
+ meter of floor area.
29
+ occupancy_schedule: A ScheduleRuleset or ScheduleFixedInterval for the
30
+ occupancy over the course of the year. The type of this schedule
31
+ should be Fractional and the fractional values will get multiplied by
32
+ the people_per_area to yield a complete occupancy profile.
33
+ activity_schedule: A ScheduleRuleset or ScheduleFixedInterval for the
34
+ activity of the occupants over the course of the year. The type of
35
+ this schedule should be ActivityLevel and the values of the schedule equal
36
+ to the number of Watts given off by an individual person in the room.
37
+ If None, a default constant schedule with 120 Watts per person
38
+ will be used, which is typical of awake, adult humans who are seated.
39
+ radiant_fraction: A number between 0 and 1 for the fraction of the
40
+ sensible heat given off by people that is radiant (as opposed to
41
+ convective). (Default: 0.3).
42
+ latent_fraction: A number between 0 and 1 for the fraction of the heat
43
+ given off by people that is latent (as opposed to sensible). This
44
+ input can also be an Autocalculate object, which will automatically
45
+ estimate the latent fraction based on the occupant's activity level.
46
+ (Default: autocalculate).
47
+
48
+ Properties:
49
+ * identifier
50
+ * display_name
51
+ * people_per_area
52
+ * area_per_person
53
+ * occupancy_schedule
54
+ * activity_schedule
55
+ * radiant_fraction
56
+ * latent_fraction
57
+ * user_data
58
+ """
59
+ __slots__ = ('_people_per_area', '_occupancy_schedule', '_activity_schedule',
60
+ '_radiant_fraction', '_latent_fraction')
61
+
62
+ def __init__(self, identifier, people_per_area, occupancy_schedule,
63
+ activity_schedule=None,
64
+ radiant_fraction=0.3, latent_fraction=autocalculate):
65
+ """Initialize People."""
66
+ _LoadBase.__init__(self, identifier)
67
+ self.people_per_area = people_per_area
68
+ self.occupancy_schedule = occupancy_schedule
69
+ self.activity_schedule = activity_schedule
70
+ self.radiant_fraction = radiant_fraction
71
+ self.latent_fraction = latent_fraction
72
+ self._properties = PeopleProperties(self)
73
+
74
+ @property
75
+ def people_per_area(self):
76
+ """Get or set the number of people per square meter of floor area."""
77
+ return self._people_per_area
78
+
79
+ @people_per_area.setter
80
+ def people_per_area(self, value):
81
+ self._people_per_area = float_positive(value, 'people per area')
82
+
83
+ @property
84
+ def area_per_person(self):
85
+ """Get or set the number of square meters of floor area per person."""
86
+ return 1 / self._people_per_area if self._people_per_area != 0 else 0
87
+
88
+ @area_per_person.setter
89
+ def area_per_person(self, value):
90
+ if float(value) != 0:
91
+ self._people_per_area = 1 / float_positive(value, 'area per person')
92
+ else:
93
+ self._people_per_area = 0
94
+
95
+ @property
96
+ def occupancy_schedule(self):
97
+ """Get or set a ScheduleRuleset or ScheduleFixedInterval for the occupancy."""
98
+ return self._occupancy_schedule
99
+
100
+ @occupancy_schedule.setter
101
+ def occupancy_schedule(self, value):
102
+ assert isinstance(value, (ScheduleRuleset, ScheduleFixedInterval)), \
103
+ 'Expected ScheduleRuleset or ScheduleFixedInterval for People ' \
104
+ 'occupancy_schedule. Got {}.'.format(type(value))
105
+ self._check_fractional_schedule_type(value, 'Occupancy')
106
+ value.lock() # lock editing in case schedule has multiple references
107
+ self._occupancy_schedule = value
108
+
109
+ @property
110
+ def activity_schedule(self):
111
+ """Get or set a ScheduleRuleset or ScheduleFixedInterval for the occupancy."""
112
+ return self._activity_schedule
113
+
114
+ @activity_schedule.setter
115
+ def activity_schedule(self, value):
116
+ if value is not None:
117
+ assert isinstance(value, (ScheduleRuleset, ScheduleFixedInterval)), \
118
+ 'Expected ScheduleRuleset or ScheduleFixedInterval for People' \
119
+ ' activity_schedule. Got {}.'.format(type(value))
120
+ self._check_activity_schedule_type(value)
121
+ value.lock() # lock editing in case schedule has multiple references
122
+ self._activity_schedule = value
123
+ else:
124
+ self._activity_schedule = _sched_lib.seated_activity
125
+
126
+ @property
127
+ def radiant_fraction(self):
128
+ """Get or set the radiant fraction of sensible heat given off by people."""
129
+ return self._radiant_fraction
130
+
131
+ @radiant_fraction.setter
132
+ def radiant_fraction(self, value):
133
+ self._radiant_fraction = float_in_range(
134
+ value, 0.0, 1.0, 'people radiant fraction')
135
+
136
+ @property
137
+ def latent_fraction(self):
138
+ """Get or set the fraction of the heat given off by people that is latent."""
139
+ return self._latent_fraction
140
+
141
+ @latent_fraction.setter
142
+ def latent_fraction(self, value):
143
+ if value == autocalculate:
144
+ self._latent_fraction = autocalculate
145
+ else:
146
+ self._latent_fraction = float_in_range(
147
+ value, 0.0, 1.0, 'people latent fraction')
148
+
149
+ def diversify(self, count, occupancy_stdev=20, schedule_offset=1, timestep=1,
150
+ schedule_indices=None):
151
+ """Get an array of diversified People derived from this "average" one.
152
+
153
+ Approximately 2/3 of the schedules in the output objects will be offset
154
+ from the mean by the input schedule_offset (1/3 ahead and another 1/3 behind).
155
+
156
+ Args:
157
+ count: An positive integer for the number of diversified objects to
158
+ generate from this mean object.
159
+ occupancy_stdev: A number between 0 and 100 for the percent of the
160
+ occupancy people_per_area representing one standard deviation
161
+ of diversification from the mean. (Default 20 percent).
162
+ schedule_offset: A positive integer for the number of timesteps at which
163
+ the occupancy schedule of the resulting objects will be shifted - roughly
164
+ 1/3 of the objects ahead and another 1/3 behind. (Default: 1).
165
+ timestep: An integer for the number of timesteps per hour at which the
166
+ shifting is occurring. This must be a value between 1 and 60, which
167
+ is evenly divisible by 60. 1 indicates that each step is an hour
168
+ while 60 indicates that each step is a minute. (Default: 1).
169
+ schedule_indices: An optional list of integers from 0 to 2 with a length
170
+ equal to the input count, which will be used to set whether a given
171
+ schedule is behind (0), ahead (2), or the same (1). This can be
172
+ used to coordinate schedules across diversified programs. If None
173
+ a random list of integers will be generated. (Default: None).
174
+ """
175
+ # generate shifted schedules and a gaussian distribution of people_per_area
176
+ occ_schs = self._shift_schedule(
177
+ self.occupancy_schedule, schedule_offset, timestep)
178
+ stdev = self.people_per_area * (occupancy_stdev / 100)
179
+ new_loads, sch_ints = self._gaussian_values(count, self.people_per_area, stdev)
180
+ sch_ints = sch_ints if schedule_indices is None else schedule_indices
181
+
182
+ # generate the new objects and return them
183
+ new_objects = []
184
+ for load_val, sch_int in zip(new_loads, sch_ints):
185
+ new_obj = self.duplicate()
186
+ new_obj.identifier = clean_and_id_ep_string(self.identifier)
187
+ new_obj.people_per_area = load_val
188
+ new_obj.occupancy_schedule = occ_schs[sch_int]
189
+ new_objects.append(new_obj)
190
+ return new_objects
191
+
192
+ @classmethod
193
+ def from_idf(cls, idf_string, schedule_dict):
194
+ """Create an People object from an EnergyPlus IDF text string.
195
+
196
+ Note that the People idf_string must use the 'people per zone floor area'
197
+ method in order to be successfully imported.
198
+
199
+ Args:
200
+ idf_string: A text string fully describing an EnergyPlus people definition.
201
+ schedule_dict: A dictionary with schedule identifiers as keys and honeybee
202
+ schedule objects as values (either ScheduleRuleset or
203
+ ScheduleFixedInterval). These will be used to assign the schedules to
204
+ the People object.
205
+
206
+ Returns:
207
+ A tuple with four elements
208
+
209
+ - people: A People object loaded from the idf_string.
210
+
211
+ - zone_identifier: The identifier of the zone to which the People
212
+ object should be assigned.
213
+ """
214
+ # check the inputs
215
+ ep_strs = parse_idf_string(idf_string, 'People,')
216
+ assert ep_strs[3].lower() == 'people/area', \
217
+ 'People must use People/Area method to be loaded from IDF to honeybee.'
218
+
219
+ # extract the properties from the string
220
+ lat_fract = autocalculate if ep_strs[8] == '' or \
221
+ ep_strs[8].lower() == 'autocalculate' else 1 - float(ep_strs[8])
222
+ rad_fract = ep_strs[7] if ep_strs[7] != '' else 0.3
223
+
224
+ # extract the schedules from the string
225
+ occ_sched, activity_sched = cls._get_occ_act_schedules_from_dict(
226
+ schedule_dict, ep_strs[2], ep_strs[9])
227
+
228
+ # return the people object and the zone id for the people object
229
+ obj_id = ep_strs[0].split('..')[0]
230
+ zone_id = ep_strs[1]
231
+ people = cls(obj_id, ep_strs[5], occ_sched, activity_sched,
232
+ rad_fract, lat_fract)
233
+ return people, zone_id
234
+
235
+ @classmethod
236
+ def from_dict(cls, data):
237
+ """Create a People object from a dictionary.
238
+
239
+ Note that the dictionary must be a non-abridged version for this classmethod
240
+ to work.
241
+
242
+ Args:
243
+ data: A People dictionary in following the format below.
244
+
245
+ .. code-block:: python
246
+
247
+ {
248
+ "type": 'People',
249
+ "identifier": 'Open_Office_People_005_03_02',
250
+ "display_name": 'Office People',
251
+ "people_per_area": 0.05, # number of people per square meter of floor area
252
+ "occupancy_schedule": {}, # ScheduleRuleset/ScheduleFixedInterval dictionary
253
+ "activity_schedule": {}, # ScheduleRuleset/ScheduleFixedInterval dictionary
254
+ "radiant_fraction": 0.3, # fraction of sensible heat that is radiant
255
+ "latent_fraction": 0.2 # fraction of total heat that is latent
256
+ }
257
+ """
258
+ assert data['type'] == 'People', \
259
+ 'Expected People dictionary. Got {}.'.format(data['type'])
260
+ occ_sched = cls._get_schedule_from_dict(data['occupancy_schedule'])
261
+ act_sched = cls._get_schedule_from_dict(data['activity_schedule']) if \
262
+ 'activity_schedule' in data and data['activity_schedule'] is not None \
263
+ else None
264
+ rad_fract, lat_fract = cls._optional_dict_keys(data)
265
+ new_obj = cls(data['identifier'], data['people_per_area'], occ_sched, act_sched,
266
+ rad_fract, lat_fract)
267
+ if 'display_name' in data and data['display_name'] is not None:
268
+ new_obj.display_name = data['display_name']
269
+ if 'user_data' in data and data['user_data'] is not None:
270
+ new_obj.user_data = data['user_data']
271
+ if 'properties' in data and data['properties'] is not None:
272
+ new_obj.properties._load_extension_attr_from_dict(data['properties'])
273
+ return new_obj
274
+
275
+ @classmethod
276
+ def from_dict_abridged(cls, data, schedule_dict):
277
+ """Create a People object from an abridged dictionary.
278
+
279
+ Args:
280
+ data: A PeopleAbridged dictionary in following the format below.
281
+ schedule_dict: A dictionary with schedule identifiers as keys and
282
+ honeybee schedule objects as values (either ScheduleRuleset or
283
+ ScheduleFixedInterval). These will be used to assign the schedules
284
+ to the People object.
285
+
286
+ .. code-block:: python
287
+
288
+ {
289
+ "type": "PeopleAbridged",
290
+ "identifier": 'Open_Office_People_005_03_02',
291
+ "display_name": 'Office People',
292
+ "people_per_area": 0.05, # number of people per square meter of floor area
293
+ "occupancy_schedule": "Office Occupancy", # Schedule identifier
294
+ "activity_schedule": "Office Activity", # Schedule identifier
295
+ "radiant_fraction": 0.3, # fraction of sensible heat that is radiant
296
+ "latent_fraction": 0.2 # fraction of total heat that is latent
297
+ }
298
+ """
299
+ assert data['type'] == 'PeopleAbridged', \
300
+ 'Expected PeopleAbridged dictionary. Got {}.'.format(data['type'])
301
+ act_sch_id = data['activity_schedule'] if 'activity_schedule' in data and \
302
+ data['activity_schedule'] is not None else ''
303
+ occ_sched, activity_sched = cls._get_occ_act_schedules_from_dict(
304
+ schedule_dict, data['occupancy_schedule'], act_sch_id)
305
+ rad_fract, lat_fract = cls._optional_dict_keys(data)
306
+ new_obj = cls(data['identifier'], data['people_per_area'], occ_sched,
307
+ activity_sched, rad_fract, lat_fract)
308
+ if 'display_name' in data and data['display_name'] is not None:
309
+ new_obj.display_name = data['display_name']
310
+ if 'user_data' in data and data['user_data'] is not None:
311
+ new_obj.user_data = data['user_data']
312
+ if 'properties' in data and data['properties'] is not None:
313
+ new_obj.properties._load_extension_attr_from_dict(data['properties'])
314
+ return new_obj
315
+
316
+ def to_idf(self, zone_identifier):
317
+ """IDF string representation of People object.
318
+
319
+ Note that this method only outputs a single string for the People object and,
320
+ to write everything needed to describe the object into an IDF, this object's
321
+ occupancy_schedule and activity_schedule must also be written. This is done
322
+ to give more control over the export process since you typically want to check
323
+ whether these schedules are used by multiple People objects and write the
324
+ schedule into the IDF only once.
325
+
326
+ Args:
327
+ zone_identifier: Text for the zone identifier that the People object
328
+ is assigned to.
329
+
330
+ .. code-block:: shell
331
+
332
+ People,
333
+ Kitchen_ZN_1_FLR_1, !- Name
334
+ Kitchen_ZN_1_FLR_1, !- Zone or ZoneList or Space or SpaceList Name
335
+ BLDG_OCC_SCH, !- Number of People Schedule Name
336
+ People/Area, !- Number of People Calculation Method
337
+ , !- Number of People
338
+ 0.05, !- People per Zone Floor Area
339
+ , !- Zone Floor Area per Person
340
+ 0.3000, !- Fraction Radiant
341
+ AUTOCALCULATE, !- Sensible Heat Fraction
342
+ ACTIVITY_SCH; !- Activity Level Schedule Name
343
+ """
344
+ sens_fract = 'autocalculate' if self.latent_fraction == autocalculate else \
345
+ 1 - float(self.latent_fraction)
346
+ values = ('{}..{}'.format(self.identifier, zone_identifier), zone_identifier,
347
+ self.occupancy_schedule.identifier, 'People/Area',
348
+ '', self.people_per_area, '', self.radiant_fraction, sens_fract,
349
+ self.activity_schedule.identifier)
350
+ comments = ('name', 'zone name', 'occupancy schedule name', 'occupancy method',
351
+ 'number of people {ppl}', 'people per floor area {ppl/m2}',
352
+ 'floor area per person {m2/ppl}', 'radiant fraction',
353
+ 'sensible heat fraction', 'activity schedule name')
354
+ return generate_idf_string('People', values, comments)
355
+
356
+ def to_dict(self, abridged=False):
357
+ """People dictionary representation.
358
+
359
+ Args:
360
+ abridged: Boolean to note whether the full dictionary describing the
361
+ object should be returned (False) or just an abridged version (True),
362
+ which only specifies the identifiers of schedules. Default: False.
363
+ """
364
+ base = {'type': 'People'} if not abridged else {'type': 'PeopleAbridged'}
365
+ base['identifier'] = self.identifier
366
+ base['people_per_area'] = self.people_per_area
367
+ base['radiant_fraction'] = self.radiant_fraction
368
+ base['latent_fraction'] = self.latent_fraction if \
369
+ isinstance(self.latent_fraction, float) else self.latent_fraction.to_dict()
370
+ if not abridged:
371
+ base['occupancy_schedule'] = self.occupancy_schedule.to_dict()
372
+ base['activity_schedule'] = self.activity_schedule.to_dict()
373
+ else:
374
+ base['occupancy_schedule'] = self.occupancy_schedule.identifier
375
+ base['activity_schedule'] = self.activity_schedule.identifier
376
+ if self._display_name is not None:
377
+ base['display_name'] = self.display_name
378
+ if self._user_data is not None:
379
+ base['user_data'] = self.user_data
380
+ prop_dict = self.properties.to_dict()
381
+ if prop_dict is not None:
382
+ base['properties'] = prop_dict
383
+ return base
384
+
385
+ @staticmethod
386
+ def average(identifier, peoples, weights=None, timestep_resolution=1):
387
+ """Get a People object that's a weighted average between other People objects.
388
+
389
+ Args:
390
+ identifier: Text string for a unique ID for the new averaged People.
391
+ Must be < 100 characters and not contain any EnergyPlus special
392
+ characters. This will be used to identify the object across a model
393
+ and in the exported IDF.
394
+ peoples: A list of People objects that will be averaged together to make
395
+ a new People.
396
+ weights: An optional list of fractional numbers with the same length
397
+ as the input peoples. These will be used to weight each of the People
398
+ objects in the resulting average. Note that these weights
399
+ can sum to less than 1 in which case the average people_per_area will
400
+ assume 0 for the unaccounted fraction of the weights.
401
+ If None, the objects will be weighted equally. Default: None.
402
+ timestep_resolution: An optional integer for the timestep resolution
403
+ at which the schedules will be averaged. Any schedule details
404
+ smaller than this timestep will be lost in the averaging process.
405
+ Default: 1.
406
+ """
407
+ weights, u_weights = People._check_avg_weights(peoples, weights, 'People')
408
+
409
+ # calculate the average values
410
+ ppl_area = sum([ppl.people_per_area * w for ppl, w in zip(peoples, weights)])
411
+ rad_fract = sum([ppl.radiant_fraction * w for ppl, w in zip(peoples, u_weights)])
412
+ lat_fracts = []
413
+ for i, ppl in enumerate(peoples):
414
+ if ppl.latent_fraction == autocalculate:
415
+ lat_fract = autocalculate
416
+ break
417
+ lat_fracts.append(ppl.latent_fraction * u_weights[i])
418
+ else:
419
+ lat_fract = sum(lat_fracts)
420
+
421
+ # calculate the average schedules
422
+ occ_sched = People._average_schedule(
423
+ '{}_Occ Schedule'.format(identifier),
424
+ [ppl.occupancy_schedule for ppl in peoples], u_weights, timestep_resolution)
425
+ act_sched = People._average_schedule(
426
+ '{}_Act Schedule'.format(identifier),
427
+ [ppl.activity_schedule for ppl in peoples], u_weights, timestep_resolution)
428
+
429
+ # return the averaged people object
430
+ return People(identifier, ppl_area, occ_sched, act_sched, rad_fract, lat_fract)
431
+
432
+ def _check_activity_schedule_type(self, schedule):
433
+ """Check that the type limit of an input schedule is fractional."""
434
+ if schedule.schedule_type_limit is not None:
435
+ t_lim = schedule.schedule_type_limit
436
+ assert t_lim.unit_type == 'ActivityLevel', 'Activity schedule must have a ' \
437
+ 'unit type of ActivityLevel. Got a schedule' \
438
+ ' of unit type [{}].'.format(t_lim.unit_type)
439
+ assert t_lim.lower_limit == 0, 'Activity schedule should have either ' \
440
+ 'no type limit or a lower limit of 0. Got a schedule type with ' \
441
+ 'lower limit [{}].'.format(t_lim.lower_limit)
442
+
443
+ @staticmethod
444
+ def _optional_dict_keys(data):
445
+ """Get the optional keys from a People dictionary."""
446
+ rad_fract = data['radiant_fraction'] if 'radiant_fraction' in data else 0.3
447
+ lat_fract = autocalculate if 'latent_fraction' not in data or \
448
+ data['latent_fraction'] == autocalculate.to_dict() \
449
+ else data['latent_fraction']
450
+ return rad_fract, lat_fract
451
+
452
+ @staticmethod
453
+ def _get_occ_act_schedules_from_dict(schedule_dict, occ_sch_id, act_sch_id):
454
+ """Get schedule objects from a dictionary."""
455
+ try:
456
+ occ_sched = schedule_dict[occ_sch_id]
457
+ except KeyError as e:
458
+ raise ValueError('Failed to find {} in the schedule_dict.'.format(e))
459
+ if act_sch_id == '' or act_sch_id.lower() == 'seated adult activity':
460
+ activity_sched = None
461
+ else:
462
+ try:
463
+ activity_sched = schedule_dict[act_sch_id]
464
+ except KeyError as e:
465
+ raise ValueError(
466
+ 'Failed to find {} in the People schedule_dict.'.format(e)
467
+ )
468
+ return occ_sched, activity_sched
469
+
470
+ def __key(self):
471
+ """A tuple based on the object properties, useful for hashing."""
472
+ return (self.identifier, self.people_per_area, hash(self.occupancy_schedule),
473
+ hash(self.activity_schedule), self.radiant_fraction,
474
+ str(self.latent_fraction))
475
+
476
+ def __hash__(self):
477
+ return hash(self.__key())
478
+
479
+ def __eq__(self, other):
480
+ return isinstance(other, People) and self.__key() == other.__key()
481
+
482
+ def __ne__(self, other):
483
+ return not self.__eq__(other)
484
+
485
+ def __copy__(self):
486
+ new_obj = People(
487
+ self.identifier, self.people_per_area, self.occupancy_schedule,
488
+ self.activity_schedule, self.radiant_fraction, self.latent_fraction)
489
+ new_obj._display_name = self._display_name
490
+ new_obj._user_data = None if self._user_data is None else self._user_data.copy()
491
+ new_obj._properties._duplicate_extension_attr(self._properties)
492
+ return new_obj
493
+
494
+ def __repr__(self):
495
+ return 'People: {} [{} people/m2] [schedule: {}]'.format(
496
+ self.display_name, round(self.people_per_area, 3),
497
+ self.occupancy_schedule.display_name)