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,1867 @@
1
+ # coding=utf-8
2
+ """Complete annual schedule object built from ScheduleDay and rules for applying them."""
3
+ from __future__ import division
4
+
5
+ import re
6
+
7
+ from honeybee._lockable import lockable
8
+ from honeybee.typing import tuple_with_length, valid_ep_string
9
+ from ladybug.analysisperiod import AnalysisPeriod
10
+ from ladybug.datacollection import HourlyContinuousCollection
11
+ from ladybug.datatype.generic import GenericType
12
+ from ladybug.dt import Date, Time
13
+ from ladybug.header import Header
14
+
15
+ from ..reader import parse_idf_string, clean_idf_file_contents
16
+ from ..writer import generate_idf_string
17
+ from ..properties.extension import ScheduleRulesetProperties
18
+ from .day import ScheduleDay
19
+ from .rule import ScheduleRule
20
+ from .typelimit import ScheduleTypeLimit
21
+
22
+
23
+ @lockable
24
+ class ScheduleRuleset(object):
25
+ """A complete schedule assembled from ScheduleDay and ScheduleRules.
26
+
27
+ Args:
28
+ identifier: Text string for a unique Schedule ID. Must be < 100 characters
29
+ and not contain any EnergyPlus special characters. This will be used to
30
+ identify the object across a model and in the exported IDF.
31
+ default_day_schedule: A ScheduleDay object that will be used for all
32
+ days where there is no ScheduleRule applied.
33
+ schedule_rules: A list of ScheduleRule objects that note exceptions
34
+ to the default_day_schedule. These rules should be ordered from
35
+ highest to lowest priority.
36
+ schedule_type_limit: A ScheduleTypeLimit object that will be used to
37
+ validate schedule values against upper/lower limits and assign units
38
+ to the schedule values. If None, no validation will occur.
39
+ summer_designday_schedule: A ScheduleDay object that will be used for
40
+ the summer design day (used to size the cooling system).
41
+ winter_designday_schedule: A ScheduleDay object that will be used for
42
+ the winter design day (used to size the heating system).
43
+ holiday_schedule: A ScheduleDay object that will be used for holidays.
44
+
45
+ Properties:
46
+ * identifier
47
+ * display_name
48
+ * default_day_schedule
49
+ * schedule_rules
50
+ * schedule_type_limit
51
+ * summer_designday_schedule
52
+ * winter_designday_schedule
53
+ * holiday_schedule
54
+ * day_schedules
55
+ * typical_day_schedules
56
+ * is_constant
57
+ * is_single_week
58
+ * user_data
59
+ """
60
+ __slots__ = ('_identifier', '_display_name', '_default_day_schedule',
61
+ '_schedule_rules', '_holiday_schedule', '_summer_designday_schedule',
62
+ '_winter_designday_schedule', '_schedule_type_limit',
63
+ '_locked', '_properties', '_user_data')
64
+ _dow_text_to_int = {'sunday': 1, 'monday': 2, 'tuesday': 3, 'wednesday': 4,
65
+ 'thursday': 2, 'friday': 3, 'saturday': 7}
66
+ _schedule_week_comments = (
67
+ 'name', 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday',
68
+ 'saturday', 'holiday', 'summer design day', 'winter design day',
69
+ 'custom day 1', 'custom day 2')
70
+
71
+ def __init__(self, identifier, default_day_schedule, schedule_rules=None,
72
+ schedule_type_limit=None, holiday_schedule=None,
73
+ summer_designday_schedule=None, winter_designday_schedule=None):
74
+ """Initialize Schedule Ruleset."""
75
+ self._locked = False # unlocked by default
76
+ self.identifier = identifier
77
+ self._display_name = None
78
+ self.default_day_schedule = default_day_schedule
79
+ self.schedule_rules = schedule_rules
80
+ self.schedule_type_limit = schedule_type_limit
81
+ self.holiday_schedule = holiday_schedule
82
+ self.summer_designday_schedule = summer_designday_schedule
83
+ self.winter_designday_schedule = winter_designday_schedule
84
+
85
+ # initialize properties for extensions and user data
86
+ self._properties = ScheduleRulesetProperties(self)
87
+ self._user_data = None
88
+
89
+ @property
90
+ def identifier(self):
91
+ """Get or set the text string for schedule unique identifier."""
92
+ return self._identifier
93
+
94
+ @identifier.setter
95
+ def identifier(self, identifier):
96
+ self._identifier = valid_ep_string(identifier, 'schedule ruleset identifier')
97
+
98
+ @property
99
+ def display_name(self):
100
+ """Get or set a string for the object name without any character restrictions.
101
+
102
+ If not set, this will be equal to the identifier.
103
+ """
104
+ if self._display_name is None:
105
+ return self._identifier
106
+ return self._display_name
107
+
108
+ @display_name.setter
109
+ def display_name(self, value):
110
+ if value is not None:
111
+ try:
112
+ value = str(value)
113
+ except UnicodeEncodeError: # Python 2 machine lacking the character set
114
+ pass # keep it as unicode
115
+ self._display_name = value
116
+
117
+ @property
118
+ def default_day_schedule(self):
119
+ """Get or set the DaySchedule object that will be used by default."""
120
+ return self._default_day_schedule
121
+
122
+ @default_day_schedule.setter
123
+ def default_day_schedule(self, schedule):
124
+ assert isinstance(schedule, ScheduleDay), 'Expected ScheduleDay for ' \
125
+ 'ScheduleRuleset default_day_schedule. Got {}.'.format(type(schedule))
126
+ self._check_schedule_parent(schedule, 'default_day_schedule')
127
+ self._default_day_schedule = schedule
128
+
129
+ @property
130
+ def schedule_rules(self):
131
+ """Get or set an array of ScheduleRules that note exceptions to the default.
132
+
133
+ These rules are ordered from highest priority to lowest priority meaning that,
134
+ if two rules cover the same date range and day of the week, the rule that comes
135
+ first in this list will take precedence. Following this logic, you typically
136
+ want rules that only apply for part of a year to precede rules that are
137
+ applied over the whole year. This way, the schedule over the whole year doesn't
138
+ overwrite the partial-year schedule underneath it.
139
+ """
140
+ return tuple(self._schedule_rules)
141
+
142
+ @schedule_rules.setter
143
+ def schedule_rules(self, rules):
144
+ self._schedule_rules = self._check_schedule_rules(rules)
145
+
146
+ @property
147
+ def schedule_type_limit(self):
148
+ """Get or set a ScheduleTypeLimit object used to assign units to schedule values.
149
+ """
150
+ return self._schedule_type_limit
151
+
152
+ @schedule_type_limit.setter
153
+ def schedule_type_limit(self, schedule_type):
154
+ if schedule_type is not None:
155
+ assert isinstance(schedule_type, ScheduleTypeLimit), 'Expected ' \
156
+ 'ScheduleTypeLimit for ScheduleRuleset schedule_type_limit. ' \
157
+ 'Got {}.'.format(type(schedule_type))
158
+ self._schedule_type_limit = schedule_type
159
+
160
+ @property
161
+ def holiday_schedule(self):
162
+ """Get or set the DaySchedule that will be used for holidays.
163
+
164
+ Note that, if this property is None, the default_day_schedule is
165
+ ultimately written into the IDF for the holidays.
166
+ """
167
+ return self._holiday_schedule
168
+
169
+ @holiday_schedule.setter
170
+ def holiday_schedule(self, schedule):
171
+ if schedule is not None:
172
+ assert isinstance(schedule, ScheduleDay), 'Expected ScheduleDay for ' \
173
+ 'ScheduleRuleset holiday_schedule. Got {}.'.format(
174
+ type(schedule))
175
+ self._check_schedule_parent(schedule, 'holiday_schedule')
176
+ self._holiday_schedule = schedule
177
+
178
+ @property
179
+ def summer_designday_schedule(self):
180
+ """Get or set the DaySchedule that will be used for the summer design day.
181
+
182
+ Note that, if this property is None, the default_day_schedule is
183
+ ultimately written into the IDF for the summer design day.
184
+ """
185
+ return self._summer_designday_schedule
186
+
187
+ @summer_designday_schedule.setter
188
+ def summer_designday_schedule(self, schedule):
189
+ if schedule is not None:
190
+ assert isinstance(schedule, ScheduleDay), 'Expected ScheduleDay for ' \
191
+ 'ScheduleRuleset summer_designday_schedule. Got {}.'.format(
192
+ type(schedule))
193
+ self._check_schedule_parent(schedule, 'summer_designday_schedule')
194
+ self._summer_designday_schedule = schedule
195
+
196
+ @property
197
+ def winter_designday_schedule(self):
198
+ """Get or set the DaySchedule that will be used for the winter design day.
199
+
200
+ Note that, if this property is None, the default_day_schedule is
201
+ ultimately written into the IDF for the winter design day.
202
+ """
203
+ return self._winter_designday_schedule
204
+
205
+ @winter_designday_schedule.setter
206
+ def winter_designday_schedule(self, schedule):
207
+ if schedule is not None:
208
+ assert isinstance(schedule, ScheduleDay), 'Expected ScheduleDay for ' \
209
+ 'ScheduleRuleset winter_designday_schedule. Got {}.'.format(
210
+ type(schedule))
211
+ self._check_schedule_parent(schedule, 'winter_designday_schedule')
212
+ self._winter_designday_schedule = schedule
213
+
214
+ @property
215
+ def day_schedules(self):
216
+ """Get a list of all unique ScheduleDay objects used in this ScheduleRuleset."""
217
+ day_scheds = [self.default_day_schedule]
218
+ if self._summer_designday_schedule is not None and not \
219
+ self._instance_in_array(self._summer_designday_schedule, day_scheds):
220
+ day_scheds.append(self._summer_designday_schedule)
221
+ if self._winter_designday_schedule is not None and not \
222
+ self._instance_in_array(self._winter_designday_schedule, day_scheds):
223
+ day_scheds.append(self._winter_designday_schedule)
224
+ if self._holiday_schedule is not None and not \
225
+ self._instance_in_array(self._holiday_schedule, day_scheds):
226
+ day_scheds.append(self._holiday_schedule)
227
+ for rule in self.schedule_rules:
228
+ if not self._instance_in_array(rule._schedule_day, day_scheds):
229
+ day_scheds.append(rule._schedule_day)
230
+ return day_scheds
231
+
232
+ @property
233
+ def typical_day_schedules(self):
234
+ """Get a list of all unique ScheduleDay objects used over the annual run period.
235
+
236
+ This excludes the schedules of the design days and the holiday schedule,
237
+ which gives a sense of the typical schedule over the year.
238
+ """
239
+ day_scheds = [self.default_day_schedule]
240
+ for rule in self.schedule_rules:
241
+ if not self._instance_in_array(rule._schedule_day, day_scheds):
242
+ day_scheds.append(rule._schedule_day)
243
+ return day_scheds
244
+
245
+ @property
246
+ def is_constant(self):
247
+ """Boolean noting whether the schedule is representable with a single value."""
248
+ return self.default_day_schedule.is_constant and self._schedule_rules == [] and \
249
+ self._summer_designday_schedule is None and \
250
+ self._winter_designday_schedule is None and self._holiday_schedule is None
251
+
252
+ @property
253
+ def is_single_week(self):
254
+ """Boolean noting whether this schedule is representable with one week schedule.
255
+ """
256
+ if self._schedule_rules == []:
257
+ return True
258
+ elif all([sch._start_doy == 1 and sch._end_doy == 365
259
+ for sch in self._schedule_rules]):
260
+ return True
261
+ return False
262
+
263
+ @property
264
+ def user_data(self):
265
+ """Get or set an optional dictionary for additional meta data for this object.
266
+
267
+ This will be None until it has been set. All keys and values of this
268
+ dictionary should be of a standard Python type to ensure correct
269
+ serialization of the object to/from JSON (eg. str, float, int, list, dict)
270
+ """
271
+ return self._user_data
272
+
273
+ @user_data.setter
274
+ def user_data(self, value):
275
+ if value is not None:
276
+ assert isinstance(value, dict), 'Expected dictionary for honeybee_energy' \
277
+ 'object user_data. Got {}.'.format(type(value))
278
+ self._user_data = value
279
+
280
+ @property
281
+ def properties(self):
282
+ """Get properties for extensions."""
283
+ return self._properties
284
+
285
+ def add_rule(self, rule):
286
+ """Add a ScheduleRule to this ScheduleRuleset.
287
+
288
+ Note that adding a rule here will add it as highest priority in the full list
289
+ of schedule_rules, meaning it may overwrite other rules underneath it.
290
+
291
+ Args:
292
+ rule: A ScheduleRule object to be added to this ScheduleRuleset.
293
+ ScheduleRule objects note the exceptions to the default_day_schedule.
294
+ """
295
+ self._check_rule(rule)
296
+ self._check_schedule_parent(rule.schedule_day, 'schedule_rule')
297
+ self._schedule_rules.insert(0, rule)
298
+
299
+ def remove_rule(self, rule_index):
300
+ """Remove a ScheduleRule from the schedule by its index in schedule_rules.
301
+
302
+ Args:
303
+ rule_index: An integer for the index of the rule to remove.
304
+ """
305
+ self._schedule_rules[rule_index].schedule_day._parent = None
306
+ del self._schedule_rules[rule_index]
307
+
308
+ def reorder_rule(self, rule_index, new_index=0):
309
+ """Change the priority of a ScheduleRule in the full schedule_rules list.
310
+
311
+ Lower indices (ordered first) in the schedule_rules indicate the rule has
312
+ a higher priority.
313
+
314
+ Args:
315
+ rule_index: An integer for the index of the rule to reorder.
316
+ new_index: An integer for the new index of the rule. The default is 0,
317
+ which will re-insert the selected rule at the top of the
318
+ priority list.
319
+ """
320
+ self._schedule_rules.insert(new_index, self._schedule_rules.pop(rule_index))
321
+
322
+ def values(self, timestep=1, start_date=Date(1, 1), end_date=Date(12, 31),
323
+ start_dow='Sunday', holidays=None, leap_year=False):
324
+ """Get a list of sequential schedule values over the year at a given timestep.
325
+
326
+ Note that there are two possible ways that these values can be mapped to
327
+ corresponding times. See the ScheduleDay.values_at_timestep method
328
+ documentation for a complete description of these two interpretations.
329
+
330
+ Args:
331
+ timestep: An integer for the number of steps per hour at which to return
332
+ the resulting values.
333
+ start_date: An optional ladybug Date object for when to start the list
334
+ of values. Default: 1 Jan.
335
+ end_date: An optional ladybug Date object for when to end the list
336
+ of values. Default: 31 Dec.
337
+ start_dow: An optional text string for the starting day of the week.
338
+ Default: Sunday.
339
+ holidays: An optional list of ladybug Date objects for the holidays. For
340
+ any holiday in this list, schedule rules set to apply_holiday will
341
+ take effect.
342
+ leap_year: Boolean to note whether the generated values should be for a
343
+ leap year (True) or a non-leap year (False). Default: False.
344
+ """
345
+ # get the values over the day for each of the ScheduleDay objects
346
+ sch_day_vals = [rule.schedule_day.values_at_timestep(timestep)
347
+ for rule in self._schedule_rules]
348
+ sch_day_vals.append(self.default_day_schedule.values_at_timestep(timestep))
349
+ hol_vals = None
350
+ if self.holiday_schedule is not None and holidays is not None:
351
+ hol_vals = self.holiday_schedule.values_at_timestep(timestep)
352
+ # ensure that everything is consistent across leap years
353
+ if start_date.leap_year is not leap_year:
354
+ start_date = Date(start_date.month, start_date.day, leap_year)
355
+ if end_date.leap_year is not leap_year:
356
+ end_date = Date(end_date.month, end_date.day, leap_year)
357
+ # ensure start date is before end date
358
+ assert start_date <= end_date, 'ScheduleRuleset values() start_date must come ' \
359
+ 'before end_date. {} comes after {}.'.format(start_date, end_date)
360
+ # process the holidays if they are input
361
+ if holidays is not None:
362
+ hol_doy = []
363
+ for hol in holidays:
364
+ if hol.leap_year is not leap_year:
365
+ hol = Date(hol.month, hol.day, leap_year)
366
+ hol_doy.append(hol.doy)
367
+ else:
368
+ hol_doy = []
369
+ # process the start_dow into an integer.
370
+ dow = self._dow_text_to_int[start_dow.lower()]
371
+ # generate the full list of annual values
372
+ if not leap_year:
373
+ return self._get_sch_values(
374
+ sch_day_vals, dow, start_date, end_date, hol_doy, hol_vals)
375
+ else:
376
+ return self._get_sch_values_leap_year(
377
+ sch_day_vals, dow, start_date, end_date, hol_doy, hol_vals)
378
+
379
+ def data_collection(self, timestep=1, start_date=Date(1, 1), end_date=Date(12, 31),
380
+ start_dow='Sunday', holidays=None, leap_year=False):
381
+ """Get a ladybug DataCollection representing this schedule at a given timestep.
382
+
383
+ Note that ladybug DataCollections always follow the "Ladybug Tools
384
+ Interpretation" of date time values as noted in the
385
+ ScheduleDay.values_at_timestep documentation.
386
+
387
+ Args:
388
+ timestep: An integer for the number of steps per hour at which to make
389
+ the resulting DataCollection.
390
+ start_date: An optional ladybug Date object for when to start the
391
+ DataCollection. Default: 1 Jan.
392
+ end_date: An optional ladybug Date object for when to end the
393
+ DataCollection. Default: 31 Dec.
394
+ start_dow: An optional text string for the starting day of the week.
395
+ Default: Sunday.
396
+ holidays: An optional list of ladybug Date objects for the holidays. For
397
+ any holiday in this list, schedule rules set to apply_holiday will
398
+ take effect.
399
+ leap_year: Boolean to note whether the generated values should be for a
400
+ leap year (True) or a non-leap year (False). Default: False.
401
+ """
402
+ a_period = AnalysisPeriod(start_date.month, start_date.day, 0,
403
+ end_date.month, end_date.day, 23,
404
+ timestep, leap_year)
405
+ if self.schedule_type_limit is not None:
406
+ data_type = self.schedule_type_limit.data_type
407
+ unit = self.schedule_type_limit.unit
408
+ else:
409
+ unit = 'unknown'
410
+ data_type = GenericType('Unknown Data Type', unit)
411
+ header = Header(data_type, unit, a_period,
412
+ metadata={'schedule': self.identifier})
413
+ values = self.values(timestep, start_date, end_date, start_dow,
414
+ holidays, leap_year)
415
+ return HourlyContinuousCollection(header, values)
416
+
417
+ def shift_by_step(self, step_count=1, timestep=1):
418
+ """Get a version of this object where the day_schedule values are shifted.
419
+
420
+ This is useful when attempting to derive a set of diversified schedules
421
+ from a single average schedule.
422
+
423
+ Args:
424
+ step_count: An integer for the number of timesteps at which the schedule
425
+ will be shifted. Positive values indicate a shift of values forward
426
+ in time while negative values indicate a shift backwards in
427
+ time. (Default: 1).
428
+ timestep: An integer for the number of timesteps per hour at which the
429
+ shifting is occurring. This must be a value between 1 and 60, which
430
+ is evenly divisible by 60. 1 indicates that each step is an hour
431
+ while 60 indicates that each step is a minute. (Default: 1).
432
+ """
433
+ # shift all of the day schedules according to the inputs
434
+ day_scheds = self.day_schedules
435
+ shift_dict = {sch.identifier: sch.shift_by_step(step_count, timestep)
436
+ for sch in day_scheds}
437
+ # figure out where each of the shifted schedules belong
438
+ new_default = shift_dict[self.default_day_schedule.identifier]
439
+ new_summer = shift_dict[self._summer_designday_schedule.identifier] \
440
+ if self._summer_designday_schedule is not None else None
441
+ new_winter = shift_dict[self._winter_designday_schedule.identifier] \
442
+ if self._winter_designday_schedule is not None else None
443
+ new_holiday = shift_dict[self._holiday_schedule.identifier] \
444
+ if self._holiday_schedule is not None else None
445
+ new_rules = []
446
+ for rule in self.schedule_rules:
447
+ new_rule = rule.duplicate()
448
+ new_rule.schedule_day = shift_dict[rule.schedule_day.identifier]
449
+ new_rules.append(new_rule)
450
+ # return the shifted schedule
451
+ new_id = '{}_Shift_{}mins'.format(
452
+ self.identifier, int((60 / timestep) * step_count))
453
+ return ScheduleRuleset(
454
+ new_id, new_default, new_rules, self.schedule_type_limit, new_holiday,
455
+ new_summer, new_winter)
456
+
457
+ @classmethod
458
+ def from_constant_value(cls, identifier, value, schedule_type_limit=None):
459
+ """Create a ScheduleRuleset fromm a single constant value.
460
+
461
+ Args:
462
+ identifier: Text string for a unique Schedule ID. Must be < 100 characters
463
+ and not contain any EnergyPlus special characters. This will be used to
464
+ identify the object across a model and in the exported IDF.
465
+ value: A single constant value to be applied throughout the whole year.
466
+ schedule_type_limit: A ScheduleTypeLimit object that will be used to
467
+ validate schedule values against upper/lower limits and assign
468
+ units to the schedule values.
469
+ """
470
+ default_sched = ScheduleDay('{}_Day Schedule'.format(identifier), [value])
471
+ return cls(identifier, default_sched, None, schedule_type_limit)
472
+
473
+ @classmethod
474
+ def from_daily_values(cls, identifier, daily_values, timestep=1,
475
+ schedule_type_limit=None):
476
+ """Create a ScheduleRuleset from a list of repeating daily values at a timestep.
477
+
478
+ Args:
479
+ identifier: Text string for a unique Schedule ID. Must be < 100 characters
480
+ and not contain any EnergyPlus special characters. This will be used to
481
+ identify the object across a model and in the exported IDF.
482
+ daily_values: A list of [24 * timestep] numbers for schedule values.
483
+ timestep: An integer for the number of steps per hour that the input
484
+ values correspond to. For example, if each value represents 30
485
+ minutes, the timestep is 2. For 15 minutes, it is 4. Default is 1,
486
+ meaning each value represents a single hour. Must be one of the
487
+ following: (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60).
488
+ schedule_type_limit: A ScheduleTypeLimit object that will be used to
489
+ validate schedule values against upper/lower limits and assign units
490
+ to the schedule values.
491
+ """
492
+ default_sched = ScheduleDay.from_values_at_timestep(
493
+ '{}_Day Schedule'.format(identifier), daily_values, timestep)
494
+ return cls(identifier, default_sched, None, schedule_type_limit)
495
+
496
+ @classmethod
497
+ def from_week_daily_values(
498
+ cls, identifier, sunday_values, monday_values, tuesday_values,
499
+ wednesday_values, thursday_values, friday_values, saturday_values,
500
+ holiday_values, timestep=1, schedule_type_limit=None,
501
+ summer_designday_values=None, winter_designday_values=None):
502
+ """Create a ScheduleRuleset from lists of daily values for each day of the week.
503
+
504
+ Args:
505
+ identifier: Text string for a unique Schedule ID. Must be < 100 characters
506
+ and not contain any EnergyPlus special characters. This will be used to
507
+ identify the object across a model and in the exported IDF.
508
+ sunday_values: A list of [24 * timestep] numerical values for Sundays.
509
+ monday_values: A list of [24 * timestep] numerical values for Mondays.
510
+ tuesday_values: A list of [24 * timestep] numerical values for Tuesdays.
511
+ wednesday_values: A list of [24 * timestep] numerical values for Wednesdays.
512
+ thursday_values: A list of [24 * timestep] numerical values for Thursdays.
513
+ friday_values: A list of [24 * timestep] numerical values for Fridays.
514
+ saturday_values: A list of [24 * timestep] numerical values for Saturdays.
515
+ holiday_values: A list of [24 * timestep] numerical values for Holidays.
516
+ timestep: An integer for the number of steps per hour that the input
517
+ values correspond to. For example, if each value represents 30
518
+ minutes, the timestep is 2. For 15 minutes, it is 4. Default is 1,
519
+ meaning each value represents a single hour. Must be one of the
520
+ following: (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60).
521
+ schedule_type_limit: A ScheduleTypeLimit object that will be used to
522
+ validate schedule values against upper/lower limits and assign
523
+ units to the schedule values. (Default: None).
524
+ summer_designday_values: A list of [24 * timestep] numerical values for
525
+ the summer design day. If None, the daily schedule with the highest
526
+ average value will be used unless the schedule_type_limit has a
527
+ Temperature unit_type, in which case it will be the daily schedule
528
+ with the lowest average value. (Default: None).
529
+ winter_designday_values: A list of [24 * timestep] numerical values for
530
+ the winter design day. If None, the daily schedule with the lowest
531
+ average value will be used unless the schedule_type_limit has a
532
+ Temperature unit_type, in which case it will be the daily schedule
533
+ with the highest average value. (Default: None).
534
+ """
535
+ # process the rules for the days of the week
536
+ schedule_rules = []
537
+ applied_day_values = []
538
+ all_vals = (sunday_values, monday_values, tuesday_values, wednesday_values,
539
+ thursday_values, friday_values, saturday_values)
540
+ for i, day_vals in enumerate(all_vals):
541
+ if day_vals not in applied_day_values: # make a new ScheduleDay and rule
542
+ d_id = '{}_{}'.format(
543
+ identifier, cls._schedule_week_comments[i + 1].title())
544
+ sch_day = ScheduleDay.from_values_at_timestep(d_id, day_vals, timestep)
545
+ rule = ScheduleRule(sch_day)
546
+ rule.apply_day_by_dow(i + 1)
547
+ schedule_rules.append(rule)
548
+ applied_day_values.append(day_vals)
549
+ else: # edit one of the existing rules to apply it to the new day
550
+ for count, sch in enumerate(applied_day_values):
551
+ if day_vals == sch:
552
+ sch_rule_index = count
553
+ rule = schedule_rules[sch_rule_index]
554
+ rule.apply_day_by_dow(i + 1)
555
+
556
+ # get ScheduleDay for the holidays
557
+ holiday = ScheduleDay.from_values_at_timestep(
558
+ '{}_Hol'.format(identifier), holiday_values, timestep)
559
+
560
+ # get ScheduleDay for summer and winter design days
561
+ avg_day_vals = [sum(vals) / len(vals) for vals in applied_day_values]
562
+ temp_type = True if schedule_type_limit is not None and \
563
+ schedule_type_limit.unit_type == 'Temperature' else False
564
+ if summer_designday_values is None:
565
+ sch_i = avg_day_vals.index(min(avg_day_vals)) if temp_type \
566
+ else avg_day_vals.index(max(avg_day_vals))
567
+ summer = schedule_rules[sch_i]._schedule_day.duplicate()
568
+ summer.identifier = '{}_SmrDsn'.format(summer.identifier)
569
+ else:
570
+ summer = ScheduleDay.from_values_at_timestep(
571
+ '{}_SmrDsn'.format(identifier), summer_designday_values, timestep)
572
+ if winter_designday_values is None:
573
+ sch_i = avg_day_vals.index(max(avg_day_vals)) if temp_type \
574
+ else avg_day_vals.index(min(avg_day_vals))
575
+ winter = schedule_rules[sch_i]._schedule_day.duplicate()
576
+ winter.identifier = '{}_WntrDsn'.format(summer.identifier)
577
+ else:
578
+ winter = ScheduleDay.from_values_at_timestep(
579
+ '{}_WntrDsn'.format(identifier), winter_designday_values, timestep)
580
+
581
+ return cls(identifier, schedule_rules[0].schedule_day, schedule_rules[1:],
582
+ schedule_type_limit, holiday, summer, winter)
583
+
584
+ @classmethod
585
+ def from_week_day_schedules(
586
+ cls, identifier, sunday_schedule, monday_schedule, tuesday_schedule,
587
+ wednesday_schedule, thursday_schedule, friday_schedule, saturday_schedule,
588
+ holiday_schedule, summer_designday_schedule, winter_designday_schedule,
589
+ schedule_type_limit=None):
590
+ """Create a ScheduleRuleset from ScheduleDay objects for each day of the week.
591
+
592
+ Args:
593
+ identifier: Text string for a unique Schedule ID. Must be < 100 characters
594
+ and not contain any EnergyPlus special characters. This will be used to
595
+ identify the object across a model and in the exported IDF.
596
+ sunday_schedule: A ScheduleDay for Sundays.
597
+ monday_schedule: A ScheduleDay for Mondays.
598
+ tuesday_schedule: A ScheduleDay for Tuesdays.
599
+ wednesday_schedule: A ScheduleDay for Wednesdays.
600
+ thursday_schedule: A ScheduleDay for Thursdays.
601
+ friday_schedule: A ScheduleDay for Fridays.
602
+ saturday_schedule: A ScheduleDay for Saturdays.
603
+ holiday_schedule: A ScheduleDay for Holidays.
604
+ summer_designday_schedule: A ScheduleDay for the summer design day.
605
+ winter_designday_schedule: A ScheduleDay for the winter design day.
606
+ schedule_type_limit: A ScheduleTypeLimit object that will be used to
607
+ validate schedule values against upper/lower limits and assign
608
+ units to the schedule values.
609
+ """
610
+ schedule_rules = []
611
+ applied_day_ids = []
612
+ all_sched = (sunday_schedule, monday_schedule, tuesday_schedule,
613
+ wednesday_schedule, thursday_schedule, friday_schedule,
614
+ saturday_schedule)
615
+ for i, day_sch in enumerate(all_sched):
616
+ if day_sch.identifier not in applied_day_ids: # make a new rule
617
+ rule = ScheduleRule(day_sch)
618
+ rule.apply_day_by_dow(i + 1)
619
+ schedule_rules.append(rule)
620
+ applied_day_ids.append(day_sch.identifier)
621
+ else: # edit one of the existing rules to apply it to the new day
622
+ sch_rule_index = applied_day_ids.index(day_sch.identifier)
623
+ rule = schedule_rules[sch_rule_index]
624
+ rule.apply_day_by_dow(i + 1)
625
+
626
+ # get ScheduleDay for the holidays
627
+ if holiday_schedule.identifier in applied_day_ids: # avoid duplicate
628
+ holiday_schedule = holiday_schedule.duplicate()
629
+ holiday_schedule.identifier = '{}_Hol'.format(holiday_schedule.identifier)
630
+
631
+ # get ScheduleDay for summer and winter design days
632
+ if summer_designday_schedule.identifier in applied_day_ids: # avoid duplicate
633
+ summer_designday_schedule = summer_designday_schedule.duplicate()
634
+ summer_designday_schedule.identifier = \
635
+ '{}_SmrDsn'.format(summer_designday_schedule.identifier)
636
+ if winter_designday_schedule.identifier in applied_day_ids: # avoid duplicate
637
+ winter_designday_schedule = winter_designday_schedule.duplicate()
638
+ winter_designday_schedule.identifier = \
639
+ '{}_WntrDsn'.format(winter_designday_schedule.identifier)
640
+ return cls(identifier, schedule_rules[0].schedule_day, schedule_rules[1:],
641
+ schedule_type_limit, holiday_schedule, summer_designday_schedule,
642
+ winter_designday_schedule)
643
+
644
+ @classmethod
645
+ def from_idf(cls, year_idf_string, week_idf_strings, day_idf_strings,
646
+ type_idf_string=None):
647
+ """Create a ScheduleRuleset from an EnergyPlus IDF text strings.
648
+
649
+ Args:
650
+ year_idf_string: A text string fully describing an EnergyPlus
651
+ Schedule:Year.
652
+ week_idf_strings: A list of text strings for all of the Schedule:Week
653
+ objects used in the Schedule:Year.
654
+ day_idf_strings: A list of text strings for all of the Schedule:Day
655
+ objects used in the week_idf_strings.
656
+ type_idf_string: An optional text string for the ScheduleTypeLimits.
657
+ If None, the resulting schedule will have no ScheduleTypeLimit.
658
+ """
659
+ # process the schedule components
660
+ day_schedule_dict = cls._idf_day_schedule_dictionary(day_idf_strings)
661
+ week_sch_dict, week_dd_dict = cls._idf_week_schedule_dictionary(
662
+ week_idf_strings, day_schedule_dict)
663
+ schedule_type = ScheduleTypeLimit.from_idf(type_idf_string) if type_idf_string \
664
+ is not None else None
665
+
666
+ # use the year schedule to bring it all together
667
+ year_sch = parse_idf_string(year_idf_string)
668
+ all_rules = []
669
+ for i in range(2, len(year_sch), 5):
670
+ rules = week_sch_dict[year_sch[i]]
671
+ st_date = Date(int(year_sch[i + 1]), int(year_sch[i + 2]))
672
+ end_date = Date(int(year_sch[i + 3]), int(year_sch[i + 4]))
673
+ for rule in rules:
674
+ rule.start_date = st_date
675
+ rule.end_date = end_date
676
+ all_rules.extend(rules)
677
+ default_day_schedule = all_rules[0].schedule_day
678
+ holiday_sch, summer_dd_sch, winter_dd_sch = week_dd_dict[year_sch[2]]
679
+ sched = cls(year_sch[0], default_day_schedule, all_rules[1:], schedule_type)
680
+ cls._apply_designdays_with_check(
681
+ sched, holiday_sch, summer_dd_sch, winter_dd_sch)
682
+ return sched
683
+
684
+ @classmethod
685
+ def from_idf_constant(cls, idf_string, type_idf_string=None):
686
+ """Create a ScheduleRuleset from an EnergyPlus Schedule:Constant string.
687
+
688
+ Args:
689
+ idf_string: A text string fully describing an EnergyPlus Schedule:Constant.
690
+ type_idf_string: An optional text string for the ScheduleTypeLimits.
691
+ If None, the resulting schedule will have no ScheduleTypeLimit.
692
+ """
693
+ const_sch = parse_idf_string(idf_string)
694
+ sched_val = float(const_sch[2]) if const_sch[2] != '' else 0
695
+ schedule_type = ScheduleTypeLimit.from_idf(type_idf_string) if type_idf_string \
696
+ is not None else None
697
+ return ScheduleRuleset.from_constant_value(
698
+ const_sch[0], sched_val, schedule_type)
699
+
700
+ @classmethod
701
+ def from_dict(cls, data):
702
+ """Create a ScheduleRuleset from a dictionary.
703
+
704
+ Note that the dictionary must be a non-abridged version for this
705
+ classmethod to work.
706
+
707
+ Args:
708
+ data: ScheduleRuleset dictionary following the format below.
709
+
710
+ .. code-block:: python
711
+
712
+ {
713
+ "type": 'ScheduleRuleset',
714
+ "identifier": 'Office_Occ_900_1700_weekends',
715
+ "display_name": 'Office Occupancy',
716
+ "day_schedules": [], # Array of ScheduleDay dictionary representations
717
+ "default_day_schedule": str, # ScheduleDay identifier
718
+ "schedule_rules": [], # list of ScheduleRuleAbridged dictionaries
719
+ "schedule_type_limit": {}, # ScheduleTypeLimit dictionary representation
720
+ "holiday_schedule": str, # ScheduleDay identifier
721
+ "summer_designday_schedule": str, # ScheduleDay identifier
722
+ "winter_designday_schedule": str # ScheduleDay identifier
723
+ }
724
+ """
725
+ assert data['type'] == 'ScheduleRuleset', \
726
+ 'Expected ScheduleRuleset. Got {}.'.format(data['type'])
727
+
728
+ sch_day_dict = {}
729
+ for day_sch in data['day_schedules']:
730
+ sch_day_dict[day_sch['identifier']] = ScheduleDay.from_dict(day_sch)
731
+
732
+ default_sched = sch_day_dict[data['default_day_schedule']]
733
+ rules = None
734
+ if 'schedule_rules' in data and data['schedule_rules'] is not None:
735
+ rules = []
736
+ for rule in data['schedule_rules']:
737
+ sch_day = sch_day_dict[rule['schedule_day']]
738
+ rules.append(ScheduleRule.from_dict_abridged(rule, sch_day))
739
+ holiday_sched = None
740
+ if 'holiday_schedule' in data and data['holiday_schedule'] is not None:
741
+ holiday_sched = sch_day_dict[data['holiday_schedule']]
742
+ summer_sched = None
743
+ if 'summer_designday_schedule' in data and \
744
+ data['summer_designday_schedule'] is not None:
745
+ summer_sched = sch_day_dict[data['summer_designday_schedule']]
746
+ winter_sched = None
747
+ if 'winter_designday_schedule' in data and \
748
+ data['winter_designday_schedule'] is not None:
749
+ winter_sched = sch_day_dict[data['winter_designday_schedule']]
750
+
751
+ sched_type = None
752
+ if 'schedule_type_limit' in data and data['schedule_type_limit'] is not None:
753
+ sched_type = ScheduleTypeLimit.from_dict(data['schedule_type_limit'])
754
+
755
+ new_obj = cls(data['identifier'], default_sched, rules, sched_type,
756
+ holiday_sched, summer_sched, winter_sched)
757
+ if 'display_name' in data and data['display_name'] is not None:
758
+ new_obj.display_name = data['display_name']
759
+ if 'user_data' in data and data['user_data'] is not None:
760
+ new_obj.user_data = data['user_data']
761
+ if 'properties' in data and data['properties'] is not None:
762
+ new_obj.properties._load_extension_attr_from_dict(data['properties'])
763
+ return new_obj
764
+
765
+ @classmethod
766
+ def from_dict_abridged(cls, data, schedule_type_limits):
767
+ """Create a ScheduleRuleset from an abridged dictionary.
768
+
769
+ Args:
770
+ data: ScheduleRulesetAbridged dictionary.
771
+ schedule_type_limits: A dictionary with identifiers of schedule type limits
772
+ as keys and Python schedule type limit objects as values.
773
+
774
+ .. code-block:: python
775
+
776
+ {
777
+ "type": 'ScheduleRulesetAbridged',
778
+ "identifier": 'Office_Occ_900_1700_weekends',
779
+ "display_name": 'Office Occupancy',
780
+ "day_schedules": [], # Array of ScheduleDay dictionary representations
781
+ "default_day_schedule": str, # ScheduleDay identifier
782
+ "schedule_rules": [], # list of ScheduleRuleAbridged dictionaries
783
+ "schedule_type_limit": str, # ScheduleTypeLimit identifier
784
+ "holiday_schedule": str, # ScheduleDay identifier
785
+ "summer_designday_schedule": str, # ScheduleDay identifier
786
+ "winter_designday_schedule": str # ScheduleDay identifier
787
+ }
788
+ """
789
+ assert data['type'] == 'ScheduleRulesetAbridged', \
790
+ 'Expected ScheduleRulesetAbridged. Got {}.'.format(data['type'])
791
+
792
+ data = data.copy() # copy original dictionary so we don't edit it
793
+ typ_lim = None
794
+ if 'schedule_type_limit' in data:
795
+ typ_lim = data['schedule_type_limit']
796
+ data['schedule_type_limit'] = None
797
+ data['type'] = 'ScheduleRuleset'
798
+ schedule = cls.from_dict(data)
799
+ schedule.schedule_type_limit = schedule_type_limits[typ_lim] if \
800
+ typ_lim is not None else None
801
+ if 'display_name' in data and data['display_name'] is not None:
802
+ schedule.display_name = data['display_name']
803
+ if 'user_data' in data and data['user_data'] is not None:
804
+ schedule.user_data = data['user_data']
805
+ if 'properties' in data and data['properties'] is not None:
806
+ schedule.properties._load_extension_attr_from_dict(data['properties'])
807
+ return schedule
808
+
809
+ def to_rules(self, start_date, end_date):
810
+ """Get all of rules needed to implement this ScheduleRuleset over a date range.
811
+
812
+ This is useful when you want to apply this entire ScheduleRuleset over a
813
+ particular time period of another ScheduleRuleset.
814
+
815
+ Args:
816
+ start_date: A ladybug Date object for the start of the period that rules
817
+ should be obtained.
818
+ end_date: A ladybug Date object for the end of the period that rules
819
+ should be obtained.
820
+ """
821
+ # check the date inputs
822
+ ScheduleRule._check_date(start_date)
823
+ ScheduleRule._check_date(end_date)
824
+ st_doy = ScheduleRule._doy_non_leap_year(start_date)
825
+ end_doy = ScheduleRule._doy_non_leap_year(end_date)
826
+ assert start_date <= end_date, \
827
+ 'Start date must be before end date for ScheduleRuleset.to_rules().'
828
+
829
+ # assemble all of the rules already applied to this ScheduleRuleset
830
+ rules = []
831
+ for rule in self._schedule_rules:
832
+ if not rule.is_reversed:
833
+ if (rule._start_doy < st_doy and rule._end_doy < st_doy) or \
834
+ (rule._start_doy > st_doy and rule._end_doy > end_doy):
835
+ pass # no overlap with input period
836
+ else:
837
+ new_rule = rule.duplicate()
838
+ if rule._start_doy < st_doy:
839
+ new_rule.start_date = start_date
840
+ if rule._end_doy > end_doy:
841
+ new_rule.end_date = end_date
842
+ rules.append(new_rule)
843
+ else:
844
+ if rule._start_doy <= end_doy:
845
+ new_rule1 = rule.duplicate()
846
+ new_rule1.end_date = end_doy
847
+ rules.append(new_rule1)
848
+ if rule._end_doy >= st_doy:
849
+ new_rule2 = rule.duplicate()
850
+ new_rule2.start_date = st_doy
851
+ rules.append(new_rule2)
852
+
853
+ # add the default_day_schedule for all days not covered by rules
854
+ default_rule = ScheduleRule(self.default_day_schedule.duplicate(),
855
+ start_date=start_date, end_date=end_date)
856
+ for dow in range(7):
857
+ for rule in rules:
858
+ if rule.week_apply_tuple[dow]:
859
+ break
860
+ else: # no rule applies; use default_day_schedule.
861
+ default_rule.apply_day_by_dow(dow + 1)
862
+ rules.append(default_rule)
863
+
864
+ return rules
865
+
866
+ def to_idf(self):
867
+ """IDF string representation of the schedule.
868
+
869
+ Note that this method only outputs Schedule:Year and Schedule:Week objects
870
+ (or a Schedule:Constant object if applicable). However, to write the full
871
+ schedule into an IDF, the schedules's day_schedules must also be
872
+ written as well as the ScheduleTypeLimit object.
873
+
874
+ The method is set up this way primarily to give better control over the export
875
+ process. For example, you usually want to see if there are other schedules
876
+ in a model using the same ScheduleTypeLimit object and then write it into
877
+ the IDF only once rather than writing it multiple times for each schedule
878
+ that references it. ScheduleDay objects can often follow a similar logic
879
+ where the same ScheduleDay objects are used by multiple ScheduleRulesets.
880
+
881
+ Returns:
882
+ A tuple with two elements
883
+
884
+ - year_schedule: Text string representation of the Schedule:Year
885
+ describing this schedule. This will be a Schedule:Constant if this
886
+ schedule can be described as such.
887
+
888
+ - week_schedules: A list of Schedule:Week:Daily test strings that are
889
+ referenced in the year_schedule. Will be None when year_schedule is
890
+ a Schedule:Constant.
891
+
892
+ .. code-block:: shell
893
+
894
+ Schedule:Year,
895
+ test_name, !- Name
896
+ 'Availability', !- Schedule Type Limits Name
897
+ avail_schedge1, !- Schedule:Week Name 1
898
+ 1, !- Start Month 1
899
+ 1, !- Start Day 1
900
+ 12, !- End Month 1
901
+ 31; !- End Day 1
902
+ """
903
+ # beginning fields used for all schedules
904
+ year_fields = [self.identifier]
905
+ shc_typ = self._schedule_type_limit.identifier if \
906
+ self._schedule_type_limit is not None else ''
907
+ year_fields.append(shc_typ)
908
+ year_comments = ['schedule name', 'schedule type limits']
909
+
910
+ # check if this schedule can simply be represented with a Schedule:Constant
911
+ if self.is_constant:
912
+ year_fields.append(self.default_day_schedule[0])
913
+ year_comments.append('value')
914
+ year_schedule = generate_idf_string(
915
+ 'Schedule:Constant', year_fields, year_comments)
916
+ return year_schedule, None
917
+
918
+ # prepare to create a full Schedule:Year
919
+ date_comments = ['start month {}', 'start day {}', 'end month {}', 'end day {}']
920
+ week_schedules = []
921
+
922
+ if self.is_single_week: # create the only one week schedule
923
+ wk_sch, wk_sch_id = \
924
+ self._idf_week_schedule_from_rule_indices(range(len(self)), 1)
925
+ week_schedules.append(wk_sch)
926
+ yr_wk_s_ids = [wk_sch_id]
927
+ yr_wk_dt_range = [[Date(1, 1), Date(12, 31)]]
928
+ else: # create a set of week schedules throughout the year
929
+ # loop through 365 days of the year to find unique combinations of rules
930
+ rules_each_day = []
931
+ for doy in range(1, 366):
932
+ rules_on_doy = tuple(i for i, rule in enumerate(self._schedule_rules)
933
+ if rule.does_rule_apply_doy(doy))
934
+ rules_each_day.append(rules_on_doy)
935
+ unique_rule_sets = set(rules_each_day)
936
+ # check if any combination yield the same week schedule and remove duplicates
937
+ week_tuples = [tuple(self._get_week_list(rule_set))
938
+ for rule_set in unique_rule_sets]
939
+ unique_week_tuples = list(set(week_tuples))
940
+ # create the unique week schedules from the combinations of rules
941
+ week_sched_ids = []
942
+ for i, week_list in enumerate(unique_week_tuples):
943
+ wk_schedule, wk_sch_id = \
944
+ self._idf_week_schedule_from_week_list(week_list, i + 1)
945
+ week_schedules.append(wk_schedule)
946
+ week_sched_ids.append(wk_sch_id)
947
+ # create a dictionary mapping unique rule index lists to week schedule ids
948
+ rule_set_map = {}
949
+ for rule_i, week_list in zip(unique_rule_sets, week_tuples):
950
+ unique_week_i = unique_week_tuples.index(week_list)
951
+ rule_set_map[rule_i] = week_sched_ids[unique_week_i]
952
+ # loop through all 365 days of the year to find when rules change
953
+ yr_wk_s_ids = []
954
+ yr_wk_dt_range = []
955
+ prev_week_sched = None
956
+ for doy in range(1, 366):
957
+ week_sched = rule_set_map[rules_each_day[doy - 1]]
958
+ if week_sched != prev_week_sched: # change to a new rule set
959
+ yr_wk_s_ids.append(week_sched)
960
+ if doy != 1:
961
+ yr_wk_dt_range[-1].append(Date.from_doy(doy - 1))
962
+ yr_wk_dt_range.append([Date.from_doy(doy)])
963
+ else:
964
+ yr_wk_dt_range.append([Date(1, 1)])
965
+ prev_week_sched = week_sched
966
+ yr_wk_dt_range[-1].append(Date(12, 31))
967
+
968
+ # create the year fields and comments
969
+ for i, (wk_sch_id, dt_range) in enumerate(zip(yr_wk_s_ids, yr_wk_dt_range)):
970
+ year_fields.append(wk_sch_id)
971
+ count = i + 1
972
+ year_comments.append('week schedule name {}'.format(count))
973
+ year_fields.extend([dt_range[0].month, dt_range[0].day,
974
+ dt_range[1].month, dt_range[1].day])
975
+ for com in date_comments:
976
+ year_comments.append(com.format(count))
977
+
978
+ year_schedule = generate_idf_string('Schedule:Year', year_fields, year_comments)
979
+ return year_schedule, week_schedules
980
+
981
+ def to_dict(self, abridged=False):
982
+ """Schedule Ruleset dictionary representation.
983
+
984
+ Args:
985
+ abridged: Boolean to note whether the full dictionary describing the
986
+ object should be returned (False) or just an abridged version (True),
987
+ which only specifies the identifier of the ScheduleTypeLimit.
988
+ Default: False.
989
+ """
990
+ # required properties
991
+ base = {'type': 'ScheduleRuleset'} if not \
992
+ abridged else {'type': 'ScheduleRulesetAbridged'}
993
+ base['identifier'] = self.identifier
994
+ base['day_schedules'] = [sch_day.to_dict() for sch_day in self.day_schedules]
995
+ base['default_day_schedule'] = self.default_day_schedule.identifier
996
+
997
+ # optional properties
998
+ if len(self._schedule_rules) != 0:
999
+ base['schedule_rules'] = \
1000
+ [rule.to_dict(True) for rule in self._schedule_rules]
1001
+ if self._holiday_schedule is not None:
1002
+ base['holiday_schedule'] = self._holiday_schedule.identifier
1003
+ if self._summer_designday_schedule is not None:
1004
+ base['summer_designday_schedule'] = \
1005
+ self._summer_designday_schedule.identifier
1006
+ if self._winter_designday_schedule is not None:
1007
+ base['winter_designday_schedule'] = \
1008
+ self._winter_designday_schedule.identifier
1009
+
1010
+ # optional properties that can be abridged
1011
+ if self._schedule_type_limit is not None:
1012
+ if not abridged:
1013
+ base['schedule_type_limit'] = self._schedule_type_limit.to_dict()
1014
+ else:
1015
+ base['schedule_type_limit'] = self._schedule_type_limit.identifier
1016
+ if self._display_name is not None:
1017
+ base['display_name'] = self.display_name
1018
+ if self._user_data is not None:
1019
+ base['user_data'] = self.user_data
1020
+ prop_dict = self.properties.to_dict()
1021
+ if prop_dict is not None:
1022
+ base['properties'] = prop_dict
1023
+ return base
1024
+
1025
+ def duplicate(self):
1026
+ """Get a copy of this object."""
1027
+ return self.__copy__()
1028
+
1029
+ def lock(self):
1030
+ """The lock() method also locks the ScheduleDay and ScheduleRule objects."""
1031
+ self._locked = True
1032
+ self._default_day_schedule.lock()
1033
+ if self._holiday_schedule is not None:
1034
+ self._holiday_schedule.lock()
1035
+ if self._summer_designday_schedule is not None:
1036
+ self._summer_designday_schedule.lock()
1037
+ if self._winter_designday_schedule is not None:
1038
+ self._winter_designday_schedule.lock()
1039
+ for rule in self._schedule_rules:
1040
+ rule.lock()
1041
+
1042
+ def unlock(self):
1043
+ """The unlock() method also unlocks the ScheduleDay and ScheduleRule objects."""
1044
+ self._locked = False
1045
+ self._default_day_schedule.unlock()
1046
+ if self._holiday_schedule is not None:
1047
+ self._holiday_schedule.unlock()
1048
+ if self._summer_designday_schedule is not None:
1049
+ self._summer_designday_schedule.unlock()
1050
+ if self._winter_designday_schedule is not None:
1051
+ self._winter_designday_schedule.unlock()
1052
+ for rule in self._schedule_rules:
1053
+ rule.unlock()
1054
+
1055
+ @staticmethod
1056
+ def extract_all_from_idf_file(idf_file, import_compact=False):
1057
+ """Extract all ScheduleRuleset objects from an EnergyPlus IDF file.
1058
+
1059
+ Args:
1060
+ idf_file: A path to an IDF file containing objects for Schedule:Year and
1061
+ corresponding Schedule:Week and Schedule:Day objects. The Schedule:Year
1062
+ will be used to assemble all of these into a ScheduleRuleset.
1063
+ import_compact: Boolean to note whether to parse Schedule:Compact (True)
1064
+ or not (False).
1065
+ Default: False.
1066
+
1067
+ Returns:
1068
+ schedules --
1069
+ A list of all Schedule:Year objects in the IDF file as honeybee_energy
1070
+ ScheduleRuleset objects.
1071
+ """
1072
+ # read the file and remove lines of comments
1073
+ file_contents = clean_idf_file_contents(idf_file)
1074
+ # extract all of the ScheduleDay objects
1075
+ day_pattern1 = re.compile(r"(?i)(Schedule:Day:Interval,[\s\S]*?;)")
1076
+ day_pattern2 = re.compile(r"(?i)(Schedule:Day:Hourly,[\s\S]*?;)")
1077
+ day_pattern3 = re.compile(r"(?i)(Schedule:Day:List,[\s\S]*?;)")
1078
+ day_sch_str = day_pattern1.findall(file_contents) + \
1079
+ day_pattern2.findall(file_contents) + day_pattern3.findall(file_contents)
1080
+ day_schedule_dict = ScheduleRuleset._idf_day_schedule_dictionary(day_sch_str)
1081
+ # extract all of the Schedule:Week objects
1082
+ week_pattern_1 = re.compile(r"(?i)(Schedule:Week:Daily,[\s\S]*?;)")
1083
+ week_pattern_2 = re.compile(r"(?i)(Schedule:Week:Compact,[\s\S]*?;)")
1084
+ week_sch_str = week_pattern_1.findall(file_contents) + \
1085
+ week_pattern_2.findall(file_contents)
1086
+ week_sch_dict, week_dd_dict = ScheduleRuleset._idf_week_schedule_dictionary(
1087
+ week_sch_str, day_schedule_dict)
1088
+ # extract all of the ScheduleTypeLimit objects
1089
+ type_pattern = re.compile(r"(?i)(ScheduleTypeLimits,[\s\S]*?;)")
1090
+ sch_type_str = type_pattern.findall(file_contents)
1091
+ sch_type_dict = ScheduleRuleset._idf_schedule_type_dictionary(sch_type_str)
1092
+ # extract all of the Schedule:Year objects and convert to ScheduleRuleset
1093
+ year_pattern = re.compile(r"(?i)(Schedule:Year,[\s\S]*?;)")
1094
+ year_props = tuple(parse_idf_string(idf_string) for
1095
+ idf_string in year_pattern.findall(file_contents))
1096
+ # extract all of the Schedule:Constant objects and convert to ScheduleRuleset
1097
+ constant_pattern = re.compile(r"(?i)(Schedule:Constant,[\s\S]*?;)")
1098
+ constant_props = tuple(parse_idf_string(idf_string) for
1099
+ idf_string in constant_pattern.findall(file_contents))
1100
+ # extract all of the Schedule:Compact objects and convert to ScheduleRuleset
1101
+ conpact_pattern = re.compile(r"(?i)(Schedule:Compact,[\s\S]*?;)")
1102
+ compact_props = (
1103
+ tuple(
1104
+ parse_idf_string(idf_string)
1105
+ for idf_string in conpact_pattern.findall(file_contents)
1106
+ )
1107
+ if import_compact # only if user chooses so.
1108
+ else []
1109
+ )
1110
+
1111
+ # compile all of the ScheduleRuleset objects from extracted properties
1112
+ schedules = []
1113
+ for year_sch in year_props:
1114
+ # gather the rules
1115
+ all_rules = []
1116
+ for i in range(2, len(year_sch), 5):
1117
+ rules = week_sch_dict[year_sch[i]]
1118
+ st_date = Date(int(year_sch[i + 1]), int(year_sch[i + 2]))
1119
+ end_date = Date(int(year_sch[i + 3]), int(year_sch[i + 4]))
1120
+ for rule in rules:
1121
+ rule.end_date = end_date
1122
+ rule.start_date = st_date
1123
+ all_rules.extend(rules)
1124
+ # gather the other day schedules
1125
+ holiday_sch, summer_dd_sch, winter_dd_sch = week_dd_dict[year_sch[2]]
1126
+ schedule_type = sch_type_dict[year_sch[1]] if year_sch[1] != '' else None
1127
+ # check to be sure the schedule days don't already have a parent
1128
+ for rule in all_rules:
1129
+ if rule.schedule_day._parent is not None:
1130
+ rule.schedule_day = rule.schedule_day.duplicate()
1131
+ if holiday_sch._parent is not None:
1132
+ holiday_sch = holiday_sch.duplicate()
1133
+ if summer_dd_sch._parent is not None:
1134
+ summer_dd_sch = summer_dd_sch.duplicate()
1135
+ if winter_dd_sch._parent is not None:
1136
+ winter_dd_sch = summer_dd_sch.duplicate()
1137
+ # create the ScheduleRuleset
1138
+ default_day_schedule = all_rules[0].schedule_day
1139
+ sch_ruleset = ScheduleRuleset(
1140
+ year_sch[0], default_day_schedule, all_rules[1:], schedule_type)
1141
+ ScheduleRuleset._apply_designdays_with_check(
1142
+ sch_ruleset, holiday_sch, summer_dd_sch, winter_dd_sch)
1143
+ schedules.append(sch_ruleset)
1144
+ for const_sch in constant_props:
1145
+ sched_val = float(const_sch[2]) if const_sch[2] != '' else 0
1146
+ schedule_type = sch_type_dict[const_sch[1]] if const_sch[1] != '' else None
1147
+ sch_ruleset = ScheduleRuleset.from_constant_value(
1148
+ const_sch[0], sched_val, schedule_type)
1149
+ schedules.append(sch_ruleset)
1150
+ for compact_sch in compact_props:
1151
+ schedule_type = (
1152
+ sch_type_dict[compact_sch[1]] if compact_sch[1] != "" else None
1153
+ )
1154
+ schedule_rules = []
1155
+ holiday_schedule = None
1156
+ winter_designday_schedule = None
1157
+ summer_designday_schedule = None
1158
+ start_date = Date.from_doy(1)
1159
+ end_date = Date.from_doy(365)
1160
+ untils = [Time(0, 0)] # initialize list of until times.
1161
+ n = -1 # initialize the n-th until time
1162
+ rules = [] # initialize list of rules.
1163
+ for field in compact_sch[2:]:
1164
+ field = field.lower()
1165
+ if "through" in field:
1166
+ # Each `through` field generates a new ScheduleRule
1167
+ # initialize rule with ScheduleDay as placeholder.
1168
+ rule = ScheduleRule(ScheduleDay(compact_sch[0], [0], [Time(0, 0)]))
1169
+
1170
+ # start_date is either Jan 1st or end_date from previous
1171
+ # `through` field
1172
+ start_date = (
1173
+ start_date if end_date == Date.from_doy(365) else end_date
1174
+ )
1175
+
1176
+ _, date = field.split(":") # get end_date from field
1177
+ month, day = date.split("/")
1178
+ end_date = Date.from_dict({"month": int(month), "day": int(day)})
1179
+ elif "for" in field:
1180
+ # reset values for new set; each `for` is a new rule
1181
+ n = -1 # reset the n-th until time
1182
+ untils = [Time(0, 0)] # reset list of until times.
1183
+ rules = [] # reset list of rules for this `for` field.
1184
+
1185
+ # Create a rule; all different `if` statements because we want to
1186
+ # catch more than one case,
1187
+ # eg. `For: Sunday Holidays AllOtherDays, !- Field 54`
1188
+ def create_rule_for(apply_to):
1189
+ """Return ScheduleRule with the `apply_to` rule set to True."""
1190
+ apply_to_attr_map = {
1191
+ "alldays": "apply_all",
1192
+ "weekdays": "apply_weekday",
1193
+ "weekends": "apply_weekend",
1194
+ "sunday": "apply_sunday",
1195
+ "monday": "apply_monday",
1196
+ "tuesday": "apply_tuesday",
1197
+ "wednesday": "apply_wednesday",
1198
+ "thursday": "apply_thursday",
1199
+ "friday": "apply_friday",
1200
+ "saturday": "apply_saturday",
1201
+ }
1202
+ rule = ScheduleRule(ScheduleDay(apply_to, [0], [Time(0, 0)]))
1203
+ setattr(rule, apply_to_attr_map[apply_to], True)
1204
+ return rule
1205
+
1206
+ # Create rules for regular days
1207
+ for rule_name in [
1208
+ "alldays",
1209
+ "weekdays",
1210
+ "weekends",
1211
+ "sunday",
1212
+ "monday",
1213
+ "tuesday",
1214
+ "wednesday",
1215
+ "thursday",
1216
+ "friday",
1217
+ "saturday",
1218
+ ]:
1219
+ if rule_name in field:
1220
+ rule = create_rule_for(rule_name)
1221
+ rules.append(rule)
1222
+
1223
+ # Create rules for holidays and designdays
1224
+ if "holiday" in field:
1225
+ rule = ScheduleRule(ScheduleDay("holiday", [0], [Time(0, 0)]))
1226
+ holiday_schedule = rule.schedule_day
1227
+ rules.append(rule)
1228
+ if "summerdesignday" in field:
1229
+ rule = ScheduleRule(
1230
+ ScheduleDay("summerdesignday", [0], [Time(0, 0)])
1231
+ )
1232
+ summer_designday_schedule = rule.schedule_day
1233
+ rules.append(rule)
1234
+ if "winterdesignday" in field:
1235
+ rule = ScheduleRule(
1236
+ ScheduleDay("winterdesignday", [0], [Time(0, 0)])
1237
+ )
1238
+ winter_designday_schedule = rule.schedule_day
1239
+ rules.append(rule)
1240
+ if "allotherdays" in field:
1241
+ rule = ScheduleRule(
1242
+ ScheduleDay("allotherdays", [0], [Time(0, 0)])
1243
+ )
1244
+ apply_mtx = [rul.week_apply_tuple for rul in schedule_rules]
1245
+ if not apply_mtx: # situation if allotherdays is the only rule.
1246
+ rule.apply_all = True
1247
+ else:
1248
+ for j, dow in enumerate(zip(*apply_mtx)):
1249
+ if not any(dow):
1250
+ rule.apply_day_by_dow(j + 1)
1251
+ rules.append(rule)
1252
+
1253
+ for rule in rules:
1254
+ # for each created rule in this `for` field, add rules to
1255
+ # ScheduleRuleset list of rules and set start_date and end_date.
1256
+ if len(rule.days_applied) != 0:
1257
+ schedule_rules.append(rule)
1258
+
1259
+ # set range for rule (from previous `through` field)
1260
+ rule.start_date = start_date
1261
+ rule.end_date = end_date
1262
+ elif "until" in field:
1263
+ _, hour, min = field.split(":") # get hour and minutes
1264
+
1265
+ # value is applied `until` a certain Time, but `ScheduleDay` is
1266
+ # applied `from` a certain Time. Also, Time is 0:23 Hours while IDF
1267
+ # is 1:24 Hours.
1268
+ until = Time(int(hour) - 1, int(min)) # to 0:23 Hours repr
1269
+ untils.append(until)
1270
+
1271
+ # increment n
1272
+ n += 1
1273
+ elif "interpolate" in field:
1274
+ # Set interpolate on all rules for this `for` field
1275
+ _, interpolate = field.split(":")
1276
+ for rule in rules:
1277
+ rule.schedule_day.interpolate = interpolate != "no"
1278
+ else:
1279
+ begin = untils[n] # index list of `until` times
1280
+ for rule in rules:
1281
+ # apply field value for each created rules; try to replace the
1282
+ # placeholder value first, else add the value.
1283
+ try:
1284
+ rule.schedule_day.replace_value_by_time(begin, float(field))
1285
+ except ValueError:
1286
+ rule.schedule_day.add_value(float(field), begin)
1287
+ default_day_schedule = schedule_rules[0].schedule_day
1288
+ sch_ruleset = ScheduleRuleset(
1289
+ default_day_schedule=default_day_schedule,
1290
+ identifier=compact_sch[0],
1291
+ schedule_type_limit=schedule_type,
1292
+ schedule_rules=schedule_rules[1:]
1293
+ )
1294
+ ScheduleRuleset._apply_designdays_with_check(
1295
+ sch_ruleset,
1296
+ holiday_schedule,
1297
+ summer_designday_schedule,
1298
+ winter_designday_schedule)
1299
+ schedules.append(sch_ruleset)
1300
+ return schedules
1301
+
1302
+ @staticmethod
1303
+ def average_schedules(identifier, schedules, weights=None, timestep_resolution=1):
1304
+ """Create a ScheduleRuleset that is a weighted average between ScheduleRulesets.
1305
+
1306
+ Args:
1307
+ identifier: Text string for a unique ID for the new unique ScheduleRuleset.
1308
+ Must be < 100 characters and not contain any EnergyPlus special
1309
+ characters. This will be used to identify the object across a
1310
+ model and in the exported IDF.
1311
+ schedules: A list of ScheduleRuleset objects that will be averaged together
1312
+ to make a new ScheduleRuleset.
1313
+ weights: An optional list of fractional numbers with the same length
1314
+ as the input schedules that sum to 1. These will be used to weight
1315
+ each of the ScheduleRuleset objects in the resulting average schedule.
1316
+ If None, the individual schedules will be weighted equally.
1317
+ timestep_resolution: An optional integer for the timestep resolution
1318
+ at which the schedules will be averaged. Any schedule details
1319
+ smaller than this timestep will be lost in the averaging process.
1320
+ Default: 1.
1321
+ """
1322
+ # check the inputs
1323
+ assert isinstance(schedules, (list, tuple)), 'Expected a list of ' \
1324
+ 'ScheduleRuleset objects for average_schedules. ' \
1325
+ 'Got {}.'.format(type(schedules))
1326
+ if weights is None:
1327
+ weight = 1 / len(schedules)
1328
+ weights = [weight for i in schedules]
1329
+ else:
1330
+ weights = tuple_with_length(weights, len(schedules), float,
1331
+ 'average schedules weights')
1332
+ assert abs(sum(weights) - 1.0) <= 1e-9, 'Average schedule weights must ' \
1333
+ 'sum to 1. Got {}.'.format(sum(weights))
1334
+
1335
+ # if all input schedules are single week, the averaging process is a lot simpler
1336
+ if all([sched.is_single_week for sched in schedules]):
1337
+ rule_indices = [range(len(sched)) for sched in schedules]
1338
+ return ScheduleRuleset._get_avg_week(
1339
+ identifier, schedules, weights, timestep_resolution, rule_indices)
1340
+ else:
1341
+ # loop through 365 days of the year to find unique combinations of rules
1342
+ rules_each_day = []
1343
+ for doy in range(1, 366):
1344
+ rules_on_doy = tuple(tuple(
1345
+ i for i, rule in enumerate(sched._schedule_rules)
1346
+ if rule._start_doy <= doy <= rule._end_doy)
1347
+ for sched in schedules)
1348
+ rules_each_day.append(rules_on_doy)
1349
+ unique_rule_sets = set(rules_each_day)
1350
+ # create the average week schedules from the unique combinations of rules
1351
+ week_schedules = []
1352
+ for i, rule_indices in enumerate(unique_rule_sets):
1353
+ week_identifier = '{}_{}'.format(identifier, i)
1354
+ week_sched = ScheduleRuleset._get_avg_week(
1355
+ week_identifier, schedules, weights,
1356
+ timestep_resolution, rule_indices
1357
+ )
1358
+ week_schedules.append(week_sched)
1359
+ # combine the week schedules into rules
1360
+ final_rules, holiday_sch, summer_dd_sch, winter_dd_sch = \
1361
+ ScheduleRuleset._combine_week_schedules(
1362
+ unique_rule_sets, week_schedules, rules_each_day)
1363
+ # add all rules to a final ScheduleRuleset
1364
+ default_day_schedule = final_rules[0].schedule_day
1365
+ schedule_type = schedules[0].schedule_type_limit
1366
+ return ScheduleRuleset(
1367
+ identifier, default_day_schedule, final_rules[1:], schedule_type,
1368
+ holiday_sch, summer_dd_sch, winter_dd_sch)
1369
+
1370
+ @staticmethod
1371
+ def max_schedules(identifier, schedules, timestep_resolution=1):
1372
+ """Create a ScheduleRuleset that uses the maximum value between ScheduleRulesets.
1373
+
1374
+ This is useful for resolving a set of schedules where the highest value
1375
+ should govern, like when several ventilation schedules must be merged
1376
+ into one.
1377
+
1378
+ Args:
1379
+ identifier: Text string for a unique ID for the new unique ScheduleRuleset.
1380
+ Must be < 100 characters and not contain any EnergyPlus special
1381
+ characters. This will be used to identify the object across a
1382
+ model and in the exported IDF.
1383
+ schedules: A list of ScheduleRuleset objects that will have the maximum
1384
+ value taken at each timestep to make a new ScheduleRuleset.
1385
+ timestep_resolution: An optional integer for the timestep resolution
1386
+ at which the schedules will be combined. Any schedule details
1387
+ smaller than this timestep will be lost in the process. (Default: 1).
1388
+ """
1389
+ # check the inputs
1390
+ assert isinstance(schedules, (list, tuple)), 'Expected a list of ' \
1391
+ 'ScheduleRuleset objects for max_schedules. Got {}.'.format(type(schedules))
1392
+
1393
+ # if all input schedules are single week, the process is a lot simpler
1394
+ if all([sched.is_single_week for sched in schedules]):
1395
+ rule_indices = [range(len(sched)) for sched in schedules]
1396
+ return ScheduleRuleset._get_ext_week(
1397
+ identifier, schedules, max, timestep_resolution, rule_indices)
1398
+ else:
1399
+ # loop through 365 days of the year to find unique combinations of rules
1400
+ rules_each_day = []
1401
+ for doy in range(1, 366):
1402
+ rules_on_doy = tuple(tuple(
1403
+ i for i, rule in enumerate(sched._schedule_rules)
1404
+ if rule._start_doy <= doy <= rule._end_doy)
1405
+ for sched in schedules)
1406
+ rules_each_day.append(rules_on_doy)
1407
+ unique_rule_sets = set(rules_each_day)
1408
+ # create the combined week schedules from the unique combinations of rules
1409
+ week_schedules = []
1410
+ for i, rule_indices in enumerate(unique_rule_sets):
1411
+ week_identifier = '{}_{}'.format(identifier, i)
1412
+ week_sched = ScheduleRuleset._get_ext_week(
1413
+ week_identifier, schedules, max,
1414
+ timestep_resolution, rule_indices
1415
+ )
1416
+ week_schedules.append(week_sched)
1417
+ # combine the week schedules into rules
1418
+ final_rules, holiday_sch, summer_dd_sch, winter_dd_sch = \
1419
+ ScheduleRuleset._combine_week_schedules(
1420
+ unique_rule_sets, week_schedules, rules_each_day)
1421
+ # add all rules to a final ScheduleRuleset
1422
+ default_day_schedule = final_rules[0].schedule_day
1423
+ schedule_type = schedules[0].schedule_type_limit
1424
+ return ScheduleRuleset(
1425
+ identifier, default_day_schedule, final_rules[1:], schedule_type,
1426
+ holiday_sch, summer_dd_sch, winter_dd_sch)
1427
+
1428
+ @staticmethod
1429
+ def min_schedules(identifier, schedules, timestep_resolution=1):
1430
+ """Create a ScheduleRuleset that uses the minimum value between ScheduleRulesets.
1431
+
1432
+ This is useful for resolving a set of schedules where the lowest value
1433
+ should govern, like when several cooling setpoint schedules must be merged
1434
+ into one.
1435
+
1436
+ Args:
1437
+ identifier: Text string for a unique ID for the new unique ScheduleRuleset.
1438
+ Must be < 100 characters and not contain any EnergyPlus special
1439
+ characters. This will be used to identify the object across a
1440
+ model and in the exported IDF.
1441
+ schedules: A list of ScheduleRuleset objects that will have the minimum
1442
+ value taken at each timestep to make a new ScheduleRuleset.
1443
+ timestep_resolution: An optional integer for the timestep resolution
1444
+ at which the schedules will be combined. Any schedule details
1445
+ smaller than this timestep will be lost in the process. (Default: 1).
1446
+ """
1447
+ # check the inputs
1448
+ assert isinstance(schedules, (list, tuple)), 'Expected a list of ' \
1449
+ 'ScheduleRuleset objects for min_schedules. Got {}.'.format(type(schedules))
1450
+
1451
+ # if all input schedules are single week, the process is a lot simpler
1452
+ if all([sched.is_single_week for sched in schedules]):
1453
+ rule_indices = [range(len(sched)) for sched in schedules]
1454
+ return ScheduleRuleset._get_ext_week(
1455
+ identifier, schedules, min, timestep_resolution, rule_indices)
1456
+ else:
1457
+ # loop through 365 days of the year to find unique combinations of rules
1458
+ rules_each_day = []
1459
+ for doy in range(1, 366):
1460
+ rules_on_doy = tuple(tuple(
1461
+ i for i, rule in enumerate(sched._schedule_rules)
1462
+ if rule._start_doy <= doy <= rule._end_doy)
1463
+ for sched in schedules)
1464
+ rules_each_day.append(rules_on_doy)
1465
+ unique_rule_sets = set(rules_each_day)
1466
+ # create the combined week schedules from the unique combinations of rules
1467
+ week_schedules = []
1468
+ for i, rule_indices in enumerate(unique_rule_sets):
1469
+ week_identifier = '{}_{}'.format(identifier, i)
1470
+ week_sched = ScheduleRuleset._get_ext_week(
1471
+ week_identifier, schedules, min,
1472
+ timestep_resolution, rule_indices
1473
+ )
1474
+ week_schedules.append(week_sched)
1475
+ # combine the week schedules into rules
1476
+ final_rules, holiday_sch, summer_dd_sch, winter_dd_sch = \
1477
+ ScheduleRuleset._combine_week_schedules(
1478
+ unique_rule_sets, week_schedules, rules_each_day)
1479
+ # add all rules to a final ScheduleRuleset
1480
+ default_day_schedule = final_rules[0].schedule_day
1481
+ schedule_type = schedules[0].schedule_type_limit
1482
+ return ScheduleRuleset(
1483
+ identifier, default_day_schedule, final_rules[1:], schedule_type,
1484
+ holiday_sch, summer_dd_sch, winter_dd_sch)
1485
+
1486
+ def _get_sch_values(self, sch_day_vals, dow, start_date, end_date,
1487
+ hol_doy, hol_vals):
1488
+ """Get a list of values over a date range for a typical year."""
1489
+ values = []
1490
+ for doy in range(start_date.doy, end_date.doy + 1):
1491
+ if dow > 7: # reset the day of the week to sunday
1492
+ dow = 1
1493
+ if doy in hol_doy:
1494
+ if hol_vals is not None:
1495
+ values.extend(hol_vals)
1496
+ else: # no holiday values; use default_day_schedule.
1497
+ values.extend(sch_day_vals[-1])
1498
+ else:
1499
+ for i, rule in enumerate(self._schedule_rules): # see if rules apply
1500
+ if rule.does_rule_apply(doy, dow):
1501
+ values.extend(sch_day_vals[i])
1502
+ break
1503
+ else: # no rule applies; use default_day_schedule.
1504
+ values.extend(sch_day_vals[-1])
1505
+ dow += 1
1506
+ return values
1507
+
1508
+ def _get_sch_values_leap_year(self, sch_day_vals, dow, start_date, end_date,
1509
+ hol_doy, hol_vals):
1510
+ """Get a list of values over a date range for a leap year."""
1511
+ values = []
1512
+ for doy in range(start_date.doy, end_date.doy + 1):
1513
+ if dow > 7: # reset the day of the week to sunday
1514
+ dow = 1
1515
+ if doy in hol_doy:
1516
+ if hol_vals is not None:
1517
+ values.extend(hol_vals)
1518
+ else: # no holiday values; use default_day_schedule.
1519
+ values.extend(sch_day_vals[-1])
1520
+ else:
1521
+ for i, rule in enumerate(self._schedule_rules): # see if rules apply
1522
+ if rule.does_rule_apply_leap_year(doy, dow):
1523
+ values.extend(sch_day_vals[i])
1524
+ break
1525
+ else: # no rule applies; use default_day_schedule.
1526
+ values.extend(sch_day_vals[-1])
1527
+ dow += 1
1528
+ return values
1529
+
1530
+ def _get_week_list(self, rule_indices):
1531
+ """Get a list of the ScheduleDay identifiers applied on each day of the week."""
1532
+ week_list = []
1533
+ for dow in range(7):
1534
+ for i in rule_indices:
1535
+ if self._schedule_rules[i].week_apply_tuple[dow]:
1536
+ week_list.append(self._schedule_rules[i].schedule_day.identifier)
1537
+ break
1538
+ else: # no rule applies; use default_day_schedule.
1539
+ week_list.append(self.default_day_schedule.identifier)
1540
+ return week_list
1541
+
1542
+ def _get_extra_week_fields(self):
1543
+ """Get schedule identifiers of extra days in Schedule:Week."""
1544
+ # add summer and winter design days
1545
+ week_fields = []
1546
+ if self._holiday_schedule is not None:
1547
+ week_fields.append(self._holiday_schedule.identifier)
1548
+ else:
1549
+ week_fields.append(self._default_day_schedule.identifier)
1550
+ if self._summer_designday_schedule is not None:
1551
+ week_fields.append(self._summer_designday_schedule.identifier)
1552
+ else:
1553
+ week_fields.append(self._default_day_schedule.identifier)
1554
+ if self._winter_designday_schedule is not None:
1555
+ week_fields.append(self._winter_designday_schedule.identifier)
1556
+ else:
1557
+ week_fields.append(self._default_day_schedule.identifier)
1558
+ for _ in range(2): # add the extra 2 custom days that are rarely used in E+
1559
+ week_fields.append(self.default_day_schedule.identifier)
1560
+ return week_fields
1561
+
1562
+ def _idf_week_schedule_from_rule_indices(self, rule_indices, week_index):
1563
+ """Create an IDF string of a week schedule from a list of rules indices."""
1564
+ week_sch_id = '{}_Week {}'.format(self.identifier, week_index)
1565
+ week_fields = [week_sch_id]
1566
+ # check rules that apply for the days of the week
1567
+ week_fields.extend(self._get_week_list(rule_indices))
1568
+ # add extra days (including summer and winter design days)
1569
+ week_fields.extend(self._get_extra_week_fields())
1570
+ week_schedule = generate_idf_string(
1571
+ 'Schedule:Week:Daily', week_fields, self._schedule_week_comments)
1572
+ return week_schedule, week_sch_id
1573
+
1574
+ def _idf_week_schedule_from_week_list(self, week_list, week_index):
1575
+ """Create an IDF string of a week schedule from a list ScheduleDay identifiers.
1576
+ """
1577
+ week_sch_id = '{}_Week {}'.format(self.identifier, week_index)
1578
+ week_fields = [week_sch_id]
1579
+ week_fields.extend(week_list)
1580
+ week_fields.extend(self._get_extra_week_fields())
1581
+ week_schedule = generate_idf_string(
1582
+ 'Schedule:Week:Daily', week_fields, self._schedule_week_comments)
1583
+ return week_schedule, week_sch_id
1584
+
1585
+ def _check_schedule_parent(self, schedule, sch_type='child'):
1586
+ """Used to ensure that a ScheduleDay object has only one parent ScheduleRuleset.
1587
+
1588
+ This is important to ensure ScheduleRulesets remain self-contained units
1589
+ and that editing one ScheduleRuleset does not edit another one.
1590
+ """
1591
+ if schedule._parent is None or schedule._parent is self:
1592
+ schedule._parent = self
1593
+ else:
1594
+ raise ValueError(
1595
+ 'ScheduleDay objects can be assigned to a ScheduleRuleset only once.\n'
1596
+ 'ScheduleDay "{}" cannot be the {} of ScheduleRuleset "{}" since it is '
1597
+ 'already assigned to "{}".\nTry duplicating the ScheduleDay, changing '
1598
+ 'its identifier, and then assigning it to this ScheduleRuleset.'.format(
1599
+ schedule.identifier, sch_type, self.identifier,
1600
+ schedule._parent.identifier))
1601
+
1602
+ def _check_schedule_rules(self, rules):
1603
+ """Check schedule_rules whenever they come through the setter."""
1604
+ if rules is None:
1605
+ return []
1606
+ if not isinstance(rules, list):
1607
+ try:
1608
+ rules = list(rules)
1609
+ except (ValueError, TypeError):
1610
+ raise TypeError('ScheduleRuleset schedule_rules must be iterable.')
1611
+ for rule in rules:
1612
+ self._check_rule(rule)
1613
+ self._check_schedule_parent(rule.schedule_day, 'schedule_rule')
1614
+ return rules
1615
+
1616
+ @staticmethod
1617
+ def _check_rule(rule):
1618
+ """Check that an individual rule is a ScheduleRule."""
1619
+ assert isinstance(rule, ScheduleRule), \
1620
+ 'Expected ScheduleRule for ScheduleRuleset. Got {}.'.format(type(rule))
1621
+
1622
+ @staticmethod
1623
+ def _apply_designdays_with_check(sched, holiday_sch, summer_dd_sch, winter_dd_sch):
1624
+ """Apply summer + winter design day schedules with a check for duplicates."""
1625
+ try:
1626
+ sched.holiday_schedule = holiday_sch
1627
+ except ValueError: # summer design day schedule is not unique
1628
+ holiday_sch = holiday_sch.duplicate()
1629
+ holiday_sch.identifier = '{}_Hol'.format(holiday_sch.identifier)
1630
+ sched.holiday_schedule = holiday_sch
1631
+ try:
1632
+ sched.summer_designday_schedule = summer_dd_sch
1633
+ except ValueError: # summer design day schedule is not unique
1634
+ summer_dd_sch = summer_dd_sch.duplicate()
1635
+ summer_dd_sch.identifier = '{}_SmrDsn'.format(summer_dd_sch.identifier)
1636
+ sched.summer_designday_schedule = summer_dd_sch
1637
+ try:
1638
+ sched.winter_designday_schedule = winter_dd_sch
1639
+ except ValueError: # winter design day schedule is not unique
1640
+ winter_dd_sch = winter_dd_sch.duplicate()
1641
+ winter_dd_sch.identifier = '{}_WntrDsn'.format(winter_dd_sch.identifier)
1642
+ sched.winter_designday_schedule = winter_dd_sch
1643
+
1644
+ @staticmethod
1645
+ def _idf_day_schedule_dictionary(day_idf_strings):
1646
+ """Get a dictionary of DaySchedule objects from an IDF string list."""
1647
+ day_schedule_dict = {}
1648
+ for sch_str in day_idf_strings:
1649
+ sch_str = sch_str.strip()
1650
+ sch_obj = ScheduleDay.from_idf(sch_str)
1651
+ day_schedule_dict[sch_obj.identifier] = sch_obj
1652
+ return day_schedule_dict
1653
+
1654
+ @staticmethod
1655
+ def _idf_week_schedule_dictionary(week_idf_strings, day_sch_dict):
1656
+ """Get a dictionary of ScheduleRule objects from Schedule:Week strings."""
1657
+ week_schedule_dict = {}
1658
+ week_designday_dict = {}
1659
+ for sch_str in week_idf_strings:
1660
+ sch_str = sch_str.strip()
1661
+ rules = ScheduleRule.extract_all_from_schedule_week(sch_str, day_sch_dict)
1662
+ if sch_str.startswith('Schedule:Week:Daily,'):
1663
+ ep_strs = parse_idf_string(sch_str)
1664
+ holiday = day_sch_dict[ep_strs[8]]
1665
+ summer_dd = day_sch_dict[ep_strs[9]]
1666
+ winter_dd = day_sch_dict[ep_strs[10]]
1667
+ else:
1668
+ ep_strs = parse_idf_string(sch_str, 'Schedule:Week:Compact,')
1669
+ holiday = summer_dd = winter_dd = rules[-1].schedule_day
1670
+ for i in range(1, len(ep_strs), 2):
1671
+ day_type, day_sch_id = ep_strs[i].lower(), ep_strs[i + 1]
1672
+ if 'holiday' in day_type:
1673
+ holiday = day_sch_dict[day_sch_id]
1674
+ elif 'summerdesignday' in day_type:
1675
+ summer_dd = day_sch_dict[day_sch_id]
1676
+ elif 'winterdesignday' in day_type:
1677
+ winter_dd = day_sch_dict[day_sch_id]
1678
+ sch_week_id = ep_strs[0]
1679
+ week_schedule_dict[sch_week_id] = rules
1680
+ week_designday_dict[sch_week_id] = [holiday, summer_dd, winter_dd]
1681
+ return week_schedule_dict, week_designday_dict
1682
+
1683
+ @staticmethod
1684
+ def _idf_schedule_type_dictionary(type_idf_strings):
1685
+ """Get a dictionary of ScheduleTypeLimit objects from ScheduleTypeLimits strings.
1686
+ """
1687
+ sch_type_dict = {}
1688
+ for type_str in type_idf_strings:
1689
+ type_str = type_str.strip()
1690
+ type_obj = ScheduleTypeLimit.from_idf(type_str)
1691
+ sch_type_dict[type_obj.identifier] = type_obj
1692
+ return sch_type_dict
1693
+
1694
+ @staticmethod
1695
+ def _combine_week_schedules(unique_rule_sets, week_schedules, rules_each_day):
1696
+ """Create the average week schedules from the unique combinations of rules."""
1697
+ # create a dictionary mapping unique rule indices to average week schedules
1698
+ rule_set_map = {}
1699
+ for rule_i, week_sched in zip(unique_rule_sets, week_schedules):
1700
+ rule_set_map[rule_i] = week_sched
1701
+ # loop through all 365 days of the year to find when rules change
1702
+ yr_wk_scheds = []
1703
+ yr_wk_dt_range = []
1704
+ prev_week_rules = None
1705
+ for doy in range(1, 366):
1706
+ week_rules = rules_each_day[doy - 1]
1707
+ if week_rules != prev_week_rules: # change to a new rule set
1708
+ yr_wk_scheds.append(rule_set_map[week_rules])
1709
+ if doy != 1:
1710
+ yr_wk_dt_range[-1].append(Date.from_doy(doy - 1))
1711
+ yr_wk_dt_range.append([Date.from_doy(doy)])
1712
+ else:
1713
+ yr_wk_dt_range.append([Date(1, 1)])
1714
+ prev_week_rules = week_rules
1715
+ yr_wk_dt_range[-1].append(Date(12, 31))
1716
+
1717
+ # convert week ScheduleRulesets to_rules and assign start + end dates
1718
+ final_rules = []
1719
+ for wk_sch, dt_range in zip(yr_wk_scheds, yr_wk_dt_range):
1720
+ final_rules.extend(wk_sch.to_rules(dt_range[0], dt_range[1]))
1721
+
1722
+ # add all rules to a final average ScheduleRuleset
1723
+ holiday_sch = yr_wk_scheds[0].holiday_schedule.duplicate()
1724
+ summer_dd_sch = yr_wk_scheds[0].summer_designday_schedule.duplicate()
1725
+ winter_dd_sch = yr_wk_scheds[0].winter_designday_schedule.duplicate()
1726
+ return final_rules, holiday_sch, summer_dd_sch, winter_dd_sch
1727
+
1728
+ @staticmethod
1729
+ def _get_avg_week(identifier, schedules, weights, timestep_resolution, rule_indices):
1730
+ """Get an average week schedule across several schedules and rule_indices."""
1731
+ # get matrix with each ruleset schedule in rows and each day of week in cols
1732
+ val_mtx = ScheduleRuleset._sch_value_mtx(
1733
+ schedules, timestep_resolution, rule_indices)
1734
+ # transpose the matrix and compute weighted average values for each dow
1735
+ avg_mtx = []
1736
+ for dow_list in zip(*val_mtx):
1737
+ sch_vals = [sum([val * weights[i] for i, val in enumerate(values)])
1738
+ for values in zip(*dow_list)]
1739
+ avg_mtx.append(sch_vals)
1740
+ # create the final ScheduleRuleset from the values
1741
+ return ScheduleRuleset.from_week_daily_values(
1742
+ identifier, avg_mtx[0], avg_mtx[1], avg_mtx[2], avg_mtx[3], avg_mtx[4],
1743
+ avg_mtx[5], avg_mtx[6], avg_mtx[7], timestep_resolution,
1744
+ schedules[0].schedule_type_limit, avg_mtx[8], avg_mtx[9])
1745
+
1746
+ @staticmethod
1747
+ def _get_ext_week(identifier, schedules, operator, timestep_resolution, rule_indices):
1748
+ """Get a max or min week schedule across several schedules and rule_indices.
1749
+
1750
+ Note that the operator is expected to be either the native Python max
1751
+ operator or the native Python min operator depending on the type of extreme
1752
+ being requested.
1753
+ """
1754
+ # get matrix with each ruleset schedule in rows and each day of week in cols
1755
+ val_mtx = ScheduleRuleset._sch_value_mtx(
1756
+ schedules, timestep_resolution, rule_indices)
1757
+ # transpose the matrix and compute weighted average values for each dow
1758
+ ext_mtx = []
1759
+ for dow_list in zip(*val_mtx):
1760
+ sch_vals = [operator(values) for values in zip(*dow_list)]
1761
+ ext_mtx.append(sch_vals)
1762
+ # create the final ScheduleRuleset from the values
1763
+ return ScheduleRuleset.from_week_daily_values(
1764
+ identifier, ext_mtx[0], ext_mtx[1], ext_mtx[2], ext_mtx[3], ext_mtx[4],
1765
+ ext_mtx[5], ext_mtx[6], ext_mtx[7], timestep_resolution,
1766
+ schedules[0].schedule_type_limit, ext_mtx[8], ext_mtx[9])
1767
+
1768
+ @staticmethod
1769
+ def _sch_value_mtx(schedules, timestep_resolution, rule_indices):
1770
+ """Get a matrix with each ruleset schedule in rows and each day of week in cols.
1771
+ """
1772
+ # get matrix with each ruleset schedule in rows and each day of week in cols
1773
+ val_mtx = []
1774
+ for s_i, sched in enumerate(schedules):
1775
+ week_list = []
1776
+ for dow in range(7):
1777
+ for i in rule_indices[s_i]: # see if rules apply
1778
+ if sched[i].week_apply_tuple[dow]:
1779
+ week_list.append(sched[i].schedule_day)
1780
+ break
1781
+ else: # no rule applies; use default_day_schedule.
1782
+ week_list.append(sched.default_day_schedule)
1783
+
1784
+ # check the rules applied for holidays + summer and winter design days
1785
+ holiday = sched.default_day_schedule if sched._holiday_schedule \
1786
+ is None else sched._holiday_schedule
1787
+ week_list.append(holiday)
1788
+ summer = sched.default_day_schedule if sched._summer_designday_schedule \
1789
+ is None else sched._summer_designday_schedule
1790
+ week_list.append(summer)
1791
+ winter = sched.default_day_schedule if sched._winter_designday_schedule \
1792
+ is None else sched._winter_designday_schedule
1793
+ week_list.append(winter)
1794
+ # add all values to the matrix
1795
+ val_mtx.append([day_sch.values_at_timestep(timestep_resolution)
1796
+ for day_sch in week_list])
1797
+ return val_mtx
1798
+
1799
+ @staticmethod
1800
+ def _instance_in_array(object_instance, object_array):
1801
+ """Check if a specific object instance is already in an array.
1802
+
1803
+ This can be much faster than `if object_instance in object_array`
1804
+ when you expect to be testing a lot of the same instance of an object for
1805
+ inclusion in an array since the builtin method uses an == operator to
1806
+ test inclusion.
1807
+ """
1808
+ for val in object_array:
1809
+ if val is object_instance:
1810
+ return True
1811
+ return False
1812
+
1813
+ def __bool__(self):
1814
+ return True
1815
+
1816
+ def __nonzero__(self):
1817
+ return True
1818
+
1819
+ def __len__(self):
1820
+ return len(self._schedule_rules)
1821
+
1822
+ def __getitem__(self, key):
1823
+ return self._schedule_rules[key]
1824
+
1825
+ def __iter__(self):
1826
+ return iter(self._schedule_rules)
1827
+
1828
+ def __key(self):
1829
+ """A tuple based on the object properties, useful for hashing."""
1830
+ return (self.identifier, hash(self.default_day_schedule),
1831
+ hash(self.holiday_schedule), hash(self.summer_designday_schedule),
1832
+ hash(self.winter_designday_schedule), hash(self.schedule_type_limit)) + \
1833
+ tuple(hash(rule) for rule in self.schedule_rules)
1834
+
1835
+ def __hash__(self):
1836
+ return hash(self.__key())
1837
+
1838
+ def __eq__(self, other):
1839
+ return isinstance(other, ScheduleRuleset) and self.__key() == other.__key()
1840
+
1841
+ def __ne__(self, other):
1842
+ return not self.__eq__(other)
1843
+
1844
+ def __copy__(self):
1845
+ holiday = self._holiday_schedule.duplicate() if \
1846
+ self._holiday_schedule is not None else None
1847
+ summer = self._summer_designday_schedule.duplicate() if \
1848
+ self._summer_designday_schedule is not None else None
1849
+ winter = self._winter_designday_schedule.duplicate() if \
1850
+ self._winter_designday_schedule is not None else None
1851
+ new_obj = ScheduleRuleset(
1852
+ self.identifier, self.default_day_schedule.duplicate(),
1853
+ [rule.duplicate() for rule in self._schedule_rules],
1854
+ self._schedule_type_limit, holiday, summer, winter)
1855
+ new_obj._display_name = self._display_name
1856
+ new_obj._user_data = None if self._user_data is None else self._user_data.copy()
1857
+ new_obj._properties._duplicate_extension_attr(self._properties)
1858
+ return new_obj
1859
+
1860
+ def ToString(self):
1861
+ """Overwrite .NET ToString."""
1862
+ return self.__repr__()
1863
+
1864
+ def __repr__(self):
1865
+ return 'ScheduleRuleset: {} [default day: {}] [{} rules]'.format(
1866
+ self.display_name, self.default_day_schedule.display_name,
1867
+ len(self._schedule_rules))