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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/LICENSE.md +1 -1
- data/README.md +1 -0
- data/lib/measures/PeakPeriodSchedulesShift/LICENSE.md +13 -0
- data/lib/measures/PeakPeriodSchedulesShift/README.md +140 -0
- data/lib/measures/PeakPeriodSchedulesShift/README.md.erb +106 -0
- data/lib/measures/PeakPeriodSchedulesShift/docs/measures-overview.png +0 -0
- data/lib/measures/PeakPeriodSchedulesShift/docs/other-examples1.png +0 -0
- data/lib/measures/PeakPeriodSchedulesShift/docs/other-examples2.png +0 -0
- data/lib/measures/PeakPeriodSchedulesShift/docs/other-examples3.png +0 -0
- data/lib/measures/PeakPeriodSchedulesShift/measure.rb +441 -0
- data/lib/measures/PeakPeriodSchedulesShift/measure.xml +230 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/PeakPeriodSchedulesShift_Test.rb +603 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/base-schedules-detailed-occupancy-stochastic.osm +8306 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/base.osm +15647 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/files/schedules20230418-20180-18nykxu.csv +8761 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/files/unmodified_schedules.csv +8761 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/test_smooth_schedules.osm +23698 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/test_stochastic_schedules.osm +8985 -0
- data/lib/measures/PeakPeriodSchedulesShift/tests/test_stochastic_schedules_no_stacking.osm +8306 -0
- data/lib/measures/ShiftScheduleByType/LICENSE.md +1 -1
- data/lib/measures/ShiftScheduleByType/measure.xml +23 -23
- data/lib/measures/add_central_ice_storage/LICENSE.md +1 -1
- data/lib/measures/add_central_ice_storage/measure.xml +3 -3
- data/lib/measures/add_hpwh/LICENSE.md +1 -1
- data/lib/measures/add_hpwh/measure.rb +19 -19
- data/lib/measures/add_hpwh/measure.xml +4 -4
- data/lib/measures/add_packaged_ice_storage/LICENSE.md +1 -1
- data/lib/measures/add_packaged_ice_storage/measure.xml +3 -3
- data/lib/openstudio/load_flexibility_measures/version.rb +1 -1
- data/openstudio-load-flexibility-measures.gemspec +2 -3
- 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>
|