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 @@
1
+ """honeybee-energy schedules."""
@@ -0,0 +1,626 @@
1
+ # coding=utf-8
2
+ """Schedule describing a single day."""
3
+ from __future__ import division
4
+
5
+ from .typelimit import ScheduleTypeLimit
6
+ from ..reader import parse_idf_string
7
+ from ..writer import generate_idf_string
8
+
9
+ from honeybee._lockable import lockable
10
+ from honeybee.typing import valid_ep_string, tuple_with_length
11
+
12
+ from ladybug.datacollection import HourlyContinuousCollection
13
+ from ladybug.header import Header
14
+ from ladybug.analysisperiod import AnalysisPeriod
15
+ from ladybug.dt import Date, Time
16
+ from ladybug.datatype.generic import GenericType
17
+
18
+ from collections import deque
19
+ try:
20
+ from collections.abc import Iterable # python < 3.7
21
+ except ImportError:
22
+ from collections import Iterable # python >= 3.8
23
+
24
+
25
+ @lockable
26
+ class ScheduleDay(object):
27
+ """Schedule for a single day.
28
+
29
+ Note that a ScheduleDay cannot be assigned to Rooms, Shades, etc. The ScheduleDay
30
+ must be added to a ScheduleRuleset or a ScheduleRule and then the ScheduleRuleset
31
+ can be applied to such objects.
32
+
33
+ Args:
34
+ identifier: Text string for a unique ScheduleDay ID. Must be < 100 characters
35
+ and not contain any EnergyPlus special characters. This will be used to
36
+ identify the object across a model and in the exported IDF.
37
+ values: A list of floats or integers for the values of the schedule.
38
+ The length of this list must match the length of the times list.
39
+ times: A list of ladybug Time objects with the same length as the input
40
+ values. Each time represents the time of day that the corresponding
41
+ value begins to take effect. For example [0:00, 9:00, 17:00] in
42
+ combination with the values [0, 1, 0] denotes a schedule value of
43
+ 0 from 0:00 to 9:00, a value of 1 from 9:00 to 17:00 and 0 from 17:00
44
+ to the end of the day.
45
+ If this input is None, the default will be a single time at 0:00,
46
+ indicating the `values` input should be a single constant value that
47
+ goes all of the way until the end of the day.
48
+ Note that these times follow a different convention than EnergyPlus,
49
+ which uses "time until" instead of "time of beginning".
50
+ interpolate: Boolean to note whether values in between times should be
51
+ linearly interpolated or whether successive values should take effect
52
+ immediately upon the beginning time corresponding to them. Default: False
53
+
54
+ Properties:
55
+ * identifier
56
+ * display_name
57
+ * times
58
+ * values
59
+ * interpolate
60
+ * is_constant
61
+ """
62
+ __slots__ = ('_identifier', '_display_name', '_values', '_times',
63
+ '_interpolate', '_parent', '_locked')
64
+
65
+ _start_of_day = Time(0, 0)
66
+ VALIDTIMESTEPS = (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60)
67
+
68
+ def __init__(self, identifier, values, times=None, interpolate=False):
69
+ """Initialize Schedule Day."""
70
+ self._locked = False # unlocked by default
71
+ self._parent = None # no parent ScheduleRuleset by default
72
+ self.identifier = identifier
73
+ self._display_name = None
74
+
75
+ # assign the times and values
76
+ if times is None:
77
+ self._times = (self._start_of_day,)
78
+ else:
79
+ if not isinstance(times, tuple):
80
+ try:
81
+ times = tuple(times)
82
+ except (ValueError, TypeError):
83
+ raise TypeError('ScheduleDay times must be iterable.')
84
+ for time in times:
85
+ self._check_time(time)
86
+ self._times = times
87
+ self.values = values
88
+
89
+ # if times are not ordered chronologically, sort them
90
+ if not self._are_chronological(self._times):
91
+ self._times, self._values = zip(*sorted(zip(self._times, self._values)))
92
+
93
+ # ensure that the schedule always starts from 0:00
94
+ assert self._times[0] == self._start_of_day, 'ScheduleDay times must always ' \
95
+ 'start with 0:00. Got {}.'.format(self._times[0])
96
+
97
+ self.interpolate = interpolate
98
+
99
+ @property
100
+ def identifier(self):
101
+ """Get or set a text string for a unique schedule day identifier."""
102
+ return self._identifier
103
+
104
+ @identifier.setter
105
+ def identifier(self, identifier):
106
+ self._identifier = valid_ep_string(identifier, 'schedule day identifier')
107
+
108
+ @property
109
+ def display_name(self):
110
+ """Get or set a string for the object name without any character restrictions.
111
+
112
+ If not set, this will be equal to the identifier.
113
+ """
114
+ if self._display_name is None:
115
+ return self._identifier
116
+ return self._display_name
117
+
118
+ @display_name.setter
119
+ def display_name(self, value):
120
+ if value is not None:
121
+ try:
122
+ value = str(value)
123
+ except UnicodeEncodeError: # Python 2 machine lacking the character set
124
+ pass # keep it as unicode
125
+ self._display_name = value
126
+
127
+ @property
128
+ def values(self):
129
+ """Get or set the schedule's numerical values, which correspond to the times."""
130
+ return self._values
131
+
132
+ @values.setter
133
+ def values(self, values):
134
+ self._values = self._check_values(values)
135
+
136
+ @property
137
+ def times(self):
138
+ """Get or set the Schedule's times, which correspond to the numerical values."""
139
+ return self._times
140
+
141
+ @times.setter
142
+ def times(self, times):
143
+ self._times = self._check_times(times)
144
+
145
+ @property
146
+ def interpolate(self):
147
+ """Get or set a boolean noting whether values should be interpolated."""
148
+ return self._interpolate
149
+
150
+ @interpolate.setter
151
+ def interpolate(self, interpolate):
152
+ self._interpolate = bool(interpolate)
153
+
154
+ @property
155
+ def is_constant(self):
156
+ """Boolean noting whether the schedule is representable with a single value."""
157
+ return len(self) == 1
158
+
159
+ def add_value(self, value, time):
160
+ """Add a value to the schedule along with the time it begins to take effect.
161
+
162
+ Args:
163
+ value: A number for the schedule value.
164
+ time: The ladybug Time object for the time at which the value begins to
165
+ take effect.
166
+ """
167
+ self._check_time(time)
168
+ value = self._check_value(value)
169
+ self._times = self._times + (time,)
170
+ self._values = self._values + (value,)
171
+ if self._times[-1] < self._times[-2]: # ensure times are chronological
172
+ self._times, self._values = zip(*sorted(zip(self._times, self._values)))
173
+
174
+ def remove_value(self, value_index):
175
+ """Remove a value from the schedule by its index.
176
+
177
+ Args:
178
+ value_index: An integer for the index of the value to remove.
179
+ """
180
+ assert len(self._values) > 1, 'ScheduleDay must have at least one value.'
181
+ assert value_index != 0, 'ScheduleDay cannot remove value at index 0.'
182
+ if value_index < 0:
183
+ value_index = len(self._values) + value_index
184
+ self._values = tuple(x for i, x in enumerate(self._values) if i != value_index)
185
+ self._times = tuple(x for i, x in enumerate(self._times) if i != value_index)
186
+
187
+ def remove_value_by_time(self, time):
188
+ """Remove a value from the schedule by its time in the times property.
189
+
190
+ Args:
191
+ time: An ladybug Time for the time and the value to remove.
192
+ """
193
+ self.remove_value(self._times.index(time))
194
+
195
+ def replace_value(self, value_index, new_value):
196
+ """Replace an existing value in the schedule with a new one.
197
+
198
+ Args:
199
+ value_index: An integer for the index of the value to replace.
200
+ new_value: A number for the new value to use at the given index.
201
+ """
202
+ val_list = list(self._values)
203
+ val_list[value_index] = self._check_value(new_value)
204
+ self._values = tuple(val_list)
205
+
206
+ def replace_value_by_time(self, time, new_value):
207
+ """Replace an existing value in the schedule using its time.
208
+
209
+ Args:
210
+ time: An ladybug Time for the time and the value to replace.
211
+ new_value: A number for the new value to use at the given time.
212
+ """
213
+ self.replace_value(self._times.index(time), new_value)
214
+
215
+ def values_at_timestep(self, timestep=1):
216
+ """Get a list of sequential schedule values over the day at a given timestep.
217
+
218
+ Note that there are two possible ways that these values can be mapped to
219
+ corresponding times (here referred to as the "Ladybug Tools Interpretation"
220
+ and the "EnergyPlus Interpretation"). Both of these interpretations ultimately
221
+ refer to the exact same schedule in the calculations of EnergyPlus but the
222
+ times of day that each of the values are mapped to differ.
223
+
224
+ Ladybug Tools Interpretation - The first value in the returned list here
225
+ corresponds to the time 0:00 and the value for this time is applied over
226
+ the rest of the following timestep. In this way, an office schedule that is set
227
+ to be occupied from 9:00 until 17:00 will show 9:00 as occupied but 17:00 as
228
+ unoccupied.
229
+
230
+ EnergyPlus Interpretation - The first value in the returned list here
231
+ corresponds to the timestep after 0:00. For example, if the timestep is 1,
232
+ the time mapped to the first value is 1:00. If the timestep is 6, the first
233
+ value corresponds to 0:10. In this interpretation, the value for this time is
234
+ applied over all of the previous timestep. In this way, an office schedule that
235
+ is set to be occupied from 9:00 until 17:00 will show 9:00 as unoccupied but
236
+ 17:00 as occupied.
237
+
238
+ Args:
239
+ timestep: An integer for the number of steps per hour at which to return
240
+ the resulting values.
241
+ """
242
+ assert timestep in self.VALIDTIMESTEPS, 'ScheduleDay timestep "{}" is invalid.' \
243
+ ' Must be one of the following:\n{}'.format(timestep, self.VALIDTIMESTEPS)
244
+ values = []
245
+ minute_delta = 60 / timestep
246
+ mod = 0 # track the minute of day through iteration
247
+ time_index = 1 # track the index of the next time of change
248
+ until_mod = self._get_until_mod(time_index) # get the mod of the next change
249
+ if not self.interpolate:
250
+ for _ in range(24 * timestep):
251
+ if mod >= until_mod:
252
+ time_index += 1
253
+ until_mod = self._get_until_mod(time_index)
254
+ values.append(self._values[time_index - 1])
255
+ mod += minute_delta
256
+ else:
257
+ for _ in range(24 * timestep):
258
+ if mod >= until_mod:
259
+ i = 0
260
+ delta = self._values[time_index] - self._values[time_index - 1]
261
+ until_mod = self._get_until_mod(time_index + 1)
262
+ n_steps = (until_mod - self._times[time_index].mod) / minute_delta
263
+ values.append(self._values[time_index - 1])
264
+ time_index += 1
265
+ elif time_index == 1:
266
+ values.append(self._values[time_index - 1])
267
+ else:
268
+ i += 1
269
+ values.append(self._values[time_index - 2] + ((i / n_steps) * delta))
270
+ mod += minute_delta
271
+ del values[0] # delete first value, which is makes interpolation off by one
272
+ values.append(self._values[-1]) # add the final value that is reached
273
+ return values
274
+
275
+ def data_collection(self, date=Date(1, 1), schedule_type_limit=None, timestep=1):
276
+ """Get a ladybug DataCollection representing this schedule at a given timestep.
277
+
278
+ Note that ladybug DataCollections always follow the "Ladybug Tools
279
+ Interpretation" of date time values as noted in the values_at_timestep
280
+ documentation.
281
+
282
+ Args:
283
+ date: A ladybug Date object for the day of the year the DataCollection
284
+ is representing. (Default: 1 Jan)
285
+ schedule_type_limit: A ScheduleTypeLimit object that describes the schedule,
286
+ which will be used to make the header for the DataCollection. If None,
287
+ a generic "Unknown" type will be used. (Default: None)
288
+ timestep: An integer for the number of steps per hour at which to make
289
+ the resulting DataCollection.
290
+ """
291
+ assert isinstance(date, Date), \
292
+ 'Expected ladybug Date. Got {}.'.format(type(date))
293
+ if schedule_type_limit is not None:
294
+ assert isinstance(schedule_type_limit, ScheduleTypeLimit), 'Expected ' \
295
+ 'Honeybee ScheduleTypeLimit. Got {}.'.format(type(schedule_type_limit))
296
+ d_type = schedule_type_limit.data_type
297
+ unit = schedule_type_limit.unit
298
+ else:
299
+ d_type = GenericType('Unknown Data Type', 'unknown')
300
+ unit = 'unknown'
301
+ a_period = AnalysisPeriod(date.month, date.day, 0, date.month, date.day, 23,
302
+ timestep, date.leap_year)
303
+ header = Header(d_type, unit, a_period, metadata={'schedule': self.identifier})
304
+ return HourlyContinuousCollection(header, self.values_at_timestep(timestep))
305
+
306
+ def shift_by_step(self, step_count=1, timestep=1):
307
+ """Get a version of this object where the values are shifted in time.
308
+
309
+ This is useful when attempting to derive a set of diversified schedules
310
+ from a single average schedule.
311
+
312
+ Args:
313
+ step_count: An integer for the number of timesteps at which the schedule
314
+ will be shifted. Positive values indicate a shift of values forward
315
+ in time while negative values indicate a shift backwards in
316
+ time. (Default: 1).
317
+ timestep: An integer for the number of timesteps per hour at which the
318
+ shifting is occurring. This must be a value between 1 and 60, which
319
+ is evenly divisible by 60. 1 indicates that each step is an hour
320
+ while 60 indicates that each step is a minute. (Default: 1)
321
+ """
322
+ value_deque = deque(self.values_at_timestep(timestep))
323
+ value_deque.rotate(step_count)
324
+ new_id = '{}_Shift_{}mins'.format(
325
+ self.identifier, int((60 / timestep) * step_count))
326
+ return ScheduleDay.from_values_at_timestep(new_id, list(value_deque), timestep)
327
+
328
+ @classmethod
329
+ def from_values_at_timestep(cls, identifier, values, timestep=1,
330
+ remove_repeated=True):
331
+ """Make a ScheduleDay from a list of values at a certain timestep.
332
+
333
+ Args:
334
+ identifier: Text string for a unique Schedule ID. Must be < 100 characters
335
+ and not contain any EnergyPlus special characters. This will be used to
336
+ identify the object across a model and in the exported IDF.
337
+ values: A list of numerical values with a length equal to 24 * timestep.
338
+ timestep: An integer for the number of steps per hour that the input
339
+ values correspond to. For example, if each value represents 30
340
+ minutes, the timestep is 2. For 15 minutes, it is 4. Default is 1,
341
+ meaning each value represents a single hour. Must be one of the
342
+ following: (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60)
343
+ remove_repeated: Boolean to note whether sequentially repeated values
344
+ should be removed from the resulting `values` and `times` comprising
345
+ the schedule. Default is True, which results in a lighter, more compact
346
+ schedule. However, you may want to set this to False when planning to
347
+ set the schedule's `interpolate` property to True as this avoids
348
+ interpolation over long, multi-hour periods.
349
+ """
350
+ # check the inputs
351
+ assert timestep in cls.VALIDTIMESTEPS, 'ScheduleDay timestep "{}" is invalid.' \
352
+ ' Must be one of the following:\n{}'.format(timestep, cls.VALIDTIMESTEPS)
353
+ n_vals = 24 * timestep
354
+ assert len(values) == n_vals, 'There must be {} ScheduleDay values when' \
355
+ 'the timestep is {}. Got {}.'.format(n_vals, timestep, len(values))
356
+
357
+ # build the list of schedule values and times
358
+ schedule_times = [cls._start_of_day]
359
+ minute_delta = 60 / timestep
360
+ mod = minute_delta
361
+ if remove_repeated:
362
+ schedule_values = [values[0]]
363
+ for i in range(1, n_vals):
364
+ if values[i] != schedule_values[-1]: # non-repeated value
365
+ schedule_times.append(Time.from_mod(mod))
366
+ schedule_values.append(values[i])
367
+ mod += minute_delta
368
+ else:
369
+ schedule_values = values # we don't care if there are repeated values
370
+ for i in range(1, n_vals):
371
+ schedule_times.append(Time.from_mod(mod))
372
+ mod += minute_delta
373
+
374
+ return cls(identifier, schedule_values, schedule_times)
375
+
376
+ @classmethod
377
+ def from_idf(cls, idf_string):
378
+ """Create a ScheduleDay from an EnergyPlus IDF text string.
379
+
380
+ Note that this method can accept all 3 types of EnergyPlus Schedule:Day
381
+ (Schedule:Day:Interval, Schedule:Day:Hourly, and Schedule:Day:List).
382
+
383
+ Args:
384
+ idf_string: A text string fully describing an EnergyPlus
385
+ Schedule:Day:Interval.
386
+ """
387
+ if idf_string.startswith('Schedule:Day:Hourly,'):
388
+ ep_strs = parse_idf_string(idf_string)
389
+ hour_vals = [float(val) for val in ep_strs[2:]]
390
+ return cls.from_values_at_timestep(ep_strs[0], hour_vals)
391
+ if idf_string.startswith('Schedule:Day:List,'):
392
+ ep_strs = parse_idf_string(idf_string)
393
+ interpolate = False if ep_strs[2] == 'No' or ep_strs[2] == '' else True
394
+ timestep = int(60 / int(ep_strs[3]))
395
+ timestep_vals = [float(val) for val in ep_strs[4:]]
396
+ remove_repeated = True if not interpolate else False
397
+ sched_day = cls.from_values_at_timestep(
398
+ ep_strs[0], timestep_vals, timestep, remove_repeated)
399
+ sched_day.interpolate = interpolate
400
+ return sched_day
401
+ else:
402
+ ep_strs = parse_idf_string(idf_string, 'Schedule:Day:Interval,')
403
+ interpolate = False if ep_strs[2] == 'No' or ep_strs[2] == '' else True
404
+ length = len(ep_strs)
405
+ values = tuple(float(ep_strs[i]) for i in range(4, length + 1, 2))
406
+ times = [cls._start_of_day]
407
+ for i in range(3, length, 2):
408
+ try:
409
+ times.append(Time.from_time_string(ep_strs[i]))
410
+ except ValueError: # 24:00
411
+ pass
412
+ return cls(ep_strs[0], values, times, interpolate)
413
+
414
+ @classmethod
415
+ def from_dict(cls, data):
416
+ """Create a ScheduleDay from a dictionary.
417
+
418
+ Args:
419
+ data: ScheduleDay dictionary following the format below.
420
+
421
+ .. code-block:: python
422
+
423
+ {
424
+ "type": 'ScheduleDay',
425
+ "identifier": 'Office_Occ_900_1700',
426
+ "display_name": 'Office Occupancy',
427
+ "values": [0, 1, 0],
428
+ "times": [(0, 0), (9, 0), (17, 0)],
429
+ "interpolate": False
430
+ }
431
+ """
432
+ assert data['type'] == 'ScheduleDay', \
433
+ 'Expected ScheduleDay. Got {}.'.format(data['type'])
434
+
435
+ if 'times' in data and data['times'] is not None:
436
+ times = tuple(Time.from_array(tim) for tim in data['times'])
437
+ else:
438
+ times = None
439
+ interpolate = data['interpolate'] if 'interpolate' in data else None
440
+
441
+ new_obj = cls(data['identifier'], data['values'], times, interpolate)
442
+ if 'display_name' in data and data['display_name'] is not None:
443
+ new_obj.display_name = data['display_name']
444
+ return new_obj
445
+
446
+ def to_idf(self, schedule_type_limit=None):
447
+ """IDF string representation of ScheduleDay object.
448
+
449
+ Args:
450
+ schedule_type_limit: Optional ScheduleTypeLimit object, which will
451
+ be written into the IDF string in order to validate the values
452
+ within the schedule during the EnergyPlus run.
453
+
454
+ .. code-block:: shell
455
+
456
+ Schedule:Day:Interval,
457
+ dd winter rel humidity, !- Name
458
+ Percent, !- Schedule Type Limits Name
459
+ No, !- Interpolate to Timestep
460
+ until: 24:00, !- Time 1
461
+ 74; !- Value Until Time 1
462
+ """
463
+ fields = [self.identifier, ''] if schedule_type_limit is None else \
464
+ [self.identifier, schedule_type_limit.identifier]
465
+ fields.append('No' if not self.interpolate else 'Linear')
466
+ comments = ['schedule name', 'schedule type limits', 'interpolate to timestep']
467
+ for i in range(len(self._values)):
468
+ count = i + 1
469
+ try:
470
+ fields.append(self._times[count])
471
+ except IndexError: # the last "time until"
472
+ fields.append('24:00')
473
+ comments.append('time %s {hh:mm}' % count)
474
+ fields.append(self._values[i])
475
+ comments.append('value until time %s' % count)
476
+ return generate_idf_string('Schedule:Day:Interval', fields, comments)
477
+
478
+ def to_dict(self):
479
+ """ScheduleDay dictionary representation."""
480
+ base = {'type': 'ScheduleDay'}
481
+ base['identifier'] = self.identifier
482
+ base['values'] = self.values
483
+ base['times'] = [time.to_array() for time in self.times]
484
+ base['interpolate'] = self.interpolate
485
+ if self._display_name is not None:
486
+ base['display_name'] = self.display_name
487
+ return base
488
+
489
+ def duplicate(self):
490
+ """Get a copy of this object."""
491
+ return self.__copy__()
492
+
493
+ @staticmethod
494
+ def average_schedules(identifier, schedules, weights=None, timestep_resolution=1):
495
+ """Create a ScheduleDay that is a weighted average between other ScheduleDays.
496
+
497
+ Args:
498
+ identifier: Text string for a unique ID for the new unique ScheduleDay.
499
+ Must be < 100 characters and not contain any EnergyPlus special
500
+ characters. This will be used to identify the object across a
501
+ model and in the exported IDF.
502
+ schedules: A list of ScheduleDay objects that will be averaged together
503
+ to make a new ScheduleDay.
504
+ weights: An optional list of fractional numbers with the same length
505
+ as the input schedules that sum to 1. These will be used to weight
506
+ each of the ScheduleDay objects in the resulting average schedule.
507
+ If None, the individual schedules will be weighted equally.
508
+ timestep_resolution: An optional integer for the timestep resolution
509
+ at which the schedules will be averaged. Any schedule details
510
+ smaller than this timestep will be lost in the averaging process.
511
+ Default: 1.
512
+ """
513
+ # check the inputs
514
+ assert isinstance(schedules, (list, tuple)), 'Expected a list of ScheduleDay ' \
515
+ 'objects for average_schedules. Got {}.'.format(type(schedules))
516
+ if weights is None:
517
+ weight = 1 / len(schedules)
518
+ weights = [weight for i in schedules]
519
+ else:
520
+ weights = tuple_with_length(weights, len(schedules), float,
521
+ 'average schedules weights')
522
+ assert sum(weights) == 1, 'Average schedule weights must sum to 1. ' \
523
+ 'Got {}.'.format(sum(weights))
524
+
525
+ # create a weighted average list of values
526
+ all_values = [sch.values_at_timestep(timestep_resolution) for sch in schedules]
527
+ sch_vals = [sum([val * weights[i] for i, val in enumerate(values)])
528
+ for values in zip(*all_values)]
529
+
530
+ # return the final list
531
+ return ScheduleDay.from_values_at_timestep(
532
+ identifier, sch_vals, timestep_resolution)
533
+
534
+ def _get_until_mod(self, time_index):
535
+ """Get the minute of the day until a value is applied given a time_index."""
536
+ try:
537
+ return self._times[time_index].mod
538
+ except IndexError: # constant value until the end of the day
539
+ return 1440
540
+
541
+ def _check_values(self, values):
542
+ """Check values whenever they come through the values setter."""
543
+ assert isinstance(values, Iterable) and not \
544
+ isinstance(values, (str, dict, bytes, bytearray)), \
545
+ 'values should be a list or tuple. Got {}'.format(type(values))
546
+ assert len(values) == len(self._times), \
547
+ 'Length of values list must match length of times list. {} != {}'.format(
548
+ len(values), len(self._times))
549
+ assert len(values) > 0, 'ScheduleDay must include at least one value.'
550
+ try:
551
+ return tuple(float(val) for val in values)
552
+ except (ValueError, TypeError):
553
+ raise TypeError('ScheduleDay values must be numbers.')
554
+
555
+ def _check_times(self, times):
556
+ """Check times whenever they come through the times setter."""
557
+ if not isinstance(times, tuple):
558
+ try:
559
+ times = tuple(times)
560
+ except (ValueError, TypeError):
561
+ raise TypeError('ScheduleDay times must be iterable.')
562
+ for time in times:
563
+ self._check_time(time)
564
+ assert len(times) == len(self._values), \
565
+ 'Length of values list must match length of datetimes list. {} != {}'.format(
566
+ len(times), len(self._values))
567
+ if not self._are_chronological(times):
568
+ times, self._values = zip(*sorted(zip(times, self._values)))
569
+ # ensure that the schedule always starts from 0:00
570
+ assert times[0] == self._start_of_day, \
571
+ 'ScheduleDay times must always start with 0:00. Got {}.'.format(times[0])
572
+ return times
573
+
574
+ @staticmethod
575
+ def _check_value(value):
576
+ """Check that an individual input value is a number."""
577
+ try:
578
+ return float(value)
579
+ except (ValueError, TypeError):
580
+ raise TypeError('ScheduleDay values must be numbers.')
581
+
582
+ @staticmethod
583
+ def _check_time(time):
584
+ """Check that an individual time value is a ladybug Time."""
585
+ assert isinstance(time, Time), \
586
+ 'Expected ladybug Time for ScheduleDay. Got {}.'.format(type(time))
587
+
588
+ @staticmethod
589
+ def _are_chronological(times):
590
+ """Test whether a list of times is chronological."""
591
+ return all(times[i] < times[i + 1] for i in range(len(times) - 1))
592
+
593
+ def __len__(self):
594
+ return len(self.values)
595
+
596
+ def __getitem__(self, key):
597
+ return self.values[key]
598
+
599
+ def __iter__(self):
600
+ return iter(self.values)
601
+
602
+ def __key(self):
603
+ """A tuple based on the object properties, useful for hashing."""
604
+ return (self.identifier,) + self.values + tuple(hash(t) for t in self.times) + \
605
+ (self.interpolate,)
606
+
607
+ def __hash__(self):
608
+ return hash(self.__key())
609
+
610
+ def __eq__(self, other):
611
+ return isinstance(other, ScheduleDay) and self.__key() == other.__key()
612
+
613
+ def __ne__(self, other):
614
+ return not self.__eq__(other)
615
+
616
+ def __copy__(self):
617
+ new_obj = ScheduleDay(self.identifier, self.values, self.times, self.interpolate)
618
+ new_obj._display_name = self._display_name
619
+ return new_obj
620
+
621
+ def ToString(self):
622
+ """Overwrite .NET ToString."""
623
+ return self.__repr__()
624
+
625
+ def __repr__(self):
626
+ return self.to_idf()
@@ -0,0 +1,59 @@
1
+ # coding=utf-8
2
+ """Utilities to convert schedule dictionaries to Python objects."""
3
+ from honeybee_energy.schedule.ruleset import ScheduleRuleset
4
+ from honeybee_energy.schedule.fixedinterval import ScheduleFixedInterval
5
+
6
+
7
+ SCHEDULE_TYPES = ('ScheduleRuleset', 'ScheduleFixedInterval')
8
+
9
+
10
+ def dict_to_schedule(sch_dict, raise_exception=True):
11
+ """Get a Python object of any Schedule from a dictionary.
12
+
13
+ Args:
14
+ sch_dict: A dictionary of any Honeybee energy schedules. Note
15
+ that this should be a non-abridged dictionary to be valid.
16
+ raise_exception: Boolean to note whether an exception should be raised
17
+ if the object is not identified as a schedule. Default: True.
18
+
19
+ Returns:
20
+ A Python object derived from the input sch_dict.
21
+ """
22
+ try: # get the type key from the dictionary
23
+ sch_type = sch_dict['type']
24
+ except KeyError:
25
+ raise ValueError('Schedule dictionary lacks required "type" key.')
26
+
27
+ if sch_type == 'ScheduleRuleset':
28
+ return ScheduleRuleset.from_dict(sch_dict)
29
+ elif sch_type == 'ScheduleFixedInterval':
30
+ return ScheduleFixedInterval.from_dict(sch_dict)
31
+ elif raise_exception:
32
+ raise ValueError('{} is not a recognized energy Schedule type'.format(sch_type))
33
+
34
+
35
+ def dict_abridged_to_schedule(sch_dict, schedule_type_limits, raise_exception=True):
36
+ """Get a Python object of any Schedule from an abridged dictionary.
37
+
38
+ Args:
39
+ sch_dict: A dictionary of any Honeybee energy schedules. Note
40
+ that this should be a non-abridged dictionary to be valid.
41
+ schedule_type_limits: Dictionary of all schedule type limit objects that
42
+ might be used in the schedule with the type limit identifiers as the keys.
43
+ raise_exception: Boolean to note whether an exception should be raised
44
+ if the object is not identified as a schedule. Default: True.
45
+
46
+ Returns:
47
+ A Python object derived from the input sch_dict.
48
+ """
49
+ try: # get the type key from the dictionary
50
+ sch_type = sch_dict['type']
51
+ except KeyError:
52
+ raise ValueError('Schedule dictionary lacks required "type" key.')
53
+
54
+ if sch_type == 'ScheduleRulesetAbridged':
55
+ return ScheduleRuleset.from_dict_abridged(sch_dict, schedule_type_limits)
56
+ elif sch_type == 'ScheduleFixedIntervalAbridged':
57
+ return ScheduleFixedInterval.from_dict_abridged(sch_dict, schedule_type_limits)
58
+ elif raise_exception:
59
+ raise ValueError('{} is not a recognized energy Schedule type'.format(sch_type))