openstudio-load-flexibility-measures 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +1 -0
  5. data/lib/measures/PeakPeriodSchedulesShift/LICENSE.md +13 -0
  6. data/lib/measures/PeakPeriodSchedulesShift/README.md +140 -0
  7. data/lib/measures/PeakPeriodSchedulesShift/README.md.erb +106 -0
  8. data/lib/measures/PeakPeriodSchedulesShift/docs/measures-overview.png +0 -0
  9. data/lib/measures/PeakPeriodSchedulesShift/docs/other-examples1.png +0 -0
  10. data/lib/measures/PeakPeriodSchedulesShift/docs/other-examples2.png +0 -0
  11. data/lib/measures/PeakPeriodSchedulesShift/docs/other-examples3.png +0 -0
  12. data/lib/measures/PeakPeriodSchedulesShift/measure.rb +441 -0
  13. data/lib/measures/PeakPeriodSchedulesShift/measure.xml +230 -0
  14. data/lib/measures/PeakPeriodSchedulesShift/tests/PeakPeriodSchedulesShift_Test.rb +603 -0
  15. data/lib/measures/PeakPeriodSchedulesShift/tests/base-schedules-detailed-occupancy-stochastic.osm +8306 -0
  16. data/lib/measures/PeakPeriodSchedulesShift/tests/base.osm +15647 -0
  17. data/lib/measures/PeakPeriodSchedulesShift/tests/files/schedules20230418-20180-18nykxu.csv +8761 -0
  18. data/lib/measures/PeakPeriodSchedulesShift/tests/files/unmodified_schedules.csv +8761 -0
  19. data/lib/measures/PeakPeriodSchedulesShift/tests/test_smooth_schedules.osm +23698 -0
  20. data/lib/measures/PeakPeriodSchedulesShift/tests/test_stochastic_schedules.osm +8985 -0
  21. data/lib/measures/PeakPeriodSchedulesShift/tests/test_stochastic_schedules_no_stacking.osm +8306 -0
  22. data/lib/measures/ShiftScheduleByType/LICENSE.md +1 -1
  23. data/lib/measures/ShiftScheduleByType/measure.xml +23 -23
  24. data/lib/measures/add_central_ice_storage/LICENSE.md +1 -1
  25. data/lib/measures/add_central_ice_storage/measure.xml +3 -3
  26. data/lib/measures/add_hpwh/LICENSE.md +1 -1
  27. data/lib/measures/add_hpwh/measure.rb +19 -19
  28. data/lib/measures/add_hpwh/measure.xml +4 -4
  29. data/lib/measures/add_packaged_ice_storage/LICENSE.md +1 -1
  30. data/lib/measures/add_packaged_ice_storage/measure.xml +3 -3
  31. data/lib/openstudio/load_flexibility_measures/version.rb +1 -1
  32. data/openstudio-load-flexibility-measures.gemspec +2 -3
  33. metadata +23 -20
@@ -0,0 +1,441 @@
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
3
+ # See also https://openstudio.net/license
4
+ # *******************************************************************************
5
+
6
+ # frozen_string_literal: true
7
+
8
+ # see the URL below for information on how to write OpenStudio measures
9
+ # http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
10
+
11
+ require 'csv'
12
+
13
+ # start the measure
14
+ class PeakPeriodSchedulesShift < OpenStudio::Measure::ModelMeasure
15
+ # human readable name
16
+ def name
17
+ # Measure name should be the title case of the class name.
18
+ return 'PeakPeriodSchedulesShift'
19
+ end
20
+
21
+ # human readable description
22
+ def description
23
+ return 'Shifts select weekday (or weekday/weekend) schedules out of a peak period.'
24
+ end
25
+
26
+ # human readable description of modeling approach
27
+ def modeler_description
28
+ return 'Enter a peak period window, a delay value, and any applicable ScheduleRuleset or ScheduleFile schedules. Shift all schedule values falling within the peak period to after the end (offset by delay) of the peak period. Optionally prevent stacking of schedule values by only allowing shifts to all-zero periods. Optionally apply schedule shifts to weekend days.'
29
+ end
30
+
31
+ # used to populate taxonomy in readme.md
32
+ def taxonomy
33
+ return 'Whole Building.Whole Building Schedules'
34
+ end
35
+
36
+ # define the arguments that the user will input
37
+ def arguments(_model)
38
+ args = OpenStudio::Measure::OSArgumentVector.new
39
+
40
+ arg = OpenStudio::Measure::OSArgument.makeStringArgument('schedules_peak_period', true)
41
+ arg.setDisplayName('Schedules: Peak Period')
42
+ arg.setDescription('Specifies the peak period. Enter a time like "15 - 18" (start hour can be 0 through 23 and end hour can be 1 through 24).')
43
+ arg.setDefaultValue('15 - 18')
44
+ args << arg
45
+
46
+ arg = OpenStudio::Measure::OSArgument.makeIntegerArgument('schedules_peak_period_delay', true)
47
+ arg.setDisplayName('Schedules: Peak Period Delay')
48
+ arg.setUnits('hr')
49
+ arg.setDescription('The number of hours after peak period end.')
50
+ arg.setDefaultValue(0)
51
+ args << arg
52
+
53
+ arg = OpenStudio::Measure::OSArgument.makeBoolArgument('schedules_peak_period_allow_stacking', false)
54
+ arg.setDisplayName('Schedules: Peak Period Allow Stacking')
55
+ arg.setDescription('Whether schedules can be shifted to periods that already have non-zero schedule values. Defaults to true. Note that the schedule type limits upper value is increased to 2.0 when allowing stacked schedule values.')
56
+ args << arg
57
+
58
+ arg = OpenStudio::Measure::OSArgument.makeBoolArgument('schedules_peak_period_weekdays_only', false)
59
+ arg.setDisplayName('Schedules: Peak Period Weekdays Only')
60
+ arg.setDescription('Whether schedules can be shifted for weekdays only, or weekends as well. Defaults to true.')
61
+ args << arg
62
+
63
+ arg = OpenStudio::Measure::OSArgument.makeStringArgument('schedules_peak_period_schedule_rulesets_names', false)
64
+ arg.setDisplayName('Schedules: Peak Period Schedule Rulesets Names')
65
+ arg.setDescription('Comma-separated list of Schedule:Ruleset object names corresponding to schedules to shift during the specified peak period.')
66
+ args << arg
67
+
68
+ arg = OpenStudio::Measure::OSArgument.makeStringArgument('schedules_peak_period_schedule_files_column_names', false)
69
+ arg.setDisplayName('Schedules: Peak Period Schedule Files Column Names')
70
+ arg.setDescription('Comma-separated list of column names, referenced by Schedule:File objects, corresponding to schedules to shift during the specified peak period.')
71
+ args << arg
72
+
73
+ return args
74
+ end
75
+
76
+ def get_schedule_ruleset_names(model)
77
+ schedule_ruleset_names = []
78
+ model.getScheduleRulesets.each do |schedule_ruleset|
79
+ schedule_ruleset_names << schedule_ruleset.name.to_s
80
+ end
81
+ return schedule_ruleset_names.uniq.sort
82
+ end
83
+
84
+ def get_schedule_file_column_names(model)
85
+ schedule_file_column_names = []
86
+ model.getExternalFiles.each do |external_file|
87
+ external_file_path = external_file.filePath.to_s
88
+ schedule_file_column_names += CSV.foreach(external_file_path).first
89
+ end
90
+ return schedule_file_column_names.uniq.sort
91
+ end
92
+
93
+ # define what happens when the measure is run
94
+ def run(model, runner, user_arguments)
95
+ super(model, runner, user_arguments) # Do **NOT** remove this line
96
+
97
+ # use the built-in error checking
98
+ if !runner.validateUserArguments(arguments(model), user_arguments)
99
+ return false
100
+ end
101
+
102
+ schedules_peak_period = runner.getStringArgumentValue('schedules_peak_period', user_arguments)
103
+ schedules_peak_period_delay = runner.getIntegerArgumentValue('schedules_peak_period_delay', user_arguments)
104
+ schedules_peak_period_allow_stacking = runner.getOptionalBoolArgumentValue('schedules_peak_period_allow_stacking', user_arguments)
105
+ schedules_peak_period_allow_stacking = schedules_peak_period_allow_stacking.is_initialized ? schedules_peak_period_allow_stacking.get : true
106
+ schedules_peak_period_weekdays_only = runner.getOptionalBoolArgumentValue('schedules_peak_period_weekdays_only', user_arguments)
107
+ schedules_peak_period_weekdays_only = schedules_peak_period_weekdays_only.is_initialized ? schedules_peak_period_weekdays_only.get : true
108
+ schedules_peak_period_schedule_rulesets_names = runner.getOptionalStringArgumentValue('schedules_peak_period_schedule_rulesets_names', user_arguments)
109
+ schedules_peak_period_schedule_rulesets_names = schedules_peak_period_schedule_rulesets_names.is_initialized ? schedules_peak_period_schedule_rulesets_names.get.split(',').map(&:strip) : []
110
+ schedules_peak_period_schedule_files_column_names = runner.getOptionalStringArgumentValue('schedules_peak_period_schedule_files_column_names', user_arguments)
111
+ schedules_peak_period_schedule_files_column_names = schedules_peak_period_schedule_files_column_names.is_initialized ? schedules_peak_period_schedule_files_column_names.get.split(',').map(&:strip) : []
112
+
113
+ schedule_ruleset_names_enabled = {}
114
+ get_schedule_ruleset_names(model).each do |schedule_ruleset_name|
115
+ schedule_ruleset_names_enabled[schedule_ruleset_name] = schedules_peak_period_schedule_rulesets_names.include?(schedule_ruleset_name)
116
+ end
117
+
118
+ schedule_file_column_names_enabled = {}
119
+ get_schedule_file_column_names(model).each do |schedule_file_column_name|
120
+ schedule_file_column_names_enabled[schedule_file_column_name] = schedules_peak_period_schedule_files_column_names.include?(schedule_file_column_name)
121
+ end
122
+
123
+ if (schedule_ruleset_names_enabled.empty? || schedule_ruleset_names_enabled.values.all? { |value| value == false }) &&
124
+ (schedule_file_column_names_enabled.empty? || schedule_file_column_names_enabled.values.all? { |value| value == false })
125
+ runner.registerAsNotApplicable('Did not select any ScheduleRuleset or ScheduleFile objects to shift.')
126
+ return true
127
+ end
128
+
129
+ begin_hour, end_hour = Schedules.parse_time_range(schedules_peak_period)
130
+
131
+ if begin_hour >= end_hour
132
+ runner.registerError("Specified peak period (#{begin_hour} - #{end_hour}) must be at least one hour long.")
133
+ return false
134
+ end
135
+
136
+ peak_period_length = end_hour - begin_hour
137
+ if (peak_period_length + schedules_peak_period_delay > 12)
138
+ runner.registerError("Specified peak period (#{begin_hour} - #{end_hour}), plus the delay (#{schedules_peak_period_delay}), must be no longer than 12 hours.")
139
+ return false
140
+ end
141
+
142
+ if (peak_period_length + end_hour + schedules_peak_period_delay > 24)
143
+ runner.registerError('Cannot shift day schedules into the next day.')
144
+ return false
145
+ end
146
+
147
+ # get year
148
+ yd = model.getYearDescription
149
+ calendar_year = yd.assumedYear
150
+ calendar_year = yd.calendarYear.get if yd.calendarYear.is_initialized
151
+ total_days_in_year = Schedules.NumDaysInYear(calendar_year)
152
+ sim_start_day = DateTime.new(calendar_year, 1, 1)
153
+
154
+ # get steps
155
+ ts = model.getTimestep
156
+ ts_per_hour = ts.numberOfTimestepsPerHour
157
+ steps_in_day = ts_per_hour * 24
158
+
159
+ # Schedule:Ruleset
160
+ shift_summary = {}
161
+ schedule_rulesets = model.getScheduleRulesets
162
+ schedule_ruleset_names_enabled.each do |schedule_ruleset_name, peak_period_shift_enabled|
163
+ next if !peak_period_shift_enabled
164
+
165
+ shift_summary[schedule_ruleset_name] = 0
166
+
167
+ shifted_schedule = false
168
+ schedule_ruleset = schedule_rulesets.find { |schedule_ruleset| schedule_ruleset.name.to_s == schedule_ruleset_name }
169
+ schedule_ruleset.scheduleRules.reverse.each do |schedule_rule|
170
+ if schedules_peak_period_weekdays_only
171
+ next unless schedule_rule.applyMonday || schedule_rule.applyTuesday || schedule_rule.applyWednesday || schedule_rule.applyThursday || schedule_rule.applyFriday # at least one weekday applies
172
+ end
173
+
174
+ new_schedule_rule = schedule_rule.clone.to_ScheduleRule.get
175
+ new_schedule_rule.setName("#{schedule_rule.name} Shifted")
176
+ new_schedule_rule.setApplySunday(schedule_rule.applySunday)
177
+ new_schedule_rule.setApplySunday(false) if schedules_peak_period_weekdays_only
178
+ new_schedule_rule.setApplyMonday(schedule_rule.applyMonday)
179
+ new_schedule_rule.setApplyTuesday(schedule_rule.applyTuesday)
180
+ new_schedule_rule.setApplyWednesday(schedule_rule.applyWednesday)
181
+ new_schedule_rule.setApplyThursday(schedule_rule.applyThursday)
182
+ new_schedule_rule.setApplyFriday(schedule_rule.applyFriday)
183
+ new_schedule_rule.setApplySaturday(schedule_rule.applySaturday)
184
+ new_schedule_rule.setApplySaturday(false) if schedules_peak_period_weekdays_only
185
+ schedule_ruleset.setScheduleRuleIndex(new_schedule_rule, 0)
186
+
187
+ old_day_schedule = schedule_rule.daySchedule
188
+ new_day_schedule = new_schedule_rule.daySchedule
189
+ new_day_schedule.setName("#{old_day_schedule.name} Shifted")
190
+
191
+ schedule = get_hourly_values(old_day_schedule)
192
+ shifted_day_schedule = Schedules.day_peak_shift(schedule, 0, begin_hour, end_hour, schedules_peak_period_delay, schedules_peak_period_allow_stacking, 24)
193
+
194
+ if shifted_day_schedule
195
+ shift_day_schedule(calendar_year, shift_summary, schedule_ruleset_name, new_schedule_rule, new_day_schedule, schedule, schedules_peak_period_weekdays_only)
196
+ shifted_schedule = true
197
+ else
198
+ new_schedule_rule.remove
199
+ end
200
+ end
201
+
202
+ old_default_day_schedule = schedule_ruleset.defaultDaySchedule
203
+ new_default_schedule_rule = OpenStudio::Model::ScheduleRule.new(schedule_ruleset)
204
+ new_default_schedule_rule.setName("#{old_default_day_schedule.name} Shifted")
205
+ new_default_schedule_rule.setApplySunday(false)
206
+ new_default_schedule_rule.setApplySunday(true) if !schedules_peak_period_weekdays_only
207
+ new_default_schedule_rule.setApplyMonday(true)
208
+ new_default_schedule_rule.setApplyTuesday(true)
209
+ new_default_schedule_rule.setApplyWednesday(true)
210
+ new_default_schedule_rule.setApplyThursday(true)
211
+ new_default_schedule_rule.setApplyFriday(true)
212
+ new_default_schedule_rule.setApplySaturday(false)
213
+ new_default_schedule_rule.setApplySaturday(true) if !schedules_peak_period_weekdays_only
214
+ schedule_ruleset.setScheduleRuleIndex(new_default_schedule_rule, 0)
215
+
216
+ new_default_day_schedule = new_default_schedule_rule.daySchedule
217
+ new_default_day_schedule.setName("#{old_default_day_schedule.name} Shifted")
218
+
219
+ schedule = get_hourly_values(old_default_day_schedule)
220
+ shifted_day_schedule = Schedules.day_peak_shift(schedule, 0, begin_hour, end_hour, schedules_peak_period_delay, schedules_peak_period_allow_stacking, 24)
221
+
222
+ if shifted_day_schedule
223
+ shift_day_schedule(calendar_year, shift_summary, schedule_ruleset_name, new_default_schedule_rule, new_default_day_schedule, schedule, schedules_peak_period_weekdays_only)
224
+ shifted_schedule = true
225
+ else
226
+ new_default_schedule_rule.remove
227
+ end
228
+
229
+ next unless shifted_schedule && schedules_peak_period_allow_stacking
230
+
231
+ new_schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
232
+ new_schedule_type_limits.setName("#{schedule_ruleset_name} Stacked Limits")
233
+ new_schedule_type_limits.setLowerLimitValue(0)
234
+ new_schedule_type_limits.setUpperLimitValue(1)
235
+ new_schedule_type_limits.setNumericType('Continuous')
236
+
237
+ schedule_ruleset.setScheduleTypeLimits(new_schedule_type_limits)
238
+ schedule_ruleset.scheduleTypeLimits.get.setUpperLimitValue(2.0) # ScheduleTypeRegistry prevents us from setting ScheduleTypeLimits with invalid limits
239
+ end
240
+
241
+ shift_summary.each do |schedule_ruleset_name, shifted_days|
242
+ runner.registerInfo("Out of #{total_days_in_year} total days, #{shifted_days} weekday(s) were shifted for the '#{schedule_ruleset_name}' Schedule:Ruleset.")
243
+ runner.registerValue("shifted_days_#{schedule_ruleset_name}", shifted_days)
244
+ end
245
+
246
+ # Schedule:File
247
+ model.getExternalFiles.each do |external_file|
248
+ external_file_path = external_file.filePath.to_s
249
+
250
+ schedules = Schedules.new(file_path: external_file_path)
251
+ schedules.shift_schedules(model, runner, schedule_file_column_names_enabled, begin_hour, end_hour, schedules_peak_period_delay, schedules_peak_period_allow_stacking, total_days_in_year, sim_start_day, steps_in_day, schedules_peak_period_weekdays_only)
252
+ schedules.export()
253
+ end
254
+
255
+ return true
256
+ end
257
+
258
+ def shift_day_schedule(calendar_year, shift_summary, schedule_ruleset_name, schedule_rule, day_schedule, schedule, schedules_peak_period_weekdays_only)
259
+ start_date = schedule_rule.startDate.get
260
+ start_date_month = start_date.monthOfYear.value
261
+ start_date_day = start_date.dayOfMonth
262
+ end_date = schedule_rule.endDate.get
263
+ end_date_month = end_date.monthOfYear.value
264
+ end_date_day = end_date.dayOfMonth
265
+
266
+ start_date = DateTime.new(calendar_year, start_date_month, start_date_day)
267
+ end_date = DateTime.new(calendar_year, end_date_month, end_date_day)
268
+ n_days = (end_date - start_date).to_i + 1
269
+ shifted_days = 0
270
+ n_days.times do |day|
271
+ today = start_date + day
272
+ day_of_week = today.wday
273
+ shifted_days += 1 if day_of_week == 0 && schedule_rule.applySunday && !schedules_peak_period_weekdays_only
274
+ shifted_days += 1 if day_of_week == 1 && schedule_rule.applyMonday
275
+ shifted_days += 1 if day_of_week == 2 && schedule_rule.applyTuesday
276
+ shifted_days += 1 if day_of_week == 3 && schedule_rule.applyWednesday
277
+ shifted_days += 1 if day_of_week == 4 && schedule_rule.applyThursday
278
+ shifted_days += 1 if day_of_week == 5 && schedule_rule.applyFriday
279
+ shifted_days += 1 if day_of_week == 6 && schedule_rule.applySaturday && !schedules_peak_period_weekdays_only
280
+ end
281
+
282
+ shift_summary[schedule_ruleset_name] += shifted_days
283
+ for h in 0..23
284
+ time = OpenStudio::Time.new(0, h + 1, 0, 0)
285
+ day_schedule.addValue(time, schedule[h])
286
+ end
287
+ end
288
+
289
+ def get_hourly_values(day_schedule)
290
+ times = day_schedule.times
291
+ values = day_schedule.values
292
+
293
+ hourly_values = []
294
+ t0 = 0
295
+ times.each_with_index do |_time, i|
296
+ t1 = times[i].hours
297
+ t1 = 24 if t1 == 0
298
+ hours = t1 - t0
299
+ for _v in 0...hours
300
+ hourly_values << values[i]
301
+ end
302
+ t0 = t1
303
+ end
304
+ return hourly_values
305
+ end
306
+ end
307
+
308
+ class Schedules
309
+ def initialize(file_path:)
310
+ @file_path = file_path
311
+
312
+ import()
313
+ end
314
+
315
+ def import()
316
+ @schedules = {}
317
+ columns = CSV.read(@file_path).transpose
318
+ columns.each do |col|
319
+ col_name = col[0]
320
+
321
+ values = col[1..-1].reject { |v| v.nil? }
322
+
323
+ begin
324
+ values = values.map { |v| Float(v) }
325
+ rescue ArgumentError
326
+ fail "Schedule value must be numeric for column '#{col_name}'. [context: #{schedules_path}]"
327
+ end
328
+
329
+ @schedules[col_name] = values
330
+ end
331
+ end
332
+
333
+ def shift_schedules(model, runner, schedule_file_column_names_enabled, begin_hour, end_hour, delay, allow_stacking, total_days_in_year, sim_start_day, steps_in_day, schedules_peak_period_weekdays_only)
334
+ shift_summary = {}
335
+ schedule_file_column_names_enabled.each do |schedule_file_column_name, peak_period_shift_enabled|
336
+ schedule_file = model.getScheduleFiles.find { |schedule_file| schedule_file.name.to_s == schedule_file_column_name }
337
+
338
+ next if schedule_file.nil?
339
+ next if !@schedules.keys.include?(schedule_file_column_name)
340
+ next if !peak_period_shift_enabled
341
+
342
+ schedule = @schedules[schedule_file_column_name]
343
+ shift_summary[schedule_file_column_name] = 0
344
+ next if schedule.nil?
345
+
346
+ shifted_schedule = false
347
+ total_days_in_year.times do |day|
348
+ today = sim_start_day + day
349
+ day_of_week = today.wday
350
+ next if [0, 6].include?(day_of_week) && schedules_peak_period_weekdays_only
351
+
352
+ shifted_day_schedule = Schedules.day_peak_shift(schedule, day, begin_hour, end_hour, delay, allow_stacking, steps_in_day)
353
+ if shifted_day_schedule
354
+ shift_summary[schedule_file_column_name] += 1
355
+ shifted_schedule = true
356
+ end
357
+ end
358
+
359
+ next unless shifted_schedule && allow_stacking
360
+
361
+ new_schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
362
+ new_schedule_type_limits.setName("#{schedule_file_column_name} Stacked Limits")
363
+ new_schedule_type_limits.setLowerLimitValue(0)
364
+ new_schedule_type_limits.setUpperLimitValue(1)
365
+ new_schedule_type_limits.setNumericType('Continuous')
366
+
367
+ schedule_file.setScheduleTypeLimits(new_schedule_type_limits)
368
+ schedule_file.scheduleTypeLimits.get.setUpperLimitValue(2.0) # ScheduleTypeRegistry prevents us from setting ScheduleTypeLimits with invalid limits
369
+ end
370
+
371
+ shift_summary.each do |schedule_file_column_name, shifted_days|
372
+ runner.registerInfo("Out of #{total_days_in_year} total days, #{shifted_days} weekday(s) were shifted for the '#{schedule_file_column_name}' Schedule:File.")
373
+ runner.registerValue("shifted_days_#{schedule_file_column_name}", shifted_days)
374
+ end
375
+ end
376
+
377
+ def self.day_peak_shift(schedule, day, begin_hour, end_hour, delay, allow_stacking, steps_in_day)
378
+ steps_in_hour = steps_in_day / 24
379
+ period = (end_hour - begin_hour) * steps_in_hour # n steps
380
+
381
+ # peak period
382
+ peak_begin_ix = day * steps_in_day + (begin_hour * steps_in_hour)
383
+ peak_end_ix = peak_begin_ix + period
384
+
385
+ # new period
386
+ new_begin_ix = peak_end_ix + (delay * steps_in_hour)
387
+ new_end_ix = new_begin_ix + period
388
+
389
+ shifted = false
390
+ if !allow_stacking
391
+ return shifted if schedule[new_begin_ix...new_end_ix].any? { |x| x > 0 } # prevent stacking
392
+ end
393
+
394
+ shifted = true if schedule[peak_begin_ix...peak_end_ix].any? { |x| x > 0 } # schedule was actually moved
395
+ schedule[new_begin_ix...new_end_ix] = [schedule[new_begin_ix...new_end_ix], schedule[peak_begin_ix...peak_end_ix]].transpose.map(&:sum)
396
+ schedule[peak_begin_ix...peak_end_ix] = [0] * period
397
+
398
+ return shifted
399
+ end
400
+
401
+ def export()
402
+ CSV.open(@file_path, 'wb') do |csv|
403
+ csv << @schedules.keys
404
+ rows = @schedules.values.transpose
405
+ rows.each do |row|
406
+ csv << row
407
+ end
408
+ end
409
+ end
410
+
411
+ def schedules
412
+ return @schedules
413
+ end
414
+
415
+ def self.parse_time_range(time_range)
416
+ begin_end_times = time_range.split('-').map { |v| v.strip }
417
+ if begin_end_times.size != 2
418
+ fail "Invalid time format specified for '#{time_range}'."
419
+ end
420
+
421
+ begin_hour = begin_end_times[0].strip.to_i
422
+ end_hour = begin_end_times[1].strip.to_i
423
+
424
+ return begin_hour, end_hour
425
+ end
426
+
427
+ def self.NumDaysInYear(year)
428
+ num_days_in_months = NumDaysInMonths(year)
429
+ num_days_in_year = num_days_in_months.sum
430
+ return num_days_in_year
431
+ end
432
+
433
+ def self.NumDaysInMonths(year)
434
+ num_days_in_months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
435
+ num_days_in_months[1] += 1 if Date.leap?(year)
436
+ return num_days_in_months
437
+ end
438
+ end
439
+
440
+ # register the measure to be used by the application
441
+ PeakPeriodSchedulesShift.new.registerWithApplication
@@ -0,0 +1,230 @@
1
+ <?xml version="1.0"?>
2
+ <measure>
3
+ <schema_version>3.1</schema_version>
4
+ <name>peak_period_schedules_shift</name>
5
+ <uid>4954275f-9caf-422c-9e1f-061244ade152</uid>
6
+ <version_id>afa2ee97-3397-4fc6-91ee-f2a9ea97397d</version_id>
7
+ <version_modified>2024-11-16T23:41:36Z</version_modified>
8
+ <xml_checksum>A8988589</xml_checksum>
9
+ <class_name>PeakPeriodSchedulesShift</class_name>
10
+ <display_name>PeakPeriodSchedulesShift</display_name>
11
+ <description>Shifts select weekday (or weekday/weekend) schedules out of a peak period.</description>
12
+ <modeler_description>Enter a peak period window, a delay value, and any applicable ScheduleRuleset or ScheduleFile schedules. Shift all schedule values falling within the peak period to after the end (offset by delay) of the peak period. Optionally prevent stacking of schedule values by only allowing shifts to all-zero periods. Optionally apply schedule shifts to weekend days.</modeler_description>
13
+ <arguments>
14
+ <argument>
15
+ <name>schedules_peak_period</name>
16
+ <display_name>Schedules: Peak Period</display_name>
17
+ <description>Specifies the peak period. Enter a time like "15 - 18" (start hour can be 0 through 23 and end hour can be 1 through 24).</description>
18
+ <type>String</type>
19
+ <required>true</required>
20
+ <model_dependent>false</model_dependent>
21
+ <default_value>15 - 18</default_value>
22
+ </argument>
23
+ <argument>
24
+ <name>schedules_peak_period_delay</name>
25
+ <display_name>Schedules: Peak Period Delay</display_name>
26
+ <description>The number of hours after peak period end.</description>
27
+ <type>Integer</type>
28
+ <units>hr</units>
29
+ <required>true</required>
30
+ <model_dependent>false</model_dependent>
31
+ <default_value>0</default_value>
32
+ </argument>
33
+ <argument>
34
+ <name>schedules_peak_period_allow_stacking</name>
35
+ <display_name>Schedules: Peak Period Allow Stacking</display_name>
36
+ <description>Whether schedules can be shifted to periods that already have non-zero schedule values. Defaults to true. Note that the schedule type limits upper value is increased to 2.0 when allowing stacked schedule values.</description>
37
+ <type>Boolean</type>
38
+ <required>false</required>
39
+ <model_dependent>false</model_dependent>
40
+ <choices>
41
+ <choice>
42
+ <value>true</value>
43
+ <display_name>true</display_name>
44
+ </choice>
45
+ <choice>
46
+ <value>false</value>
47
+ <display_name>false</display_name>
48
+ </choice>
49
+ </choices>
50
+ </argument>
51
+ <argument>
52
+ <name>schedules_peak_period_weekdays_only</name>
53
+ <display_name>Schedules: Peak Period Weekdays Only</display_name>
54
+ <description>Whether schedules can be shifted for weekdays only, or weekends as well. Defaults to true.</description>
55
+ <type>Boolean</type>
56
+ <required>false</required>
57
+ <model_dependent>false</model_dependent>
58
+ <choices>
59
+ <choice>
60
+ <value>true</value>
61
+ <display_name>true</display_name>
62
+ </choice>
63
+ <choice>
64
+ <value>false</value>
65
+ <display_name>false</display_name>
66
+ </choice>
67
+ </choices>
68
+ </argument>
69
+ <argument>
70
+ <name>schedules_peak_period_schedule_rulesets_names</name>
71
+ <display_name>Schedules: Peak Period Schedule Rulesets Names</display_name>
72
+ <description>Comma-separated list of Schedule:Ruleset object names corresponding to schedules to shift during the specified peak period.</description>
73
+ <type>String</type>
74
+ <required>false</required>
75
+ <model_dependent>false</model_dependent>
76
+ </argument>
77
+ <argument>
78
+ <name>schedules_peak_period_schedule_files_column_names</name>
79
+ <display_name>Schedules: Peak Period Schedule Files Column Names</display_name>
80
+ <description>Comma-separated list of column names, referenced by Schedule:File objects, corresponding to schedules to shift during the specified peak period.</description>
81
+ <type>String</type>
82
+ <required>false</required>
83
+ <model_dependent>false</model_dependent>
84
+ </argument>
85
+ </arguments>
86
+ <outputs />
87
+ <provenances />
88
+ <tags>
89
+ <tag>Whole Building.Whole Building Schedules</tag>
90
+ </tags>
91
+ <attributes>
92
+ <attribute>
93
+ <name>Measure Type</name>
94
+ <value>ModelMeasure</value>
95
+ <datatype>string</datatype>
96
+ </attribute>
97
+ <attribute>
98
+ <name>Measure Language</name>
99
+ <value>Ruby</value>
100
+ <datatype>string</datatype>
101
+ </attribute>
102
+ </attributes>
103
+ <files>
104
+ <file>
105
+ <filename>LICENSE.md</filename>
106
+ <filetype>md</filetype>
107
+ <usage_type>license</usage_type>
108
+ <checksum>8696A072</checksum>
109
+ </file>
110
+ <file>
111
+ <filename>README.md</filename>
112
+ <filetype>md</filetype>
113
+ <usage_type>readme</usage_type>
114
+ <checksum>B5738296</checksum>
115
+ </file>
116
+ <file>
117
+ <filename>README.md.erb</filename>
118
+ <filetype>erb</filetype>
119
+ <usage_type>readmeerb</usage_type>
120
+ <checksum>456E1929</checksum>
121
+ </file>
122
+ <file>
123
+ <filename>measures-overview.PNG</filename>
124
+ <filetype>PNG</filetype>
125
+ <usage_type>doc</usage_type>
126
+ <checksum>0536DAD0</checksum>
127
+ </file>
128
+ <file>
129
+ <filename>measures-overview.png</filename>
130
+ <filetype>png</filetype>
131
+ <usage_type>doc</usage_type>
132
+ <checksum>0536DAD0</checksum>
133
+ </file>
134
+ <file>
135
+ <filename>other-examples1.PNG</filename>
136
+ <filetype>PNG</filetype>
137
+ <usage_type>doc</usage_type>
138
+ <checksum>39590575</checksum>
139
+ </file>
140
+ <file>
141
+ <filename>other-examples1.png</filename>
142
+ <filetype>png</filetype>
143
+ <usage_type>doc</usage_type>
144
+ <checksum>39590575</checksum>
145
+ </file>
146
+ <file>
147
+ <filename>other-examples2.PNG</filename>
148
+ <filetype>PNG</filetype>
149
+ <usage_type>doc</usage_type>
150
+ <checksum>5AA85040</checksum>
151
+ </file>
152
+ <file>
153
+ <filename>other-examples2.png</filename>
154
+ <filetype>png</filetype>
155
+ <usage_type>doc</usage_type>
156
+ <checksum>5AA85040</checksum>
157
+ </file>
158
+ <file>
159
+ <filename>other-examples3.PNG</filename>
160
+ <filetype>PNG</filetype>
161
+ <usage_type>doc</usage_type>
162
+ <checksum>D6815644</checksum>
163
+ </file>
164
+ <file>
165
+ <filename>other-examples3.png</filename>
166
+ <filetype>png</filetype>
167
+ <usage_type>doc</usage_type>
168
+ <checksum>D6815644</checksum>
169
+ </file>
170
+ <file>
171
+ <version>
172
+ <software_program>OpenStudio</software_program>
173
+ <identifier>3.5.1</identifier>
174
+ <min_compatible>3.5.1</min_compatible>
175
+ </version>
176
+ <filename>measure.rb</filename>
177
+ <filetype>rb</filetype>
178
+ <usage_type>script</usage_type>
179
+ <checksum>22D1A6CA</checksum>
180
+ </file>
181
+ <file>
182
+ <filename>PeakPeriodSchedulesShift_Test.rb</filename>
183
+ <filetype>rb</filetype>
184
+ <usage_type>test</usage_type>
185
+ <checksum>3061BC32</checksum>
186
+ </file>
187
+ <file>
188
+ <filename>base-schedules-detailed-occupancy-stochastic.osm</filename>
189
+ <filetype>osm</filetype>
190
+ <usage_type>test</usage_type>
191
+ <checksum>CFA80A78</checksum>
192
+ </file>
193
+ <file>
194
+ <filename>base.osm</filename>
195
+ <filetype>osm</filetype>
196
+ <usage_type>test</usage_type>
197
+ <checksum>8193AAB9</checksum>
198
+ </file>
199
+ <file>
200
+ <filename>files/schedules20230418-20180-18nykxu.csv</filename>
201
+ <filetype>csv</filetype>
202
+ <usage_type>test</usage_type>
203
+ <checksum>D6FBB6B6</checksum>
204
+ </file>
205
+ <file>
206
+ <filename>files/unmodified_schedules.csv</filename>
207
+ <filetype>csv</filetype>
208
+ <usage_type>test</usage_type>
209
+ <checksum>D6FBB6B6</checksum>
210
+ </file>
211
+ <file>
212
+ <filename>test_smooth_schedules.osm</filename>
213
+ <filetype>osm</filetype>
214
+ <usage_type>test</usage_type>
215
+ <checksum>C85A2F6A</checksum>
216
+ </file>
217
+ <file>
218
+ <filename>test_stochastic_schedules.osm</filename>
219
+ <filetype>osm</filetype>
220
+ <usage_type>test</usage_type>
221
+ <checksum>77977945</checksum>
222
+ </file>
223
+ <file>
224
+ <filename>test_stochastic_schedules_no_stacking.osm</filename>
225
+ <filetype>osm</filetype>
226
+ <usage_type>test</usage_type>
227
+ <checksum>36631473</checksum>
228
+ </file>
229
+ </files>
230
+ </measure>