Module: OpenstudioStandards::Schedules

Defined in:
lib/openstudio-standards/schedules/create.rb,
lib/openstudio-standards/schedules/modify.rb,
lib/openstudio-standards/schedules/parametric.rb,
lib/openstudio-standards/schedules/information.rb

Overview

The Schedules module provides methods to create, modify, and get information about Schedule objects

Create collapse

Modify:ScheduleDay collapse

Modify:ScheduleRuleset collapse

Parametric:Model collapse

Parametric:Spaces collapse

Parametric:Schedule collapse

Parametric:ScheduleRuleset collapse

Parametric:ScheduleDay collapse

Information collapse

Information:ScheduleConstant collapse

Information:ScheduleCompact collapse

Information:ScheduleDay collapse

Information:ScheduleRuleset collapse

Information:Model collapse

Class Method Details

.create_complex_schedule(model, options = {}) ⇒ OpenStudio::Model::ScheduleRuleset

create a ruleset schedule with a complex profile

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • options (Hash) (defaults to: {})

    Hash of name and time value pairs

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/openstudio-standards/schedules/create.rb', line 208

def self.create_complex_schedule(model, options = {})
  defaults = {
    'name' => nil,
    'default_day' => ['always_on', [24.0, 1.0]]
  }

  # merge user inputs with defaults
  options = defaults.merge(options)

  # ScheduleRuleset
  sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
  if name
    sch_ruleset.setName(options['name'])
  end

  # Winter Design Day
  unless options['winter_design_day'].nil?
    sch_ruleset.setWinterDesignDaySchedule(sch_ruleset.winterDesignDaySchedule)
    winter_dsn_day = sch_ruleset.winterDesignDaySchedule
    winter_dsn_day.setName("#{sch_ruleset.name} Winter Design Day")
    options['winter_design_day'].each do |data_pair|
      hour = data_pair[0].truncate
      min = ((data_pair[0] - hour) * 60).to_i
      winter_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
    end
  end

  # Summer Design Day
  unless options['summer_design_day'].nil?
    sch_ruleset.setSummerDesignDaySchedule(sch_ruleset.summerDesignDaySchedule)
    summer_dsn_day = sch_ruleset.summerDesignDaySchedule
    summer_dsn_day.setName("#{sch_ruleset.name} Summer Design Day")
    options['summer_design_day'].each do |data_pair|
      hour = data_pair[0].truncate
      min = ((data_pair[0] - hour) * 60).to_i
      summer_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
    end
  end

  # Default Day
  default_day = sch_ruleset.defaultDaySchedule
  default_day.setName("#{sch_ruleset.name} #{options['default_day'][0]}")
  default_data_array = options['default_day']
  default_data_array.delete_at(0)
  default_data_array.each do |data_pair|
    hour = data_pair[0].truncate
    min = ((data_pair[0] - hour) * 60).to_i
    default_day.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
  end

  # Rules
  unless options['rules'].nil?
    options['rules'].each do |data_array|
      rule = OpenStudio::Model::ScheduleRule.new(sch_ruleset)
      rule.setName("#{sch_ruleset.name} #{data_array[0]} Rule")
      date_range = data_array[1].split('-')
      start_date = date_range[0].split('/')
      end_date = date_range[1].split('/')
      rule.setStartDate(model.getYearDescription.makeDate(start_date[0].to_i, start_date[1].to_i))
      rule.setEndDate(model.getYearDescription.makeDate(end_date[0].to_i, end_date[1].to_i))
      days = data_array[2].split('/')
      rule.setApplySunday(true) if days.include? 'Sun'
      rule.setApplyMonday(true) if days.include? 'Mon'
      rule.setApplyTuesday(true) if days.include? 'Tue'
      rule.setApplyWednesday(true) if days.include? 'Wed'
      rule.setApplyThursday(true) if days.include? 'Thu'
      rule.setApplyFriday(true) if days.include? 'Fri'
      rule.setApplySaturday(true) if days.include? 'Sat'
      day_schedule = rule.daySchedule
      day_schedule.setName("#{sch_ruleset.name} #{data_array[0]}")
      data_array.delete_at(0)
      data_array.delete_at(0)
      data_array.delete_at(0)
      data_array.each do |data_pair|
        hour = data_pair[0].truncate
        min = ((data_pair[0] - hour) * 60).to_i
        day_schedule.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
      end
    end
  end

  return sch_ruleset
end

.create_constant_schedule_ruleset(model, value, name: nil, schedule_type_limit: nil) ⇒ OpenStudio::Model::ScheduleRuleset

Create constant ScheduleRuleset with a given value

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • value (Double)

    the value to use, 24-7, 365

  • name (String) (defaults to: nil)

    the name of the schedule

  • schedule_type_limit (String) (defaults to: nil)

    the name of a schedule type limit options are Dimensionless, Temperature, Humidity Ratio, Fraction, Fractional, OnOff, and Activity

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/openstudio-standards/schedules/create.rb', line 113

def self.create_constant_schedule_ruleset(model,
                                          value,
                                          name: nil,
                                          schedule_type_limit: nil)
  # check to see if schedule exists with same name and constant value and return if true
  unless name.nil?
    existing_sch = model.getScheduleRulesetByName(name)
    if existing_sch.is_initialized
      existing_sch = existing_sch.get
      existing_day_sch_vals = existing_sch.defaultDaySchedule.values
      if existing_day_sch_vals.size == 1 && (existing_day_sch_vals[0] - value).abs < 1.0e-6
        return existing_sch
      end
    end
  end

  # create ScheduleRuleset
  schedule = OpenStudio::Model::ScheduleRuleset.new(model)
  schedule.defaultDaySchedule.addValue(OpenStudio::Time.new(0, 24, 0, 0), value)

  # set name
  unless name.nil?
    schedule.setName(name)
    schedule.defaultDaySchedule.setName("#{name} Default")
  end

  # set schedule type limits
  if !schedule_type_limit.nil?
    sch_type_limits_obj = OpenstudioStandards::Schedules.create_schedule_type_limits(model,
                                                                                     standard_schedule_type_limit: schedule_type_limit)
    schedule.setScheduleTypeLimits(sch_type_limits_obj)
  end

  return schedule
end

.create_inverted_schedule_day(old_schedule_day, new_schedule_day: nil, schedule_name: nil) ⇒ OpenStudio::Model::ScheduleDay

Create a ScheduleDay from another ScheduleDay with inverted values

Parameters:

  • old_schedule_day (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object to invert

  • new_schedule_day (OpenStudio::Model::ScheduleDay) (defaults to: nil)

    An OpenStudio ScheduleDay object. Default nil. If provided, will add values to this ScheduleDay object instead of creating a new one.

  • schedule_name (String) (defaults to: nil)

    Optional name of new schedule

Returns:

  • (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object of inverted schedule



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/openstudio-standards/schedules/create.rb', line 510

def self.create_inverted_schedule_day(old_schedule_day, new_schedule_day: nil, schedule_name: nil)
  # create new schedule object if none provided
  if new_schedule_day.nil?
    new_schedule_day = OpenStudio::Model::ScheduleDay.new(old_schedule_day.model)
  end

  # set default name if none provided
  if schedule_name.nil?
    new_schedule_day.setName("#{old_schedule_day.name} inverted")
  else
    new_schedule_day.setName(schedule_name)
  end

  # invert schedule values
  for index in 0..old_schedule_day.times.size - 1
    old_value = old_schedule_day.values[index]
    if old_value == 0
      new_value = 1
    else
      new_value = 0
    end
    new_schedule_day.addValue(old_schedule_day.times[index], new_value)
  end

  return new_schedule_day
end

.create_inverted_schedule_ruleset(schedule_ruleset, schedule_name: nil) ⇒ OpenStudio::Model::ScheduleRuleset

Create a ScheduleRuleset from another ScheduleRuleset with inverted values

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object to invert

  • schedule_name (String) (defaults to: nil)

    Optional name of new schedule

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object of inverted schedule



542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/openstudio-standards/schedules/create.rb', line 542

def self.create_inverted_schedule_ruleset(schedule_ruleset, schedule_name: nil)
  model = schedule_ruleset.model
  new_schedule = OpenStudio::Model::ScheduleRuleset.new(model, 0.0)

  # set default name if none provided
  if schedule_name.nil?
    new_schedule.setName("#{schedule_ruleset.name} inverted")
  else
    new_schedule.setName(schedule_name)
  end

  # change summer design day
  new_summer_dd_schedule = OpenstudioStandards::Schedules.create_inverted_schedule_day(schedule_ruleset.summerDesignDaySchedule)
  new_schedule.setSummerDesignDaySchedule(new_summer_dd_schedule)

  # change winter design day
  new_winter_dd_schedule = OpenstudioStandards::Schedules.create_inverted_schedule_day(schedule_ruleset.winterDesignDaySchedule)
  new_schedule.setWinterDesignDaySchedule(new_winter_dd_schedule)

  # change the default day values
  OpenstudioStandards::Schedules.create_inverted_schedule_day(schedule_ruleset.defaultDaySchedule,
                                                              new_schedule_day: new_schedule.defaultDaySchedule)

  # change for schedule rules
  schedule_ruleset.scheduleRules.each_with_index do |rule, i|
    old_schedule_day = rule.daySchedule
    new_schedule_day = OpenstudioStandards::Schedules.create_inverted_schedule_day(old_schedule_day)

    new_rule = OpenStudio::Model::ScheduleRule.new(new_schedule, new_schedule_day)
    new_rule.setName("#{new_schedule_day.name} Rule")
    new_rule.setApplySunday(rule.applySunday)
    new_rule.setApplyMonday(rule.applyMonday)
    new_rule.setApplyTuesday(rule.applyTuesday)
    new_rule.setApplyWednesday(rule.applyWednesday)
    new_rule.setApplyThursday(rule.applyThursday)
    new_rule.setApplyFriday(rule.applyFriday)
    new_rule.setApplySaturday(rule.applySaturday)
  end

  return new_schedule
end

.create_schedule_from_rate_of_change(model, schedule_ruleset) ⇒ OpenStudio::Model::ScheduleRuleset

TODO:

fix velocity so it isn’t fraction change per step, but per hour (I need to count hours between times and divide value by this)

create a new schedule using absolute velocity of existing schedule

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/openstudio-standards/schedules/create.rb', line 299

def self.create_schedule_from_rate_of_change(model, schedule_ruleset)
  # clone source schedule
  new_schedule = schedule_ruleset.clone(model)
  new_schedule.setName("#{schedule_ruleset.name} - Rate of Change")
  new_schedule = new_schedule.to_ScheduleRuleset.get

  # create array of all profiles to change. This includes summer, winter, default, and rules
  profiles = []
  profiles << new_schedule.winterDesignDaySchedule
  profiles << new_schedule.summerDesignDaySchedule
  profiles << new_schedule.defaultDaySchedule

  # time values may need
  end_profile_time = OpenStudio::Time.new(0, 24, 0, 0)
  hour_bump_time = OpenStudio::Time.new(0, 1, 0, 0)
  one_hour_left_time = OpenStudio::Time.new(0, 23, 0, 0)

  rules = new_schedule.scheduleRules
  rules.each do |rule|
    profiles << rule.daySchedule
  end

  profiles.uniq.each do |profile|
    times = profile.times
    values = profile.values

    i = 0
    values_intermediate = []
    times_intermediate = []
    until i == values.size
      if i == 0
        values_intermediate << 0.0
        if times[i] > hour_bump_time
          times_intermediate << (times[i] - hour_bump_time)
          if times[i + 1].nil?
            time_step_value = end_profile_time.hours + (end_profile_time.minutes / 60) - times[i].hours - (times[i].minutes / 60)
          else
            time_step_value = times[i + 1].hours + (times[i + 1].minutes / 60) - times[i].hours - (times[i].minutes / 60)
          end
          values_intermediate << ((values[i + 1].to_f - values[i].to_f).abs / (time_step_value * 2))
        end
        times_intermediate << times[i]
      elsif i == (values.size - 1)
        if times[times.size - 2] < one_hour_left_time
          times_intermediate << (times[times.size - 2] + hour_bump_time) # this should be the second to last time
          time_step_value = times[i - 1].hours + (times[i - 1].minutes / 60) - times[i - 2].hours - (times[i - 2].minutes / 60)
          values_intermediate << ((values[i - 1].to_f - values[i - 2].to_f).abs / (time_step_value * 2))
        end
        values_intermediate << 0.0
        times_intermediate << times[i] # this should be the last time
      else
        # get value multiplier based on how many hours it is spread over
        time_step_value = times[i].hours + (times[i].minutes / 60) - times[i - 1].hours - (times[i - 1].minutes / 60)
        values_intermediate << ((values[i].to_f - values[i - 1].to_f).abs / time_step_value)
        times_intermediate << times[i]
      end
      i += 1
    end

    # delete all profile values
    profile.clearValues

    i = 0
    until i == times_intermediate.size
      if i == (times_intermediate.size - 1)
        profile.addValue(times_intermediate[i], values_intermediate[i].to_f)
      else
        profile.addValue(times_intermediate[i], values_intermediate[i].to_f)
      end
      i += 1
    end
  end

  return new_schedule
end

.create_schedule_type_limits(model, standard_schedule_type_limit: nil, name: nil, lower_limit_value: nil, upper_limit_value: nil, numeric_type: nil, unit_type: nil) ⇒ OpenStudio::Model::ScheduleTypeLimits

create a ScheduleTypeLimits object for a schedule

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • standard_schedule_type_limit (String) (defaults to: nil)

    the name of a standard schedule type limit with predefined limits options are Dimensionless, Temperature, Humidity Ratio, Fraction, Fractional, OnOff, and Activity

  • name (String) (defaults to: nil)

    the name of the schedule type limits

  • lower_limit_value (double) (defaults to: nil)

    the lower limit value for the schedule type

  • upper_limit_value (double) (defaults to: nil)

    the upper limit value for the schedule type

  • numeric_type (String) (defaults to: nil)

    the numeric type, options are Continuous or Discrete

  • unit_type (String) (defaults to: nil)

    the unit type, options are defined in EnergyPlus I/O reference

Returns:

  • (OpenStudio::Model::ScheduleTypeLimits)

    OpenStudio ScheduleTypeLimits object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/openstudio-standards/schedules/create.rb', line 18

def self.create_schedule_type_limits(model,
                                     standard_schedule_type_limit: nil,
                                     name: nil,
                                     lower_limit_value: nil,
                                     upper_limit_value: nil,
                                     numeric_type: nil,
                                     unit_type: nil)

  if standard_schedule_type_limit.nil?
    if lower_limit_value.nil? || upper_limit_value.nil? || numeric_type.nil? || unit_type.nil?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Create', 'If calling create_schedule_type_limits without a standard_schedule_type_limit, you must specify all properties of ScheduleTypeLimits.')
      return false
    end
    schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
    schedule_type_limits.setName(name) if !name.nil?
    schedule_type_limits.setLowerLimitValue(lower_limit_value)
    schedule_type_limits.setUpperLimitValue(upper_limit_value)
    schedule_type_limits.setNumericType(numeric_type)
    schedule_type_limits.setUnitType(unit_type)
  else
    schedule_type_limits = model.getScheduleTypeLimitsByName(standard_schedule_type_limit)
    if schedule_type_limits.empty?
      case standard_schedule_type_limit.downcase
      when 'dimensionless'
        schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
        schedule_type_limits.setName('Dimensionless')
        schedule_type_limits.setLowerLimitValue(0.0)
        schedule_type_limits.setUpperLimitValue(1000.0)
        schedule_type_limits.setNumericType('Continuous')
        schedule_type_limits.setUnitType('Dimensionless')

      when 'temperature'
        schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
        schedule_type_limits.setName('Temperature')
        schedule_type_limits.setLowerLimitValue(0.0)
        schedule_type_limits.setUpperLimitValue(100.0)
        schedule_type_limits.setNumericType('Continuous')
        schedule_type_limits.setUnitType('Temperature')

      when 'humidity ratio'
        schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
        schedule_type_limits.setName('Humidity Ratio')
        schedule_type_limits.setLowerLimitValue(0.0)
        schedule_type_limits.setUpperLimitValue(0.3)
        schedule_type_limits.setNumericType('Continuous')
        schedule_type_limits.setUnitType('Dimensionless')

      when 'fraction', 'fractional'
        schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
        schedule_type_limits.setName('Fraction')
        schedule_type_limits.setLowerLimitValue(0.0)
        schedule_type_limits.setUpperLimitValue(1.0)
        schedule_type_limits.setNumericType('Continuous')
        schedule_type_limits.setUnitType('Dimensionless')

      when 'onoff'
        schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
        schedule_type_limits.setName('OnOff')
        schedule_type_limits.setLowerLimitValue(0)
        schedule_type_limits.setUpperLimitValue(1)
        schedule_type_limits.setNumericType('Discrete')
        schedule_type_limits.setUnitType('Availability')

      when 'activity'
        schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
        schedule_type_limits.setName('Activity')
        schedule_type_limits.setLowerLimitValue(70.0)
        schedule_type_limits.setUpperLimitValue(1000.0)
        schedule_type_limits.setNumericType('Continuous')
        schedule_type_limits.setUnitType('ActivityLevel')
      else
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Create', 'Invalid standard_schedule_type_limit for method create_schedule_type_limits.')
        return false
      end
    else
      schedule_type_limits = schedule_type_limits.get
      if schedule_type_limits.name.to_s.downcase == 'temperature'
        schedule_type_limits.resetLowerLimitValue
        schedule_type_limits.resetUpperLimitValue
        schedule_type_limits.setNumericType('Continuous')
        schedule_type_limits.setUnitType('Temperature')
      end
    end
  end
  return schedule_type_limits
end

.create_simple_schedule(model, options = {}) ⇒ OpenStudio::Model::ScheduleRuleset

create a ruleset schedule with a basic profile

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • options (Hash) (defaults to: {})

    Hash of name and time value pairs

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/openstudio-standards/schedules/create.rb', line 154

def self.create_simple_schedule(model, options = {})
  defaults = {
    'name' => nil,
    'winter_time_value_pairs' => { 24.0 => 0.0 },
    'summer_time_value_pairs' => { 24.0 => 1.0 },
    'default_time_value_pairs' => { 24.0 => 1.0 }
  }

  # merge user inputs with defaults
  options = defaults.merge(options)

  # ScheduleRuleset
  sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
  if name
    sch_ruleset.setName(options['name'])
  end

  # Winter Design Day
  sch_ruleset.setWinterDesignDaySchedule(sch_ruleset.winterDesignDaySchedule)
  winter_dsn_day = sch_ruleset.winterDesignDaySchedule
  winter_dsn_day.setName("#{sch_ruleset.name} Winter Design Day")
  options['winter_time_value_pairs'].each do |k, v|
    hour = k.truncate
    min = ((k - hour) * 60).to_i
    winter_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), v)
  end

  # Summer Design Day
  sch_ruleset.setSummerDesignDaySchedule(sch_ruleset.summerDesignDaySchedule)
  summer_dsn_day = sch_ruleset.summerDesignDaySchedule
  summer_dsn_day.setName("#{sch_ruleset.name} Summer Design Day")
  options['summer_time_value_pairs'].each do |k, v|
    hour = k.truncate
    min = ((k - hour) * 60).to_i
    summer_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), v)
  end

  # All Days
  default_day = sch_ruleset.defaultDaySchedule
  default_day.setName("#{sch_ruleset.name} Schedule Week Day")
  options['default_time_value_pairs'].each do |k, v|
    hour = k.truncate
    min = ((k - hour) * 60).to_i
    default_day.addValue(OpenStudio::Time.new(0, hour, min, 0), v)
  end

  return sch_ruleset
end

.create_weighted_merge_schedules(model, schedule_weights_hash, sch_name: 'Merged Schedule') ⇒ Hash

TODO:

apply weights to schedule rules as well, not just winter, summer, and default profile

merge multiple schedules into one using load or other value to weight each schedules influence on the merge

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • schedule_weights_hash (Hash)

    Hash of OpenStudio::Model::ScheduleRuleset, Double

  • sch_name (String) (defaults to: 'Merged Schedule')

    Optional name of new schedule

Returns:

  • (Hash)

    Hash of merged schedule and the total denominator



382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/openstudio-standards/schedules/create.rb', line 382

def self.create_weighted_merge_schedules(model, schedule_weights_hash, sch_name: 'Merged Schedule')
  # get denominator for weight
  denominator = 0.0
  schedule_weights_hash.each do |schedule, weight|
    denominator += weight
  end

  # create new schedule
  sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
  sch_ruleset.setName(sch_name)

  # create winter design day profile
  winter_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
  sch_ruleset.setWinterDesignDaySchedule(winter_dsn_day)
  winter_dsn_day = sch_ruleset.winterDesignDaySchedule
  winter_dsn_day.setName("#{sch_ruleset.name} Winter Design Day")

  # create  summer design day profile
  summer_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
  sch_ruleset.setSummerDesignDaySchedule(summer_dsn_day)
  summer_dsn_day = sch_ruleset.summerDesignDaySchedule
  summer_dsn_day.setName("#{sch_ruleset.name} Summer Design Day")

  # create default profile
  default_day = sch_ruleset.defaultDaySchedule
  default_day.setName("#{sch_ruleset.name} Schedule Week Day")

  # hash of schedule rules
  rules_hash = {} # mon, tue, wed, thur, fri, sat, sun, startDate, endDate
  # to avoid stacking order issues across schedules, I may need to make a rule for each day of the week for each date range

  schedule_weights_hash.each do |schedule, weight|
    # populate winter design day profile
    old_winter_profile = schedule.to_ScheduleRuleset.get.winterDesignDaySchedule
    times_final = summer_dsn_day.times
    i = 0
    value_updated_array = []
    # loop through times already in profile and update values
    until i > times_final.size - 1
      value = old_winter_profile.getValue(times_final[i]) * weight / denominator
      starting_value = winter_dsn_day.getValue(times_final[i])
      winter_dsn_day.addValue(times_final[i], value + starting_value)
      value_updated_array << times_final[i]
      i += 1
    end
    # loop through any new times unique to the current old profile to be merged
    j = 0
    times = old_winter_profile.times
    values = old_winter_profile.values
    until j > times.size - 1
      unless value_updated_array.include? times[j]
        value = values[j] * weight / denominator
        starting_value = winter_dsn_day.getValue(times[j])
        winter_dsn_day.addValue(times[j], value + starting_value)
      end
      j += 1
    end

    # populate summer design day profile
    old_summer_profile = schedule.to_ScheduleRuleset.get.summerDesignDaySchedule
    times_final = summer_dsn_day.times
    i = 0
    value_updated_array = []
    # loop through times already in profile and update values
    until i > times_final.size - 1
      value = old_summer_profile.getValue(times_final[i]) * weight / denominator
      starting_value = summer_dsn_day.getValue(times_final[i])
      summer_dsn_day.addValue(times_final[i], value + starting_value)
      value_updated_array << times_final[i]
      i += 1
    end
    # loop through any new times unique to the current old profile to be merged
    j = 0
    times = old_summer_profile.times
    values = old_summer_profile.values
    until j > times.size - 1
      unless value_updated_array.include? times[j]
        value = values[j] * weight / denominator
        starting_value = summer_dsn_day.getValue(times[j])
        summer_dsn_day.addValue(times[j], value + starting_value)
      end
      j += 1
    end

    # populate default profile
    old_default_profile = schedule.to_ScheduleRuleset.get.defaultDaySchedule
    times_final = default_day.times
    i = 0
    value_updated_array = []
    # loop through times already in profile and update values
    until i > times_final.size - 1
      value = old_default_profile.getValue(times_final[i]) * weight / denominator
      starting_value = default_day.getValue(times_final[i])
      default_day.addValue(times_final[i], value + starting_value)
      value_updated_array << times_final[i]
      i += 1
    end
    # loop through any new times unique to the current old profile to be merged
    j = 0
    times = old_default_profile.times
    values = old_default_profile.values
    until j > times.size - 1
      unless value_updated_array.include? times[j]
        value = values[j] * weight / denominator
        starting_value = default_day.getValue(times[j])
        default_day.addValue(times[j], value + starting_value)
      end
      j += 1
    end

    # create rules

    # gather data for rule profiles

    # populate rule profiles
  end

  result = { 'mergedSchedule' => sch_ruleset, 'denominator' => denominator }
  return result
end

.model_apply_parametric_schedules(model, ramp_frequency: nil, infer_hoo_for_non_assigned_objects: true, error_on_out_of_order: true) ⇒ Array

Note:

This measure will replace any prior chagnes made to ScheduleRule objects with new ScheduleRule values from

This method applies the hours of operation for a space and the load profile formulas in the overloaded ScheduleRulset objects to update time value pairs for ScheduleDay objects. Object type specific logic will be used to generate profiles for summer and winter design days.

profile formulas

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio Model object

  • ramp_frequency (Double) (defaults to: nil)

    ramp frequency in minutes. If nil method will match simulation timestep

  • infer_hoo_for_non_assigned_objects (Boolean) (defaults to: true)

    # attempt to get hoo for objects like swh with and exterior lighting

  • error_on_out_of_order (Boolean) (defaults to: true)

    true will error if applying formula creates out of order values

Returns:

  • (Array)

    of modified ScheduleRuleset objects

Author:

  • David Goldwasser



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/openstudio-standards/schedules/parametric.rb', line 348

def self.model_apply_parametric_schedules(model,
                                          ramp_frequency: nil,
                                          infer_hoo_for_non_assigned_objects: true,
                                          error_on_out_of_order: true)
  # get ramp frequency (fractional hour) from timestep
  if ramp_frequency.nil?
    steps_per_hour = if model.getSimulationControl.timestep.is_initialized
                       model.getSimulationControl.timestep.get.numberOfTimestepsPerHour
                     else
                       6 # default OpenStudio timestep if none specified
                     end
    ramp_frequency = 1.0 / steps_per_hour.to_f
  end

  # Go through model and create parametric formulas for all schedules
  parametric_inputs = OpenstudioStandards::Schedules.model_setup_parametric_schedules(model, gather_data_only: true)

  parametric_schedules = []
  model.getScheduleRulesets.sort.each do |sch|
    if !sch.hasAdditionalProperties || !sch.additionalProperties.hasFeature('param_sch_ver')
      # for now don't look at schedules without targets, in future can alter these by looking at building level hours of operation
      next if sch.directUseCount <= 0 # won't catch if used for space type load instance, but that space type isn't used

      # @todo address schedules that fall into this category, if they are used in the model
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.Model', "For #{sch.sources.first.name}, #{sch.name} is not setup as parametric schedule. It has #{sch.sources.size} sources.")
      next
    end

    # apply parametric inputs
    OpenstudioStandards::Schedules.schedule_ruleset_apply_parametric_inputs(sch, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order, parametric_inputs)

    # add schedule to array
    parametric_schedules << sch
  end

  return parametric_schedules
end

.model_get_hvac_schedule(model) ⇒ OpenStudio::Model::Schedule

Get the predominant air loop HVAC schedule in the model by floor area served.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (OpenStudio::Model::Schedule)

    OpenStudio Schedule object



921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
# File 'lib/openstudio-standards/schedules/information.rb', line 921

def self.model_get_hvac_schedule(model)
  # lookup from model, using largest air loop
  # check multiple kinds of systems, including unitary systems
  hvac_schedule = nil
  largest_area = 0.0

  model.getAirLoopHVACs.each do |air_loop|
    air_loop_area = 0.0
    air_loop.thermalZones.each { |tz| air_loop_area += tz.floorArea }
    if air_loop_area > largest_area
      hvac_schedule = air_loop.availabilitySchedule
      largest_area = air_loop_area
    end
  end

  model.getAirLoopHVACUnitarySystems.each do |unitary|
    next unless unitary.thermalZone.is_initialized

    air_loop_area = unitary.thermalZone.get.floorArea
    if air_loop_area > largest_area
      if unitary.availabilitySchedule.is_initialized
        hvac_schedule = unitary.availabilitySchedule.get
      else
        hvac_schedule = model.alwaysOnDiscreteSchedule
      end
      largest_area = air_loop_area
    end
  end

  model.getAirLoopHVACUnitaryHeatPumpAirToAirs.each do |unitary|
    next unless unitary.controllingZone.is_initialized

    air_loop_area = unitary.controllingZone.get.floorArea
    if air_loop_area > largest_area
      hvac_schedule = unitary.availabilitySchedule.get
      largest_area = air_loop_area
    end
  end

  model.getAirLoopHVACUnitaryHeatPumpAirToAirMultiSpeeds.each do |unitary|
    next unless unitary.controllingZoneorThermostatLocation.is_initialized

    air_loop_area = unitary.controllingZoneorThermostatLocation.get.floorArea
    if air_loop_area > largest_area
      if unitary.availabilitySchedule.is_initialized
        hvac_schedule = unitary.availabilitySchedule.get
      else
        hvac_schedule = model.alwaysOnDiscreteSchedule
      end
      largest_area = air_loop_area
    end
  end

  model.getFanZoneExhausts.each do |fan|
    next unless fan.thermalZone.is_initialized

    air_loop_area = fan.thermalZone.get.floorArea
    if air_loop_area > largest_area
      if fan.availabilitySchedule.is_initialized
        hvac_schedule = fan.availabilitySchedule.get
      else
        hvac_schedule = model.alwaysOnDiscreteSchedule
      end
      largest_area = air_loop_area
    end
  end

  building_area = model.getBuilding.floorArea
  if largest_area < 0.05 * building_area
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Schedules', "The largest airloop or HVAC system serves #{largest_area.round(1)} m^2, which is less than 5% of the building area #{building_area.round(1)} m^2. Attempting to use building hours of operation schedule instead.")
    default_schedule_set = model.getBuilding.defaultScheduleSet
    if default_schedule_set.is_initialized
      default_schedule_set = default_schedule_set.get
      hoo = default_schedule_set.hoursofOperationSchedule
      if hoo.is_initialized
        hvac_schedule = hoo.get
        largest_area = building_area
      else
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules', 'Unable to determine building hours of operation schedule. Treating the building as if there is no HVAC system schedule.')
        hvac_schedule = nil
      end
    else
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules', 'Unable to determine building hours of operation schedule. Treating the building as if there is no HVAC system schedule.')
      hvac_schedule = nil
    end
  end

  unless hvac_schedule.nil?
    area_fraction = 100.0 * largest_area / building_area
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Schedules', "Using schedule #{hvac_schedule.name} serving area #{largest_area.round(1)} m^2, #{area_fraction.round(0)}% of building area #{building_area.round(1)} m^2 as the building HVAC operation schedule.")
  end

  return hvac_schedule
end

.model_infer_hours_of_operation_building(model, fraction_of_daily_occ_range: 0.25, invert_res: true, gen_occ_profile: false) ⇒ ScheduleRuleset

This method looks at occupancy profiles for the building as a whole and generates an hours of operation default schedule for the building. It also clears out any higher level hours of operation schedule assignments. Spaces are organized by res and non_res. Whichever of the two groups has higher design level of people is used for building hours of operation Resulting hours of operation can have as many rules as necessary to describe the operation. Each ScheduleDay should be an on/off schedule with only values of 0 and 1. There should not be more than one on/off cycle per day. In future this could create different hours of operation for residential vs. non-residential, by building type, story, or space type. However this measure is a stop gap to convert old generic schedules to parametric schedules. Future new schedules should be designed as paramtric from the start and would not need to run through this inference process

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • fraction_of_daily_occ_range (Double) (defaults to: 0.25)

    fraction above/below daily min range required to start and end hours of operation

  • invert_res (Boolean) (defaults to: true)

    if true will reverse hours of operation for residential space types

  • gen_occ_profile (Boolean) (defaults to: false)

    if true creates a merged occupancy schedule for diagnostic purposes. This schedule is added to the model but no specifically returned by this method

Returns:

  • (ScheduleRuleset)

    schedule that is assigned to the building as default hours of operation

Author:

  • David Goldwasser



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/openstudio-standards/schedules/parametric.rb', line 23

def self.model_infer_hours_of_operation_building(model, fraction_of_daily_occ_range: 0.25, invert_res: true, gen_occ_profile: false)
  # create an array of non-residential and residential spaces
  res_spaces = []
  non_res_spaces = []
  res_people_design = 0
  non_res_people_design = 0
  model.getSpaces.sort.each do |space|
    if OpenstudioStandards::Space.space_residential?(space)
      res_spaces << space
      res_people_design += space.numberOfPeople * space.multiplier
    else
      non_res_spaces << space
      non_res_people_design += space.numberOfPeople * space.multiplier
    end
  end
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Parametric.Model', "Model has design level of #{non_res_people_design.round(2)} people in non residential spaces and #{res_people_design.round(2)} people in residential spaces.")

  # # create merged schedule for prevalent type (not used but can be generated for diagnostics)
  # if gen_occ_profile
  #   res_prevalent = false
  #   if res_people_design > non_res_people_design
  #     occ_merged = OpenstudioStandards::Space.spaces_get_occupancy_schedule(res_spaces, sch_name: 'Calculated Occupancy Fraction Residential Merged')
  #     res_prevalent = true
  #   else
  #     occ_merged = OpenstudioStandards::Space.spaces_get_occupancy_schedule(non_res_spaces, sch_name: 'Calculated Occupancy Fraction NonResidential Merged')
  #   end
  # end

  # re-run spaces_get_occupancy_schedule with x above min occupancy to create on/off schedule
  if res_people_design > non_res_people_design
    hours_of_operation = OpenstudioStandards::Space.spaces_get_occupancy_schedule(res_spaces,
                                                                                  sch_name: 'Building Hours of Operation Residential',
                                                                                  occupied_percentage_threshold: fraction_of_daily_occ_range,
                                                                                  threshold_calc_method: 'normalized_daily_range')
    res_prevalent = true
  else
    hours_of_operation = OpenstudioStandards::Space.spaces_get_occupancy_schedule(non_res_spaces,
                                                                                  sch_name: 'Building Hours of Operation NonResidential',
                                                                                  occupied_percentage_threshold: fraction_of_daily_occ_range,
                                                                                  threshold_calc_method: 'normalized_daily_range')
  end

  # remove gaps resulting in multiple on off cycles for each rule in schedule so it will be valid hours of operation
  profiles = []
  profiles << hours_of_operation.defaultDaySchedule
  hours_of_operation.scheduleRules.each do |rule|
    profiles << rule.daySchedule
  end
  profiles.sort.each do |profile|
    times = profile.times
    values = profile.values
    next if times.size <= 3 # length of 1-3 should produce valid hours_of_operation profiles

    # Find the latest time where the value == 1
    latest_time = nil
    times.zip(values).each do |time, value|
      if value > 0
        latest_time = time
      end
    end
    # Skip profiles that are zero all the time
    next if latest_time.nil?

    # Calculate the duration from this point to midnight
    wrap_dur_left_hr = 0
    if values.first == 0 && values.last == 0
      wrap_dur_left_hr = 24.0 - latest_time.totalHours
    end

    # calculate time at first start
    first_start_time = times[values.index(0)].totalHours

    occ_gap_hash = {}
    prev_time = 0
    prev_val = nil
    times.each_with_index do |time, i|
      next if time.totalHours == 0.0 # should not see this
      next if values[i] == prev_val # check if two 0 until time next to each other

      if values[i] == 0 # only store vacant segments
        if time.totalHours == 24
          occ_gap_hash[prev_time] = wrap_dur_left_hr + first_start_time
        else
          occ_gap_hash[prev_time] = time.totalHours - prev_time
        end
      end
      prev_time = time.totalHours
      prev_val = values[i]
    end
    profile.clearValues
    max_occ_gap_start = occ_gap_hash.key(occ_gap_hash.values.max)
    max_occ_gap_end_hr = max_occ_gap_start + occ_gap_hash[max_occ_gap_start] # can't add time and duration in hours
    if max_occ_gap_end_hr > 24.0 then max_occ_gap_end_hr -= 24.0 end

    # time for gap start
    target_start_hr = max_occ_gap_start.truncate
    target_start_min = ((max_occ_gap_start - target_start_hr) * 60.0).truncate
    max_occ_gap_start = OpenStudio::Time.new(0, target_start_hr, target_start_min, 0)

    # time for gap end
    target_end_hr = max_occ_gap_end_hr.truncate
    target_end_min = ((max_occ_gap_end_hr - target_end_hr) * 60.0).truncate
    max_occ_gap_end = OpenStudio::Time.new(0, target_end_hr, target_end_min, 0)

    profile.addValue(max_occ_gap_start, 1)
    profile.addValue(max_occ_gap_end, 0)
    os_time_24 = OpenStudio::Time.new(0, 24, 0, 0)
    if max_occ_gap_start > max_occ_gap_end
      profile.addValue(os_time_24, 0)
    else
      profile.addValue(os_time_24, 1)
    end
  end

  # reverse 1 and 0 values for res_prevalent building
  # currently spaces_get_occupancy_schedule doesn't use defaultDayProflie, so only inspecting rules for now.
  if invert_res && res_prevalent
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Parametric.Model', 'Per argument passed in, hours of operation are being inverted for buildings with more people in residential versus non-residential spaces.')
    hours_of_operation.scheduleRules.each do |rule|
      profile = rule.daySchedule
      times = profile.times
      values = profile.values
      profile.clearValues
      times.each_with_index do |time, i|
        orig_val = values[i]
        new_value = nil
        if orig_val == 0 then new_value = 1 end
        if orig_val == 1 then new_value = 0 end
        profile.addValue(time, new_value)
      end
    end
  end

  # set hours of operation for building level hours of operation
  model.getDefaultScheduleSets.each(&:resetHoursofOperationSchedule)
  if model.getBuilding.defaultScheduleSet.is_initialized
    default_sch_set = model.getBuilding.defaultScheduleSet.get
  else
    default_sch_set = OpenStudio::Model::DefaultScheduleSet.new(model)
    default_sch_set.setName('Building Default Schedule Set')
    model.getBuilding.setDefaultScheduleSet(default_sch_set)
  end
  default_sch_set.setHoursofOperationSchedule(hours_of_operation)

  return hours_of_operation
end

.model_setup_parametric_schedules(model, step_ramp_logic: nil, infer_hoo_for_non_assigned_objects: true, gather_data_only: false, hoo_var_method: 'hours') ⇒ Hash

This method users the hours of operation for a space and the existing ScheduleRuleset profiles to setup parametric schedule inputs. Inputs include one or more load profile formulas. Data is stored in model attributes for downstream application. This should impact all ScheduleRuleset objects in the model. Plant and Air loop hours of operations should be traced back to a space or spaces.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • step_ramp_logic (String) (defaults to: nil)

    type of step logic to use - @TODO: this is currently not used

  • infer_hoo_for_non_assigned_objects (Boolean) (defaults to: true)

    attempt to get hours of operation for objects like swh with and exterior lighting

  • gather_data_only (Boolean) (defaults to: false)

    false (stops method before changes made if true)

  • hoo_var_method (String) (defaults to: 'hours')

    accepts ‘hours’ or ‘fractional’. Any other value value will result in hour of operation variables not being applied Options are ‘hours’, ‘fractional’

Returns:

  • (Hash)

    schedule is key, value is hash of number of objects

Author:

  • David Goldwasser



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/openstudio-standards/schedules/parametric.rb', line 183

def self.model_setup_parametric_schedules(model,
                                          step_ramp_logic: nil,
                                          infer_hoo_for_non_assigned_objects: true,
                                          gather_data_only: false,
                                          hoo_var_method: 'hours')
  parametric_inputs = {}
  default_sch_type = OpenStudio::Model::DefaultScheduleType.new('HoursofOperationSchedule')
  # thermal zones, air loops, plant loops will require some logic if they refer to more than one hours of operaiton schedule.
  # for initial use case while have same horus of operaiton so this can be pretty simple, but will have to re-visit it sometime
  # possible solution A: choose hoo that contributes the largest fraction of floor area
  # possible solution B: expand the hours of operation for a given day to include combined range of hoo objects
  # whatever approach is used for gathering parametric inputs for existing ruleset schedules should also be used for model_apply_parametric_schedules

  # loop through spaces (trace hours of operation back to space)
  OpenstudioStandards::Schedules.spaces_space_types_get_parametric_schedule_inputs(model.getSpaces, parametric_inputs, gather_data_only)

  # loop through space types (trace hours of operation back to space type).
  OpenstudioStandards::Schedules.spaces_space_types_get_parametric_schedule_inputs(model.getSpaceTypes, parametric_inputs, gather_data_only)

  # loop through thermal zones (trace hours of operation back to spaces in thermal zone)
  thermal_zone_hash = {} # key is zone and hash is hours of operation
  model.getThermalZones.sort.each do |zone|
    # identify hours of operation
    hours_of_operation = OpenstudioStandards::Space.spaces_hours_of_operation(zone.spaces)
    thermal_zone_hash[zone] = hours_of_operation
    # get thermostat setpoint schedules
    if zone.thermostatSetpointDualSetpoint.is_initialized
      thermostat = zone.thermostatSetpointDualSetpoint.get
      if thermostat.heatingSetpointTemperatureSchedule.is_initialized && thermostat.heatingSetpointTemperatureSchedule.get.to_ScheduleRuleset.is_initialized
        schedule = thermostat.heatingSetpointTemperatureSchedule.get.to_ScheduleRuleset.get
        OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(schedule, thermostat, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: 'tstat')
      end
      if thermostat.coolingSetpointTemperatureSchedule.is_initialized && thermostat.coolingSetpointTemperatureSchedule.get.to_ScheduleRuleset.is_initialized
        schedule = thermostat.coolingSetpointTemperatureSchedule.get.to_ScheduleRuleset.get
        OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(schedule, thermostat, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: 'tstat')
      end
    end
  end

  # loop through air loops (trace hours of operation back through spaces served by air loops)
  air_loop_hash = {} # key is zone and hash is hours of operation
  model.getAirLoopHVACs.sort.each do |air_loop|
    # identify hours of operation
    air_loop_spaces = []
    air_loop.thermalZones.sort.each do |zone|
      air_loop_spaces += zone.spaces
    end
    hours_of_operation = OpenstudioStandards::Space.spaces_hours_of_operation(air_loop_spaces)
    air_loop_hash[air_loop] = hours_of_operation
    if air_loop.availabilitySchedule.to_ScheduleRuleset.is_initialized
      schedule = air_loop.availabilitySchedule.to_ScheduleRuleset.get
      OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(schedule, air_loop, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method)
    end
    avail_mgrs = air_loop.availabilityManagers
    avail_mgrs.sort.each do |avail_mgr|
      # @todo I'm finding availability mangers, but not any resources for them, even if I use OpenStudio::Model.getRecursiveChildren(avail_mgr)
      resources = avail_mgr.resources
      resources = OpenStudio::Model.getRecursiveResources(avail_mgr)
      resources.sort.each do |resource|
        if resource.to_ScheduleRuleset.is_initialized
          schedule = resource.to_ScheduleRuleset.get
          OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(schedule, avail_mgr, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method)
        end
      end
    end
  end

  # look through all model HVAC components find scheduleRuleset objects, resources, that use them and zone or air loop for hours of operation
  hvac_components = model.getHVACComponents
  hvac_components.sort.each do |component|
    # identify zone, or air loop it refers to, some may refer to plant loop, OA or other component
    thermal_zone = nil
    air_loop = nil
    plant_loop = nil
    schedules = []
    if component.to_ZoneHVACComponent.is_initialized && component.to_ZoneHVACComponent.get.thermalZone.is_initialized
      thermal_zone = component.to_ZoneHVACComponent.get.thermalZone.get
    end
    if component.airLoopHVAC.is_initialized
      air_loop = component.airLoopHVAC.get
    end
    if component.plantLoop.is_initialized
      plant_loop = component.plantLoop.get
    end
    component.resources.sort.each do |resource|
      if resource.to_ThermalZone.is_initialized
        thermal_zone = resource.to_ThermalZone.get
      elsif resource.to_ScheduleRuleset.is_initialized
        schedules << resource.to_ScheduleRuleset.get
      end
    end

    # inspect resources for children of objects found in thermal zone or plant loop
    # get objects like OA controllers and unitary object components
    next if thermal_zone.nil? && air_loop.nil?

    children = OpenStudio::Model.getRecursiveChildren(component)
    children.sort.each do |child|
      child.resources.sort.each do |sub_resource|
        if sub_resource.to_ScheduleRuleset.is_initialized
          schedules << sub_resource.to_ScheduleRuleset.get
        end
      end
    end

    # process schedules found for this component
    schedules.sort.each do |schedule|
      hours_of_operation = nil
      if !thermal_zone.nil?
        hours_of_operation = thermal_zone_hash[thermal_zone]
      elsif !air_loop.nil?
        hours_of_operation = air_loop_hash[air_loop]
      elsif !plant_loop.nil?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Parametric.Model', "#{schedule.name.get} is associated with plant loop, will not gather parametric inputs")
        next
      else
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.Model', "Cannot identify where #{component.name.get} is in system. Will not gather parametric inputs for #{schedule.name.get}")
        next
      end
      OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(schedule, component, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method)
    end
  end

  # @todo Service Water Heating supply side (may or may not be associated with a space)
  # @todo water use equipment definitions (temperature, sensible, latent) may be in multiple spaces, need to identify hoo, but typically constant schedules

  # water use equipment (flow rate fraction)
  # @todo address common schedules used across multiple instances
  model.getWaterUseEquipments.sort.each do |water_use_equipment|
    if water_use_equipment.flowRateFractionSchedule.is_initialized && water_use_equipment.flowRateFractionSchedule.get.to_ScheduleRuleset.is_initialized
      schedule = water_use_equipment.flowRateFractionSchedule.get.to_ScheduleRuleset.get
      next if parametric_inputs.key?(schedule)

      opt_space = water_use_equipment.space
      if opt_space.is_initialized
        space = space.get
        hours_of_operation = OpenstudioStandards::Space.space_hours_of_operation(space)
        OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(schedule, water_use_equipment, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method)
      else
        hours_of_operation = OpenstudioStandards::Space.spaces_hours_of_operation(model.getSpaces)
        if !hours_of_operation.nil?
          OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(schedule, water_use_equipment, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: hoo_var_method)
        end
      end

    end
  end
  # @todo Refrigeration (will be associated with thermal zone)
  # @todo exterior lights (will be astronomical, but like AEDG's may have reduction later at night)

  return parametric_inputs
end

.schedule_compact_get_design_day_min_max(schedule_compact, type = 'winter') ⇒ Object

Returns the ScheduleCompact minimum and maximum values during the winter or summer design day.

return [Hash] returns a hash with ‘min’ and ‘max’ values

Parameters:

  • schedule_compact (OpenStudio::Model::ScheduleCompact)

    OpenStudio ScheduleCompact object

  • type (String) (defaults to: 'winter')

    ‘winter’ for the winter design day, ‘summer’ for the summer design day



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/openstudio-standards/schedules/information.rb', line 226

def self.schedule_compact_get_design_day_min_max(schedule_compact, type = 'winter')
  vals = []
  design_day_flag = false
  prev_str = ''
  schedule_compact.extensibleGroups.each do |eg|
    if design_day_flag && prev_str.include?('until')
      val = eg.getDouble(0)
      if val.is_initialized
        vals << val.get
      end
    end

    str = eg.getString(0)
    if str.is_initialized
      prev_str = str.get.downcase
      if prev_str.include?('for:')
        # Process a new day schedule, turn the flag off.
        design_day_flag = false
        # in the same line, if there is design day label and matches the type, turn the flag back on.
        if prev_str.include?(type) || prev_str.include?('alldays')
          design_day_flag = true
        end
      end
    end
  end

  # Error if no values were found
  if vals.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "Could not find any value in #{schedule_compact.name} design day schedule when determining min and max.")
    result = { 'min' => nil, 'max' => nil }
    return result
  end

  result = { 'min' => vals.min, 'max' => vals.max }

  return result
end

.schedule_compact_get_hourly_values(schedule_compact) ⇒ Array<Double>

Returns an array of average hourly values from a ScheduleCompact object Returns 8760 values, 8784 for leap years.

Parameters:

  • schedule_compact (OpenStudio::Model::ScheduleCompact)

    OpenStudio ScheduleCompact object

Returns:

  • (Array<Double>)

    Array of hourly values for the year



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/openstudio-standards/schedules/information.rb', line 269

def self.schedule_compact_get_hourly_values(schedule_compact)
  # set a ScheduleTypeLimits if none is present
  # this is required for the ScheduleTranslator instantiation
  unless schedule_compact.scheduleTypeLimits.is_initialized
    schedule_type_limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
    schedule_compact.setScheduleTypeLimits(schedule_type_limits)
  end

  # convert to a ScheduleRuleset and use its method
  sch_translator = ScheduleTranslator.new(schedule_compact.model, schedule_compact)
  schedule_ruleset = sch_translator.convert_schedule_compact_to_schedule_ruleset
  result = OpenstudioStandards::Schedules.schedule_ruleset_get_hourly_values(schedule_ruleset)

  return result
end

.schedule_compact_get_min_max(schedule_compact) ⇒ Object

Returns the ScheduleCompact minimum and maximum values encountered during the run-period. This method does not include summer and winter design day values.

return [Hash] returns a hash with ‘min’ and ‘max’ values

Parameters:

  • schedule_compact (OpenStudio::Model::ScheduleCompact)

    OpenStudio ScheduleCompact object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/openstudio-standards/schedules/information.rb', line 193

def self.schedule_compact_get_min_max(schedule_compact)
  vals = []
  prev_str = ''
  schedule_compact.extensibleGroups.each do |eg|
    if prev_str.include?('until')
      val = eg.getDouble(0)
      if val.is_initialized
        vals << eg.getDouble(0).get
      end
    end
    str = eg.getString(0)
    if str.is_initialized
      prev_str = str.get.downcase
    end
  end

  # Error if no values were found
  if vals.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "Could not find any value in #{schedule_compact.name} when determining min and max.")
    result = { 'min' => nil, 'max' => nil }
    return result
  end

  result = { 'min' => vals.min, 'max' => vals.max }

  return result
end

.schedule_constant_get_design_day_min_max(schedule_constant, type) ⇒ Object

Returns the ScheduleConstant minimum and maximum values during the winter or summer design day.

return [Hash] returns a hash with ‘min’ and ‘max’ values

Parameters:

  • schedule_constant (OpenStudio::Model::ScheduleConstant)

    OpenStudio ScheduleConstant object

  • type (String)

    ‘winter’ for the winter design day, ‘summer’ for the summer design day



151
152
153
154
155
# File 'lib/openstudio-standards/schedules/information.rb', line 151

def self.schedule_constant_get_design_day_min_max(schedule_constant, type)
  result = { 'min' => schedule_constant.value, 'max' => schedule_constant.value }

  return result
end

.schedule_constant_get_equivalent_full_load_hours(schedule_constant) ⇒ Object

Returns SheduleConstant equivalent full load hours (EFLH). For example a fractional schedule of 0.5, 24/7, 365 would return a value of 4380. This method includes leap days on leap years.

return [Double] The total equivalent full load hours for this schedule

Parameters:

  • schedule_constant (OpenStudio::Model::ScheduleConstant)

    OpenStudio ScheduleConstant object



163
164
165
166
167
168
169
# File 'lib/openstudio-standards/schedules/information.rb', line 163

def self.schedule_constant_get_equivalent_full_load_hours(schedule_constant)
  hours = 8760
  hours += 24 if schedule_constant.model.getYearDescription.isLeapYear
  eflh = schedule_constant.value * hours

  return eflh
end

.schedule_constant_get_hourly_values(schedule_constant) ⇒ Array<Double>

Returns an array of average hourly values from a ScheduleConstant object Returns 8760 values, 8784 for leap years.

Parameters:

  • schedule_constant (OpenStudio::Model::ScheduleConstant)

    OpenStudio ScheduleConstant object

Returns:

  • (Array<Double>)

    Array of hourly values for the year



176
177
178
179
180
181
182
# File 'lib/openstudio-standards/schedules/information.rb', line 176

def self.schedule_constant_get_hourly_values(schedule_constant)
  hours = 8760
  hours += 24 if schedule_constant.model.getYearDescription.isLeapYear
  values = Array.new(hours) { schedule_constant.value }

  return values
end

.schedule_constant_get_min_max(schedule_constant) ⇒ Object

Returns the ScheduleConstant minimum and maximum values encountered during the run-period. This method does not include summer and winter design day values.

return [Hash] returns a hash with ‘min’ and ‘max’ values

Parameters:

  • schedule_constant (OpenStudio::Model::ScheduleConstant)

    OpenStudio ScheduleConstant object



140
141
142
143
144
# File 'lib/openstudio-standards/schedules/information.rb', line 140

def self.schedule_constant_get_min_max(schedule_constant)
  result = { 'min' => schedule_constant.value, 'max' => schedule_constant.value }

  return result
end

.schedule_day_adjust_from_parameters(schedule_day, hoo_start, hoo_end, val_flr, val_clg, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order) ⇒ OpenStudio::Model::ScheduleDay

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

adjust individual schedule profiles from parametric inputs

Parameters:

  • schedule_day (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object

  • hoo_start (Double)

    hours of operation start

  • hoo_end (Double)

    hours of operation end

  • val_flr (Double)

    value floor

  • val_clg (Double)

    value ceiling

  • ramp_frequency (Double)

    ramp frequency in minutes

  • infer_hoo_for_non_assigned_objects (Boolean)

    attempt to get hoo for objects like swh with and exterior lighting

  • error_on_out_of_order (Boolean)

    true will error if applying formula creates out of order values

Returns:

  • (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object

Author:

  • David Goldwasser



1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
# File 'lib/openstudio-standards/schedules/parametric.rb', line 1145

def self.schedule_day_adjust_from_parameters(schedule_day, hoo_start, hoo_end, val_flr, val_clg, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order)
  # process hoo and floor/ceiling vars to develop formulas without variables
  formula_string = schedule_day.additionalProperties.getFeatureAsString('param_day_profile').get
  formula_hash = {}
  formula_string.split('|').each do |time_val_valopt|
    a1 = time_val_valopt.to_s.split('~')
    time = a1[0]
    value_array = a1.drop(1)
    formula_hash[time] = value_array
  end

  # setup additional variables
  if hoo_end >= hoo_start
    occ = hoo_end - hoo_start
  else
    occ = 24.0 + hoo_end - hoo_start
  end
  vac = 24.0 - occ
  range = val_clg - val_flr

  timestep_minutes = (0..60).step(60 * ramp_frequency).to_a

  OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleDay', "Schedule #{schedule_day.name} has this formula hash: #{formula_hash}")

  # apply variables and create updated hash with only numbers
  formula_hash_var_free = {}
  formula_hash.each do |time, val_in_out|
    # replace time variables with value
    time = time.gsub('hoo_start', hoo_start.to_s)
    time = time.gsub('hoo_end', hoo_end.to_s)
    time = time.gsub('occ', occ.to_s)
    # can save special variables like lunch or break using this logic
    mid_start = hoo_start + (occ * 0.5)
    mid_start_min = mid_start.modulo(1) * 60
    mid_start_min_ts = timestep_minutes.min { |a, b| (a - mid_start_min).abs <=> (b - mid_start_min).abs }
    mid_start_adjusted = mid_start.floor + (mid_start_min_ts / 60)
    time = time.gsub('mid', mid_start_adjusted.to_s)
    time = time.gsub('vac', vac.to_s)
    begin
      time_float = eval(time)
      if time_float.to_i.to_s == time_float.to_s || time_float.to_f.to_s == time_float.to_s # check to see if numeric
        time_float = time_float.to_f
      else
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Parametric.ScheduleDay', "Time formula #{time} for #{schedule_day.name} is invalid. It can't be converted to a float.")
      end
    rescue SyntaxError => e
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Parametric.ScheduleDay', "Time formula #{time} for #{schedule_day.name} is invalid. It can't be evaluated.")
    end

    # replace variables in array of values
    val_in_out_float = []
    val_in_out.each do |val|
      # replace variables for values
      val = val.gsub('val_flr', val_flr.to_s)
      val = val.gsub('val_clg', val_clg.to_s)
      val = val.gsub('val_range', range.to_s) # will expect a fractional value and will scale within ceiling and floor
      begin
        val_float = eval(val)
        if val_float.to_i.to_s == val_float.to_s || val_float.to_f.to_s == val_float.to_s # check to see if numeric
          val_float = val_float.to_f
        else
          OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Parametric.ScheduleDay', "Value formula #{val_float} for #{schedule_day.name} is invalid. It can't be converted to a float.")
        end
      rescue SyntaxError => e
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Parametric.ScheduleDay', "Time formula #{val_float} for #{schedule_day.name} is invalid. It can't be evaluated.")
      end
      val_in_out_float << val_float
    end

    # update hash
    formula_hash_var_free[time_float] = val_in_out_float
  end

  # this is old variable used in loop, just combining for now to avoid refactor, may change this later
  time_value_pairs = []
  formula_hash_var_free.each do |time, val_in_out|
    val_in_out.each do |val|
      time_value_pairs << [time, val]
    end
  end

  OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleDay', "Schedule #{schedule_day.name} will be adjusted with these time-value pairs: #{time_value_pairs}")

  # re-order so first value is lowest, and last is highest (need to adjust so no negative or > 24 values first)
  neg_time_hash = {}
  temp_min_time_hash = {}
  time_value_pairs.each_with_index do |pair, i|
    # if value  24 add it to 24 so it will go on tail end of profile
    # case when value is greater than 24 can be left alone for now, will be addressed
    if pair[0] < 0.0
      neg_time_hash[i] = pair[0]
      time = pair[0] + 24.0
      time_value_pairs[i][0] = time
    else
      time = pair[0]
    end
    temp_min_time_hash[i] = pair[0]
  end
  time_value_pairs.rotate!(temp_min_time_hash.key(temp_min_time_hash.values.min))

  # validate order, issue warning and correct if out of order
  last_time = nil
  throw_order_warning = false
  pre_fix_time_value_pairs = time_value_pairs.to_s
  time_value_pairs.each_with_index do |time_value_pair, i|
    if last_time.nil?
      last_time = time_value_pair[0]
    elsif time_value_pair[0] < last_time || neg_time_hash.key?(i)

      # @todo it doesn't actually stop here now
      if error_on_out_of_order
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Parametric.ScheduleDay', "Pre-interpolated processed hash for #{schedule_day.name} has one or more out of order conflicts: #{pre_fix_time_value_pairs}. Method will stop because Error on Out of Order was set to true.")
      end

      if neg_time_hash.key?(i)
        orig_current_time = time_value_pair[0]
        updated_time = 0.0
        last_buffer = 'NA'
      else
        # determine much space last item can move
        if i < 2
          last_buffer = time_value_pairs[i - 1][0] # can move down to 0 without any issues
        else
          last_buffer = time_value_pairs[i - 1][0] - time_value_pairs[i - 2][0]
        end

        # move to previous timestep but don't exceed available buffer
        updated_time = time_value_pairs[i - 1][0] - [ramp_frequency, last_buffer].min
      end

      # update values in array
      orig_current_time = time_value_pair[0]
      time_value_pairs[i - 1][0] = updated_time
      time_value_pairs[i][0] = updated_time

      # reporting mostly for diagnostic purposes
      # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Parametric.ScheduleDay', "For #{schedule_day.name} profile item #{i} time was #{last_time} and item #{i + 1} time was #{orig_current_time}. Last buffer is #{last_buffer}. Changing both times to #{updated_time}.")

      last_time = updated_time
      throw_order_warning = true

    else
      last_time = time_value_pair[0]
    end
  end

  # issue warning if order was changed
  if throw_order_warning
    # OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.ScheduleDay', "Pre-interpolated processed hash for #{schedule_day.name} has one or more out of order conflicts: #{pre_fix_time_value_pairs}. Time values were adjusted as shown to crate a valid profile: #{time_value_pairs}")
  end

  # add interpolated values at ramp_frequency
  time_value_pairs.each_with_index do |time_value_pair, i|
    # store current and next time and value
    current_time = time_value_pair[0]
    current_value = time_value_pair[1]
    if i + 1 < time_value_pairs.size
      next_time = time_value_pairs[i + 1][0]
      next_value = time_value_pairs[i + 1][1]
    else
      # use time and value of first item
      next_time = time_value_pairs[0][0] + 24 # need to adjust values for beginning of array
      next_value = time_value_pairs[0][1]
    end
    step_delta = next_time - current_time

    # skip if time between values is 0 or less than ramp frequency
    next if step_delta <= ramp_frequency

    # skip if next value is same
    next if current_value == next_value

    # add interpolated value to array
    interpolated_time = current_time + ramp_frequency
    interpolated_value = (next_value * (interpolated_time - current_time) / step_delta) + (current_value * (next_time - interpolated_time) / step_delta)
    time_value_pairs.insert(i + 1, [interpolated_time, interpolated_value])
  end

  # remove second instance of time when there are two
  time_values_used = []
  items_to_remove = []
  time_value_pairs.each_with_index do |time_value_pair, i|
    if time_values_used.include? time_value_pair[0]
      items_to_remove << i
    else
      time_values_used << time_value_pair[0]
    end
  end
  items_to_remove.reverse.each do |i|
    time_value_pairs.delete_at(i)
  end

  # if time is > 24 shift to front of array and adjust value
  rotate_steps = 0
  time_value_pairs.reverse.each_with_index do |time_value_pair, i|
    next unless time_value_pair[0] > 24

    rotate_steps -= 1
    time_value_pair[0] -= 24
  end
  time_value_pairs.rotate!(rotate_steps)

  # add a 24 on the end of array that matches the first value
  if time_value_pairs.last[0].to_i != 24
    time_value_pairs << [24.0, time_value_pairs.first[1]]
  end

  # OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleDay', "Schedule #{schedule_day.name} will be adjusted with these time-value pairs: #{time_value_pairs}")

  # reset scheduleDay values based on interpolated values
  schedule_day.clearValues
  time_value_pairs.each do |time_val|
    hour = time_val.first.floor
    min = ((time_val.first - hour) * 60.0).floor
    os_time = OpenStudio::Time.new(0, hour, min, 0)
    value = time_val.last
    schedule_day.addValue(os_time, value)
  end
  # @todo apply secondary logic

  # Tell EnergyPlus to interpolate schedules to timestep so that it doesn't have to be done in this code
  # sch_day.setInterpolatetoTimestep(true)
  # if model.version < OpenStudio::VersionString.new('3.8.0')
  #   day_sch.setInterpolatetoTimestep(true)
  # else
  #   day_sch.setInterpolatetoTimestep('Average')
  # end

  return schedule_day
end

.schedule_day_get_equivalent_full_load_hours(schedule_day) ⇒ Object

Returns the ScheduleDay daily equivalent full load hours (EFLH).

return [Double] The daily total equivalent full load hours for this schedule

Parameters:

  • schedule_day (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/openstudio-standards/schedules/information.rb', line 317

def self.schedule_day_get_equivalent_full_load_hours(schedule_day)
  daily_flh = 0
  values = schedule_day.values
  times = schedule_day.times

  previous_time_decimal = 0
  times.each_with_index do |time, i|
    time_decimal = (time.days * 24.0) + time.hours + (time.minutes / 60.0) + (time.seconds / 3600.0)
    duration_of_value = time_decimal - previous_time_decimal
    daily_flh += values[i] * duration_of_value
    previous_time_decimal = time_decimal
  end

  return daily_flh
end

.schedule_day_get_hourly_values(schedule_day, model = nil) ⇒ Array<Double>

Returns an array of average hourly values from a ScheduleDay object Returns 24 values

Parameters:

  • schedule_day (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object

Returns:

  • (Array<Double>)

    Array of hourly values for the day



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/openstudio-standards/schedules/information.rb', line 338

def self.schedule_day_get_hourly_values(schedule_day, model = nil)
  schedule_values = []

  if model.nil?
    model = schedule_day.model
  end

  if model.version.str < '3.8.0'
    # determine smallest time interval
    times = schedule_day.times
    time_interval_min = 15.0
    previous_time_decimal = 0.0
    times.each_with_index do |time, i|
      time_decimal = (time.days * 24.0 * 60.0) + (time.hours * 60.0) + time.minutes + (time.seconds / 60)
      interval_min = time_decimal - previous_time_decimal
      time_interval_min = interval_min if interval_min < time_interval_min
      previous_time_decimal = time_decimal
    end
    time_interval_min = time_interval_min.round(0).to_i

    # get the hourly average by averaging the values in the hour at the smallest time interval
    (0..23).each do |j|
      values = []
      times = (time_interval_min..60).step(time_interval_min).to_a
      times.each { |t| values << schedule_day.getValue(OpenStudio::Time.new(0, j, t, 0)) }
      schedule_values << (values.sum / times.size).round(5)
    end
  else
    num_timesteps = model.getTimestep.numberOfTimestepsPerHour
    day_timeseries = schedule_day.timeSeries.values.to_a
    schedule_values = day_timeseries.each_slice(num_timesteps).map { |slice| slice.sum / slice.size.to_f }
  end

  unless schedule_values.size == 24
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} returned illegal number of values: #{schedule_values.size}.")
    return false
  end

  return schedule_values
end

.schedule_day_get_min_max(schedule_day) ⇒ Hash

Returns the ScheduleDay minimum and maximum values

Parameters:

  • schedule_day (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object

Returns:

  • (Hash)

    returns a hash with ‘min’ and ‘max’ values



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/openstudio-standards/schedules/information.rb', line 293

def self.schedule_day_get_min_max(schedule_day)
  min = nil
  max = nil
  values = schedule_day.values
  values.each do |value|
    if min.nil?
      min = value
    else
      if min > value then min = value end
    end
    if max.nil?
      max = value
    else
      if max < value then max = value end
    end
  end

  result = { 'min' => min, 'max' => max }
end

.schedule_day_multiply_by_value(schedule_day, multiplier, lower_apply_limit: nil) ⇒ OpenStudio::Model::ScheduleDay

Method to multiply the values in a day schedule by a specified value The method can optionally apply the multiplier to only values above a lower limit. This limit prevents multipliers for things like occupancy sensors from affecting unoccupied hours.

Parameters:

  • schedule_day (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object

  • multiplier (Double)

    value to multiply schedule values by

  • lower_apply_limit (Double) (defaults to: nil)

    apply the multiplier to only values above this value

Returns:

  • (OpenStudio::Model::ScheduleDay)

    OpenStudio ScheduleDay object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/openstudio-standards/schedules/modify.rb', line 16

def self.schedule_day_multiply_by_value(schedule_day, multiplier, lower_apply_limit: nil)
  # Record the original times and values
  times = schedule_day.times
  values = schedule_day.values

  # Remove the original times and values
  schedule_day.clearValues

  # Create new values by using the multiplier on the original values
  new_values = []
  values.each do |value|
    if lower_apply_limit.nil?
      new_values << (value * multiplier)
    else
      if value > lower_apply_limit
        new_values << (value * multiplier)
      else
        new_values << value
      end
    end
  end

  # Add the revised time/value pairs to the schedule
  new_values.each_with_index do |new_value, i|
    schedule_day.addValue(times[i], new_value)
  end

  return schedule_day
end

.schedule_day_populate_from_array_of_values(schedule_day, value_array) ⇒ OpenStudio::Model::ScheduleDay

Sets the values of a day schedule from an array of values Clears out existing time value pairs and sets to supplied values

Parameters:

  • schedule_day (OpenStudio::Model::ScheduleDay)

    The day schedule to set.

  • value_array (Array)

    Array of 24 values. Schedule times set based on value index. Identical values will be skipped.

Returns:

  • (OpenStudio::Model::ScheduleDay)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/openstudio-standards/schedules/modify.rb', line 78

def self.schedule_day_populate_from_array_of_values(schedule_day, value_array)
  schedule_day.clearValues
  if value_array.size != 24
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Modify', "#{__method__} expects value_array to contain 24 values, instead #{value_array.size} values were given. Resulting schedule will use first #{[24, value_array.size].min} values")
  end

  value_array[0..23].each_with_index do |value, h|
    next if value == value_array[h + 1]

    time = OpenStudio::Time.new(0, h + 1, 0, 0)
    schedule_day.addValue(time, value)
  end
  return schedule_day
end

.schedule_day_set_hours_of_operation(schedule_day, start_time, end_time) ⇒ Void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Set the hours of operation (0 or 1) for a ScheduleDay. Clears out existing time/value pairs and sets to supplied values.

Parameters:

  • schedule_day (OpenStudio::Model::ScheduleDay)

    The day schedule to set.

  • start_time (OpenStudio::Time)

    Start time.

  • end_time (OpenStudio::Time)

    End time. If greater than 24:00, hours of operation will wrap over midnight.

Returns:

  • (Void)

Author:

  • Andrew Parker



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/openstudio-standards/schedules/modify.rb', line 56

def self.schedule_day_set_hours_of_operation(schedule_day, start_time, end_time)
  schedule_day.clearValues
  twenty_four_hours = OpenStudio::Time.new(0, 24, 0, 0)
  if end_time < twenty_four_hours
    # Operating hours don't wrap over midnight
    schedule_day.addValue(start_time, 0) # 0 until start time
    schedule_day.addValue(end_time, 1) # 1 from start time until end time
    schedule_day.addValue(twenty_four_hours, 0) # 0 after end time
  else
    # Operating hours start on previous day
    schedule_day.addValue(end_time - twenty_four_hours, 1) # 1 for hours started on the previous day
    schedule_day.addValue(start_time, 0) # 0 from end of previous days hours until start of today's
    schedule_day.addValue(twenty_four_hours, 1) # 1 from start of today's hours until midnight
  end
end

.schedule_get_design_day_min_max(schedule, type = 'winter') ⇒ Object

Returns the Schedule minimum and maximum values during the winter or summer design day.

return [Hash] returns a hash with ‘min’ and ‘max’ values

Parameters:

  • schedule (OpenStudio::Model::Schedule)

    OpenStudio Schedule object

  • type (String) (defaults to: 'winter')

    ‘winter’ for the winter design day, ‘summer’ for the summer design day



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/openstudio-standards/schedules/information.rb', line 45

def self.schedule_get_design_day_min_max(schedule, type = 'winter')
  case schedule.iddObjectType.valueName.to_s
  when 'OS_Schedule_Ruleset'
    schedule = schedule.to_ScheduleRuleset.get
    result = OpenstudioStandards::Schedules.schedule_ruleset_get_design_day_min_max(schedule, type)
  when 'OS_Schedule_Constant'
    schedule = schedule.to_ScheduleConstant.get
    result = OpenstudioStandards::Schedules.schedule_constant_get_design_day_min_max(schedule, type)
  when 'OS_Schedule_Compact'
    schedule = schedule.to_ScheduleCompact.get
    result = OpenstudioStandards::Schedules.schedule_compact_get_design_day_min_max(schedule, type)
  when 'OS_Schedule_Year'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleYear schedules.")
    result = { 'min' => nil, 'max' => nil }
  when 'OS_Schedule_Interval'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleInterval schedules.")
    result = { 'min' => nil, 'max' => nil }
  else
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "unrecognized schedule type #{schedule.iddObjectType.valueName} for #{__method__}.")
    result = { 'min' => nil, 'max' => nil }
  end

  return result
end

.schedule_get_equivalent_full_load_hours(schedule) ⇒ Object

Returns the Schedule equivalent full load hours (EFLH). For example a fractional schedule of 0.5, 24/7, 365 would return a value of 4380. This method includes leap days on leap years.

return [Double] The total equivalent full load hours for this schedule

Parameters:

  • schedule (OpenStudio::Model::Schedule)

    OpenStudio Schedule object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/openstudio-standards/schedules/information.rb', line 76

def self.schedule_get_equivalent_full_load_hours(schedule)
  case schedule.iddObjectType.valueName.to_s
  when 'OS_Schedule_Ruleset'
    schedule = schedule.to_ScheduleRuleset.get
    result = OpenstudioStandards::Schedules.schedule_ruleset_get_equivalent_full_load_hours(schedule)
  when 'OS_Schedule_Constant'
    schedule = schedule.to_ScheduleConstant.get
    result = OpenstudioStandards::Schedules.schedule_constant_get_equivalent_full_load_hours(schedule)
  when 'OS_Schedule_Compact'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleCompact schedules.")
    result = nil
  when 'OS_Schedule_Year'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleYear schedules.")
    result = nil
  when 'OS_Schedule_Interval'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleInterval schedules.")
    result = nil
  else
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "unrecognized schedule type #{schedule.iddObjectType.valueName} for #{__method__}.")
    result = nil
  end

  return result
end

.schedule_get_hourly_values(schedule) ⇒ Array<Double>

Returns an array of average hourly values from a Schedule object Returns 8760 values, 8784 for leap years.

Parameters:

  • schedule (OpenStudio::Model::Schedule)

    OpenStudio Schedule object

Returns:

  • (Array<Double>)

    Array of hourly values for the year



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/openstudio-standards/schedules/information.rb', line 106

def self.schedule_get_hourly_values(schedule)
  case schedule.iddObjectType.valueName.to_s
  when 'OS_Schedule_Ruleset'
    schedule = schedule.to_ScheduleRuleset.get
    result = OpenstudioStandards::Schedules.schedule_ruleset_get_hourly_values(schedule)
  when 'OS_Schedule_Constant'
    schedule = schedule.to_ScheduleConstant.get
    result = OpenstudioStandards::Schedules.schedule_constant_get_hourly_values(schedule)
  when 'OS_Schedule_Compact'
    schedule = schedule.to_ScheduleCompact.get
    result = OpenstudioStandards::Schedules.schedule_compact_get_hourly_values(schedule)
  when 'OS_Schedule_Year'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleYear schedules.")
    result = nil
  when 'OS_Schedule_Interval'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleInterval schedules.")
    result = nil
  else
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "unrecognized schedule type #{schedule.iddObjectType.valueName} for #{__method__}.")
    result = nil
  end

  return result
end

.schedule_get_min_max(schedule, only_run_period_values: false) ⇒ Object

Returns the Schedule minimum and maximum values encountered during the run-period. This method does not include summer and winter design day values.

return [Hash] returns a hash with ‘min’ and ‘max’ values

Parameters:

  • schedule (OpenStudio::Model::Schedule)

    OpenStudio Schedule object

  • only_run_period_values (Bool) (defaults to: false)

    check values encountered only during the run period Default to false. Only applicable to ScheduleRuleset schedules. This will ignore ScheduleRules or the DefaultDaySchedule if never used.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/openstudio-standards/schedules/information.rb', line 15

def self.schedule_get_min_max(schedule, only_run_period_values: false)
  case schedule.iddObjectType.valueName.to_s
  when 'OS_Schedule_Ruleset'
    schedule = schedule.to_ScheduleRuleset.get
    result = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(schedule, only_run_period_values: only_run_period_values)
  when 'OS_Schedule_Constant'
    schedule = schedule.to_ScheduleConstant.get
    result = OpenstudioStandards::Schedules.schedule_constant_get_min_max(schedule)
  when 'OS_Schedule_Compact'
    schedule = schedule.to_ScheduleCompact.get
    result = OpenstudioStandards::Schedules.schedule_compact_get_min_max(schedule)
  when 'OS_Schedule_Year'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleYear schedules.")
    result = { 'min' => nil, 'max' => nil }
  when 'OS_Schedule_Interval'
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} does not yet support ScheduleInterval schedules.")
    result = { 'min' => nil, 'max' => nil }
  else
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "unrecognized schedule type #{schedule.iddObjectType.valueName} for #{__method__}.")
    result = { 'min' => nil, 'max' => nil }
  end

  return result
end

.schedule_ruleset_add_rule(schedule_ruleset, values, start_date: nil, end_date: nil, day_names: nil, rule_name: nil) ⇒ OpenStudio::Model::ScheduleRule

Add a ScheduleRule to a ScheduleRuleset object from an array of hourly values

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • start_date (OpenStudio::Date) (defaults to: nil)

    start date of week period

  • end_date (OpenStudio::Date) (defaults to: nil)

    end date of week period

  • day_names (Array<String>) (defaults to: nil)

    list of days of week for which this day type is applicable

  • values (Array<Double>)

    array of 24 hourly values for a day

  • rule_name (String) (defaults to: nil)

    rule ScheduleDay object name

Returns:

  • (OpenStudio::Model::ScheduleRule)

    OpenStudio ScheduleRule object



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/openstudio-standards/schedules/modify.rb', line 106

def self.schedule_ruleset_add_rule(schedule_ruleset, values,
                                   start_date: nil,
                                   end_date: nil,
                                   day_names: nil,
                                   rule_name: nil)
  # create new schedule rule
  sch_rule = OpenStudio::Model::ScheduleRule.new(schedule_ruleset)
  day_sch = sch_rule.daySchedule
  day_sch.setName(rule_name) unless rule_name.nil?

  # set the dates when the rule applies
  sch_rule.setStartDate(start_date) unless start_date.nil?
  sch_rule.setEndDate(end_date) unless end_date.nil?

  # set the days for which the rule applies
  unless day_names.nil?
    day_names.each do |day_of_week|
      sch_rule.setApplySunday(true) if day_of_week == 'Sunday'
      sch_rule.setApplyMonday(true) if day_of_week == 'Monday'
      sch_rule.setApplyTuesday(true) if day_of_week == 'Tuesday'
      sch_rule.setApplyWednesday(true) if day_of_week == 'Wednesday'
      sch_rule.setApplyThursday(true) if day_of_week == 'Thursday'
      sch_rule.setApplyFriday(true) if day_of_week == 'Friday'
      sch_rule.setApplySaturday(true) if day_of_week == 'Saturday'
    end
  end

  # Create the day schedule and add hourly values
  (0..23).each do |ihr|
    next if values[ihr] == values[ihr + 1]

    day_sch.addValue(OpenStudio::Time.new(0, ihr + 1, 0, 0), values[ihr])
  end

  return sch_rule
end

.schedule_ruleset_adjust_hours_of_operation(schedule_ruleset, options = {}) ⇒ OpenStudio::Model::ScheduleRuleset

Adjust hours of operation

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • options (Hash) (defaults to: {})

    Hash of argument options

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/openstudio-standards/schedules/modify.rb', line 350

def self.schedule_ruleset_adjust_hours_of_operation(schedule_ruleset, options = {})
  defaults = {
    'base_start_hoo' => 8.0, # may not be good idea to have default
    'base_finish_hoo' => 18.0, # may not be good idea to have default
    'delta_length_hoo' => 0.0,
    'shift_hoo' => 0.0,
    'default' => true,
    'mon' => true,
    'tue' => true,
    'wed' => true,
    'thur' => true,
    'fri' => true,
    'sat' => true,
    'sun' => true,
    'summer' => false,
    'winter' => false
  }

  # merge user inputs with defaults
  options = defaults.merge(options)

  # grab schedule out of argument
  if schedule_ruleset.to_ScheduleRuleset.is_initialized
    schedule = schedule_ruleset.to_ScheduleRuleset.get
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Modify', "schedule_ruleset_adjust_hours_of_operation only applies to ScheduleRuleset objects. Skipping #{schedule.name}")
    return nil
  end

  # array of all profiles to change
  profiles = []

  # push default profiles to array
  if options['default']
    profiles << schedule.defaultDaySchedule
  end

  # push profiles to array
  schedule.scheduleRules.each do |rule|
    day_sch = rule.daySchedule

    # if any day requested also exists in the rule, then it will be altered
    alter_rule = false
    if rule.applyMonday && rule.applyMonday == options['mon'] then alter_rule = true end
    if rule.applyTuesday && rule.applyTuesday == options['tue'] then alter_rule = true end
    if rule.applyWednesday && rule.applyWednesday == options['wed'] then alter_rule = true end
    if rule.applyThursday && rule.applyThursday == options['thur'] then alter_rule = true end
    if rule.applyFriday && rule.applyFriday == options['fri'] then alter_rule = true end
    if rule.applySaturday && rule.applySaturday == options['sat'] then alter_rule = true end
    if rule.applySunday && rule.applySunday == options['sun'] then alter_rule = true end

    # @todo add in logic to warn user about conflicts where a single rule has conflicting tests

    if alter_rule
      profiles << day_sch
    end
  end

  # add design days to array
  if options['summer']
    profiles << schedule.summerDesignDaySchedule
  end
  if options['winter']
    profiles << schedule.winterDesignDaySchedule
  end

  # give info messages as I change specific profiles
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Schedules.Modify', "Adjusting #{schedule.name}")

  # rename schedule
  schedule.setName("#{schedule.name} - extend #{options['delta_length_hoo']} shift #{options['shift_hoo']}")

  # break time args into hours and minutes
  start_hoo_hours = (options['base_start_hoo']).to_i
  start_hoo_minutes = (((options['base_start_hoo']) - (options['base_start_hoo']).to_i) * 60).to_i
  finish_hoo_hours = (options['base_finish_hoo']).to_i
  finish_hoo_minutes = (((options['base_finish_hoo']) - (options['base_finish_hoo']).to_i) * 60).to_i
  delta_hours = (options['delta_length_hoo']).to_i
  delta_minutes = (((options['delta_length_hoo']) - (options['delta_length_hoo']).to_i) * 60).to_i
  shift_hours = (options['shift_hoo']).to_i
  shift_minutes = (((options['shift_hoo']) - (options['shift_hoo']).to_i) * 60).to_i

  # time objects to use in measure
  time_0 = OpenStudio::Time.new(0, 0, 0, 0)
  time_1_min = OpenStudio::Time.new(0, 0, 1, 0) # add this to avoid times in day profile less than this
  time_12 =  OpenStudio::Time.new(0, 12, 0, 0)
  time_24 =  OpenStudio::Time.new(0, 24, 0, 0)
  start_hoo_time = OpenStudio::Time.new(0, start_hoo_hours, start_hoo_minutes, 0)
  finish_hoo_time = OpenStudio::Time.new(0, finish_hoo_hours, finish_hoo_minutes, 0)
  delta_time = OpenStudio::Time.new(0, delta_hours, delta_minutes, 0) # not used
  shift_time = OpenStudio::Time.new(0, shift_hours, shift_minutes, 0)

  # calculations
  if options['base_start_hoo'] <= options['base_finish_hoo']
    base_opp_day_length = options['base_finish_hoo'] - options['base_start_hoo']
    mid_hoo = start_hoo_time + ((finish_hoo_time - start_hoo_time) / 2)
    mid_non_hoo = mid_hoo + time_12
    if mid_non_hoo > time_24 then mid_non_hoo -= time_24 end
  else
    base_opp_day_length = options['base_finish_hoo'] - options['base_start_hoo'] + 24
    mid_non_hoo = finish_hoo_time + ((start_hoo_time - finish_hoo_time) / 2)
    mid_hoo = mid_non_hoo + time_12
    if mid_non_hoo > time_24 then mid_non_hoo -= time_24 end
  end
  adjusted_opp_day_length = base_opp_day_length + options['delta_length_hoo']
  hoo_time_multiplier = adjusted_opp_day_length / base_opp_day_length
  non_hoo_time_multiplier = (24 - adjusted_opp_day_length) / (24 - base_opp_day_length)

  # check for invalid input
  if adjusted_opp_day_length < 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Modify', 'Requested hours of operation adjustment results in an invalid negative hours of operation')
    return false
  end
  # check for invalid input
  if adjusted_opp_day_length > 24
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Modify', 'Requested hours of operation adjustment results in more than 24 hours of operation')
    return false
  end

  # making some temp objects to avoid having to deal with wrap around for change of hoo times
  mid_hoo < start_hoo_time ? (adj_mid_hoo = mid_hoo + time_24) : (adj_mid_hoo = mid_hoo)
  finish_hoo_time < adj_mid_hoo ? (adj_finish_hoo_time = finish_hoo_time + time_24) : (adj_finish_hoo_time = finish_hoo_time)
  mid_non_hoo < adj_finish_hoo_time ? (adj_mid_non_hoo = mid_non_hoo + time_24) : (adj_mid_non_hoo = mid_non_hoo)
  adj_start = start_hoo_time + time_24 # not used

  # edit profiles
  profiles.each do |day_sch|
    times = day_sch.times
    values = day_sch.values

    # in this case delete all values outside of
    # todo - may need similar logic if exactly 0 hours
    if adjusted_opp_day_length == 24
      start_val = day_sch.getValue(start_hoo_time)
      finish_val = day_sch.getValue(finish_hoo_time)

      # remove times out of range that should not be reference or compressed
      if start_hoo_time < finish_hoo_time
        times.each do |time|
          if time <= start_hoo_time || time > finish_hoo_time
            day_sch.removeValue(time)
          end
        end
        # add in values
        day_sch.addValue(start_hoo_time, start_val)
        day_sch.addValue(finish_hoo_time, finish_val)
        day_sch.addValue(time_24, [start_val, finish_val].max)
      else
        times.each do |time|
          if time > start_hoo_time && time <= finish_hoo_time
            day_sch.removeValue(time)
          end
        end
        # add in values
        day_sch.addValue(finish_hoo_time, finish_val)
        day_sch.addValue(start_hoo_time, start_val)
        day_sch.addValue(time_24, [values.first, values.last].max)
      end

    end

    times = day_sch.times
    values = day_sch.values

    # arrays for values to avoid overlap conflict of times
    new_times = []
    new_values = []

    # this is to store what datapoint will be first after midnight, and what the value at that time should be
    min_time_new = time_24
    min_time_value = nil

    # flag if found time at 24
    found_24_or_0 = false

    # push times to array
    times.each do |time|
      # create logic for four possible quadrants. Assume any quadrant can pass over 24/0 threshold
      time < start_hoo_time ? (temp_time = time + time_24) : (temp_time = time)

      # calculate change in time do to hoo delta
      if temp_time <= adj_finish_hoo_time
        expand_time = ((temp_time - adj_mid_hoo) * hoo_time_multiplier) - (temp_time - adj_mid_hoo)
      else
        expand_time = ((temp_time - adj_mid_non_hoo) * non_hoo_time_multiplier) - (temp_time - adj_mid_non_hoo)
      end

      new_time = time + shift_time + expand_time

      # adjust wrap around times
      if new_time < time_0
        new_time += time_24
      elsif new_time > time_24
        new_time -= time_24
      end
      new_times << new_time

      # see which new_time has the lowest value. Then add a value at 24 equal to that
      if !found_24_or_0 && new_time <= min_time_new
        min_time_new = new_time
        min_time_value = day_sch.getValue(time)
      elsif new_time == time_24 # this was added to address time exactly at 24
        min_time_new = new_time
        min_time_value = day_sch.getValue(time)
        found_24_or_0 = true
      elsif new_time == time_0
        min_time_new = new_time
        min_time_value = day_sch.getValue(time_0)
        found_24_or_0 = true
      end
    end

    # push values to array
    values.each do |value|
      new_values << value
    end

    # add value for what will be 24
    new_times << time_24
    new_values << min_time_value

    new_time_val_hash = {}
    new_times.each_with_index do |time, i|
      new_time_val_hash[time.totalHours] = { time: time, value: new_values[i] }
    end

    # clear values
    day_sch.clearValues

    new_time_val_hash = Hash[new_time_val_hash.sort]
    prev_time = nil
    new_time_val_hash.sort.each do |hours, time_val|
      if prev_time.nil? || time_val[:time] - prev_time > time_1_min
        day_sch.addValue(time_val[:time], time_val[:value])
        prev_time = time_val[:time]
      else
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Modify', "Time step in #{day_sch.name} between #{prev_time.toString} and #{time_val[:time].toString} is too small to support, not adding value.")
      end
    end
  end

  return schedule
end

.schedule_ruleset_apply_parametric_inputs(schedule_ruleset, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order, parametric_inputs = nil) ⇒ OpenStudio::Model::ScheduleRuleset

this will use parametric inputs contained in schedule and profiles along with inferred hours of operation to generate updated ruleset schedule profiles

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • ramp_frequency (Double)

    ramp frequency in minutes

  • infer_hoo_for_non_assigned_objects (Boolean)

    attempt to get hoo for objects like swh with and exterior lighting

  • error_on_out_of_order (Boolean)

    true will error if applying formula creates out of order values

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Author:

  • David Goldwasser



948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
# File 'lib/openstudio-standards/schedules/parametric.rb', line 948

def self.schedule_ruleset_apply_parametric_inputs(schedule_ruleset, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order, parametric_inputs = nil)
  # Check if parametric inputs were supplied and generate them if not
  if parametric_inputs.nil?
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Parametric.ScheduleRuleset', "For #{schedule_ruleset.name}, no parametric inputs were not supplied so they will be generated now.")
    parametric_inputs = OpenstudioStandards::Schedules.model_setup_parametric_schedules(schedule.model, gather_data_only: true)
  end

  # Check that parametric inputs exist for this schedule after generation
  if parametric_inputs[schedule_ruleset].nil?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.ScheduleRuleset', "For #{schedule_ruleset.name}, no parametric inputs exists so schedule will not be changed.")
    return schedule_ruleset
  end

  # Check that an hours of operation schedule is associated with this schedule
  if parametric_inputs[schedule_ruleset][:hoo_inputs].nil?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.ScheduleRuleset', "For #{schedule_ruleset.name}, no associated hours of operation schedule was found so schedule will not be changed.")
    return schedule_ruleset
  end

  # Get the hours of operation schedule
  hours_of_operation = parametric_inputs[schedule_ruleset][:hoo_inputs]
  # OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleRuleset', "For #{schedule_ruleset.name} hours_of_operation = #{hours_of_operation}.")

  starting_aeflh = OpenstudioStandards::Schedules.schedule_ruleset_get_equivalent_full_load_hours(schedule_ruleset)

  # store floor and ceiling value
  val_flr = nil
  if schedule_ruleset.hasAdditionalProperties && schedule_ruleset.additionalProperties.hasFeature('param_sch_floor')
    val_flr = schedule_ruleset.additionalProperties.getFeatureAsDouble('param_sch_floor').get
  end
  val_clg = nil
  if schedule_ruleset.hasAdditionalProperties && schedule_ruleset.additionalProperties.hasFeature('param_sch_ceiling')
    val_clg = schedule_ruleset.additionalProperties.getFeatureAsDouble('param_sch_ceiling').get
  end

  # loop through schedule days from highest to lowest priority (with default as lowest priority)
  # if rule needs to be split to address hours of operation rules add new rule next to relevant existing rule
  profiles = {}
  schedule_ruleset.scheduleRules.each do |rule|
    # remove any use manually generated non parametric rules or any auto-generated rules from prior application of formulas and hoo
    sch_day = rule.daySchedule
    if !sch_day.hasAdditionalProperties || !sch_day.additionalProperties.hasFeature('param_day_tag') || (sch_day.additionalProperties.getFeatureAsString('param_day_tag').get == 'autogen')
      sch_day.remove # remove day schedule for this rule
      rule.remove # remove the rule
    elsif !sch_day.additionalProperties.hasFeature('param_day_profile')
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.ScheduleRuleset', "#{schedule.name} doesn't have a parametric formula for #{rule.name} This profile will not be altered.")
      next
    else
      profiles[sch_day] = rule
    end
  end
  profiles[schedule_ruleset.defaultDaySchedule] = nil

  # get indices for current schedule
  year_description = schedule_ruleset.model.yearDescription.get
  year = year_description.assumedYear
  year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
  year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)
  indices_vector = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date)

  # process profiles
  profiles.each do |sch_day, rule|
    # for current profile index identify hours of operation index that contains all days
    if rule.nil?
      current_rule_index = -1
    else
      current_rule_index = rule.ruleIndex
    end

    # loop through indices looking of rule in hoo that contains days in the rule
    hoo_target_index = nil
    days_used = []
    indices_vector.each_with_index do |profile_index, i|
      if profile_index == current_rule_index then days_used << (i + 1) end
    end
    # find days_used in hoo profiles that contains all days used from this profile
    hoo_profile_match_hash = {}
    best_fit_check = {}
    hours_of_operation.each do |profile_index, value|
      days_for_rule_not_in_hoo_profile = days_used - value[:days_used]
      hoo_profile_match_hash[profile_index] = days_for_rule_not_in_hoo_profile
      best_fit_check[profile_index] = days_for_rule_not_in_hoo_profile.size
      if days_for_rule_not_in_hoo_profile.empty?
        hoo_target_index = profile_index
      end
    end
    clone_needed = false
    hoo_target_index = best_fit_check.key(best_fit_check.values.min)
    if best_fit_check[hoo_target_index] > 0
      clone_needed = true
    end

    # get hours of operation for this specific profile
    hoo_start = hours_of_operation[hoo_target_index][:hoo_start]
    # puts hoo_start
    hoo_end = hours_of_operation[hoo_target_index][:hoo_end]
    # puts hoo_end

    # update scheduleDay
    OpenstudioStandards::Schedules.schedule_day_adjust_from_parameters(sch_day, hoo_start, hoo_end, val_flr, val_clg, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order)

    # clone new rule if needed
    if clone_needed

      # make list of new rules needed as has or array
      autogen_rules = {}
      days_to_fill = hoo_profile_match_hash[hoo_target_index]
      hours_of_operation.each do |profile_index, value|
        remainder = days_to_fill - value[:days_used]
        day_for_rule = days_to_fill - remainder
        if remainder.size < days_to_fill.size
          autogen_rules[profile_index] = { days_to_fill: day_for_rule, hoo_start: hoo_start, hoo_end: hoo_end}
        end
        days_to_fill = remainder
      end

      # loop through new rules to make and process
      autogen_rules.each do |autogen_rule, hash|
        # generate new rule
        sch_rule_autogen = OpenStudio::Model::ScheduleRule.new(schedule_ruleset)
        if current_rule_index
          target_index = schedule_ruleset.scheduleRules.size - 1 # just above default
        else
          target_index = current_rule_index - 1 # confirm just above orig rule
        end
        current_rule_index = target_index
        if rule.nil?
          sch_rule_autogen.setName("autogen #{schedule_ruleset.name} #{target_index}")
        else
          sch_rule_autogen.setName("autogen #{rule.name} #{target_index}")
        end
        schedule_ruleset.setScheduleRuleIndex(sch_rule_autogen, target_index)
        # @todo confirm this is higher priority than the non-auto-generated rule
        hash[:days_to_fill].each do |day|
          date = OpenStudio::Date.fromDayOfYear(day, year)
          sch_rule_autogen.addSpecificDate(date)
        end
        sch_rule_autogen.setApplySunday(true)
        sch_rule_autogen.setApplyMonday(true)
        sch_rule_autogen.setApplyTuesday(true)
        sch_rule_autogen.setApplyWednesday(true)
        sch_rule_autogen.setApplyThursday(true)
        sch_rule_autogen.setApplyFriday(true)
        sch_rule_autogen.setApplySaturday(true)

        # match profile from source rule (don't add time/values need a formula to process)
        sch_day_auto_gen = sch_rule_autogen.daySchedule
        sch_day_auto_gen.setName("#{sch_rule_autogen.name}_day_sch")
        sch_day_auto_gen.additionalProperties.setFeature('param_day_tag', 'autogen')
        val = sch_day.additionalProperties.getFeatureAsString('param_day_profile').get
        sch_day_auto_gen.additionalProperties.setFeature('param_day_profile', val)
        val = sch_day.additionalProperties.getFeatureAsString('param_day_secondary_logic').get
        sch_day_auto_gen.additionalProperties.setFeature('param_day_secondary_logic', val)
        val = sch_day.additionalProperties.getFeatureAsString('param_day_secondary_logic_arg_val').get
        sch_day_auto_gen.additionalProperties.setFeature('param_day_secondary_logic_arg_val', val)

        # get hours of operation for this specific profile
        hoo_start = hash[:hoo_start]
        hoo_end = hash[:hoo_end]

        # process new rule
        OpenstudioStandards::Schedules.schedule_day_adjust_from_parameters(sch_day_auto_gen, hoo_start, hoo_end, val_flr, val_clg, ramp_frequency, infer_hoo_for_non_assigned_objects, error_on_out_of_order)
      end

    end
  end

  # @todo create summer and winter design day profiles (make sure scheduleDay objects parametric)
  # @todo should they have their own formula, or should this be hard coded logic by schedule type

  # check orig vs. updated aeflh
  final_aeflh = OpenstudioStandards::Schedules.schedule_ruleset_get_equivalent_full_load_hours(schedule_ruleset)
  percent_change = ((starting_aeflh - final_aeflh) / starting_aeflh) * 100.0
  if percent_change.abs > 0.05
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Parametric.ScheduleRuleset', "For #{schedule_ruleset.name}, applying parametric schedules made a #{percent_change.round(1)}% change in annual equivalent full load hours. (from #{starting_aeflh.round(2)} to #{final_aeflh.round(2)})")
  end

  return schedule_ruleset
end

.schedule_ruleset_cleanup_profiles(schedule_ruleset) ⇒ OpenStudio::Model::ScheduleRuleset

TODO:

There are potential issues with overlapping rule dates or days of week when setting a profile that isn’t the lowest priority as the default day.

Remove unused profiles and set most prevalent profile as default. This method expands on the functionality of the RemoveUnusedDefaultProfiles measure.

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Author:

  • David Goldwasser



601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/openstudio-standards/schedules/modify.rb', line 601

def self.schedule_ruleset_cleanup_profiles(schedule_ruleset)
  # set start and end dates
  year_description = schedule_ruleset.model.yearDescription.get
  year = year_description.assumedYear
  year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
  year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)

  indices_vector = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date)
  most_frequent_item = indices_vector.uniq.max_by { |i| indices_vector.count(i) }
  rule_vector = schedule_ruleset.scheduleRules

  replace_existing_default = false
  if indices_vector.include?(-1) && (most_frequent_item != -1)
    # clean up if default isn't most common (e.g. sunday vs. weekday)
    # if no existing rules cover specific days of week, make new rule from default covering those days of week
    possible_days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
    used_days_of_week = []
    rule_vector.each do |rule|
      if rule.applyMonday then used_days_of_week << 'Monday' end
      if rule.applyTuesday then used_days_of_week << 'Tuesday' end
      if rule.applyWednesday then used_days_of_week << 'Wednesday' end
      if rule.applyThursday then used_days_of_week << 'Thursday' end
      if rule.applyFriday then used_days_of_week << 'Friday' end
      if rule.applySaturday then used_days_of_week << 'Saturday' end
      if rule.applySunday then used_days_of_week << 'Sunday' end
    end
    if used_days_of_week.uniq.size < possible_days_of_week.size
      replace_existing_default = true
      schedule_rule_new = OpenStudio::Model::ScheduleRule.new(schedule_ruleset, schedule_ruleset.defaultDaySchedule)
      if !used_days_of_week.include?('Monday') then schedule_rule_new.setApplyMonday(true) end
      if !used_days_of_week.include?('Tuesday') then schedule_rule_new.setApplyTuesday(true) end
      if !used_days_of_week.include?('Wednesday') then schedule_rule_new.setApplyWednesday(true) end
      if !used_days_of_week.include?('Thursday') then schedule_rule_new.setApplyThursday(true) end
      if !used_days_of_week.include?('Friday') then schedule_rule_new.setApplyFriday(true) end
      if !used_days_of_week.include?('Saturday') then schedule_rule_new.setApplySaturday(true) end
      if !used_days_of_week.include?('Sunday') then schedule_rule_new.setApplySunday(true) end
    end
  end

  if !indices_vector.include?(-1) || replace_existing_default
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Schedules.Modify', "#{schedule_ruleset.name} does not use the default profile, it will be replaced.")

    # reset values in default ScheduleDay
    old_default_schedule_day = schedule_ruleset.defaultDaySchedule
    old_default_schedule_day.clearValues

    # update selection to the most commonly used profile vs. the lowest priority, if it can be done without any conflicts
    # safe test is to see if any other rules use same days of week as most common,
    # if doesn't pass then make highest rule the new default to avoid any problems. School may not pass this test, woudl use last rule
    days_of_week_most_frequent_item = []
    schedule_rule_most_frequent = rule_vector[most_frequent_item]
    if schedule_rule_most_frequent.applyMonday then days_of_week_most_frequent_item << 'Monday' end
    if schedule_rule_most_frequent.applyTuesday then days_of_week_most_frequent_item << 'Tuesday' end
    if schedule_rule_most_frequent.applyWednesday then days_of_week_most_frequent_item << 'Wednesday' end
    if schedule_rule_most_frequent.applyThursday then days_of_week_most_frequent_item << 'Thursday' end
    if schedule_rule_most_frequent.applyFriday then days_of_week_most_frequent_item << 'Friday' end
    if schedule_rule_most_frequent.applySaturday then days_of_week_most_frequent_item << 'Saturday' end
    if schedule_rule_most_frequent.applySunday then days_of_week_most_frequent_item << 'Sunday' end

    # loop through rules
    conflict_found = false
    rule_vector.each do |rule|
      next if rule == schedule_rule_most_frequent

      days_of_week_most_frequent_item.each do |day_of_week|
        if (day_of_week == 'Monday') && rule.applyMonday then conflict_found == true end
        if (day_of_week == 'Tuesday') && rule.applyTuesday then conflict_found == true end
        if (day_of_week == 'Wednesday') && rule.applyWednesday then conflict_found == true end
        if (day_of_week == 'Thursday') && rule.applyThursday then conflict_found == true end
        if (day_of_week == 'Friday') && rule.applyFriday then conflict_found == true end
        if (day_of_week == 'Saturday') && rule.applySaturday then conflict_found == true end
        if (day_of_week == 'Sunday') && rule.applySunday then conflict_found == true end
      end
    end
    if conflict_found
      new_default_index = indices_vector.max
    else
      new_default_index = most_frequent_item
    end

    # get values for new default profile
    new_default_day_schedule = rule_vector[new_default_index].daySchedule
    new_default_day_schedule_values = new_default_day_schedule.values
    new_default_day_schedule_times = new_default_day_schedule.times

    # update values and times for default profile
    for i in 0..(new_default_day_schedule_values.size - 1)
      old_default_schedule_day.addValue(new_default_day_schedule_times[i], new_default_day_schedule_values[i])
    end

    # remove rule object that has become the default. Also try to remove the ScheduleDay
    rule_vector[new_default_index].remove # this seems to also remove the ScheduleDay associated with the rule
  end

  return schedule_ruleset
end

.schedule_ruleset_conditional_adjust_value(schedule_ruleset, test_value, pass_value, fail_value, floor_value, modification_type = 'Multiplier') ⇒ OpenStudio::Model::ScheduleRuleset

TODO:

add in design day adjustments, maybe as an optional argument

TODO:

provide option to clone existing schedule

Increase/decrease by percentage or static value change value when value passes/fails test

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • test_value (Double)

    if less than the test_value, use the pass_value to modify, otherwise use the fail_value

  • pass_value (Double)

    value to adjust by if less than test value

  • fail_value (Double)

    value to adjust by if more than test value

  • floor_value (Double)

    minimum value that the adjustment can take

  • modification_type (String) (defaults to: 'Multiplier')

    Options are ‘Multiplier’, which multiples by the value, and ‘Sum’ which adds by the value

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/openstudio-standards/schedules/modify.rb', line 211

def self.schedule_ruleset_conditional_adjust_value(schedule_ruleset, test_value, pass_value, fail_value, floor_value, modification_type = 'Multiplier')
  # gather profiles
  profiles = []
  default_profile = schedule_ruleset.to_ScheduleRuleset.get.defaultDaySchedule
  profiles << default_profile
  rules = schedule_ruleset.scheduleRules
  rules.each do |rule|
    profiles << rule.daySchedule
  end

  # alter profiles
  profiles.each do |profile|
    times = profile.times
    i = 0

    profile.values.each do |sch_value|
      # run test on this sch_value
      if sch_value < test_value
        adjust_value = pass_value
      else
        adjust_value = fail_value
      end

      # skip if sch_value is floor or less
      next if sch_value <= floor_value

      case modification_type
      when 'Multiplier'
        # take the max of the floor or resulting value
        profile.addValue(times[i], [sch_value * adjust_value, floor_value].max)
      when 'Sum'
        # take the max of the floor or resulting value
        profile.addValue(times[i], [sch_value + adjust_value, floor_value].max)
      end
      i += 1
    end
  end

  return schedule_ruleset
end

.schedule_ruleset_create_rules_from_day_list(schedule_ruleset, days_used, schedule_day: nil) ⇒ Array

creates a minimal set of ScheduleRules that applies to all days in a given array of day of year indices

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)
  • days_used (Array)

    array of day of year integers

  • schedule_day (OpenStudio::Model::ScheduleDay) (defaults to: nil)

    optional day schedule to apply to new rule. A new default schedule will be created for each rule if nil

Returns:



704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
# File 'lib/openstudio-standards/schedules/modify.rb', line 704

def self.schedule_ruleset_create_rules_from_day_list(schedule_ruleset, days_used, schedule_day: nil)
  # get year from schedule_ruleset
  year = schedule_ruleset.model.getYearDescription.assumedYear

  # split day_used into sub arrays of consecutive days
  consec_days = days_used.chunk_while { |i, j| i + 1 == j }.to_a

  # split consec_days into sub arrays of consecutive weeks by checking that any value in next array differs by seven from a value in this array
  consec_weeks = consec_days.chunk_while { |i, j| i.product(j).any? { |x, y| (x - y).abs == 7 } }.to_a

  # make new rule for blocks of consectutive weeks
  rules = []
  consec_weeks.each do |week_group|
    if schedule_day.nil?
      OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleRuleset', 'Creating new Rule Schedule from days_used vector with new Day Schedule')
      rule = OpenStudio::Model::ScheduleRule.new(schedule_ruleset)
    else
      OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.ScheduleRuleset', "Creating new Rule Schedule from days_used vector with clone of Day Schedule: #{schedule_day.name.get}")
      rule = OpenStudio::Model::ScheduleRule.new(schedule_ruleset, schedule_day)
    end

    # set day types and dates
    dates = week_group.flatten.map { |d| OpenStudio::Date.fromDayOfYear(d, year) }
    day_types = dates.map { |date| date.dayOfWeek.valueName }.uniq
    day_types.each { |type| rule.send("setApply#{type}", true) }
    rule.setStartDate(dates.min)
    rule.setEndDate(dates.max)

    rules << rule
  end

  return rules
end

.schedule_ruleset_get_annual_days_used(schedule_ruleset) ⇒ Hash

Return the annual days of year that covered by each rule of a schedule ruleset

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Returns:

  • (Hash)

    hash of rule_index => [days_used]. Default day has rule_index = -1



889
890
891
892
893
894
895
896
897
898
899
900
# File 'lib/openstudio-standards/schedules/information.rb', line 889

def self.schedule_ruleset_get_annual_days_used(schedule_ruleset)
  year_description = schedule_ruleset.model.getYearDescription
  year = year_description.assumedYear
  year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
  year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)
  sch_indices_vector = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date)
  days_used_hash = Hash.new { |h, k| h[k] = [] }
  sch_indices_vector.uniq.sort.each do |rule_i|
    sch_indices_vector.each_with_index { |rule, i| days_used_hash[rule_i] << (i + 1) if rule_i == rule }
  end
  return days_used_hash
end

.schedule_ruleset_get_day_schedules(schedule_ruleset, include_design_days: false) ⇒ Array<OpenStudio::Model::ScheduleDay>

Returns the day schedules associated with a schedule ruleset Optionally includes summer and winter design days

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • include_design_days (Bool) (defaults to: false)

    include summer and winter design day profiles Defaults to false

Returns:

  • (Array<OpenStudio::Model::ScheduleDay>)

    array of day schedules



859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
# File 'lib/openstudio-standards/schedules/information.rb', line 859

def self.schedule_ruleset_get_day_schedules(schedule_ruleset, include_design_days: false)
  profiles = []
  profiles << schedule_ruleset.defaultDaySchedule
  schedule_ruleset.scheduleRules.each do |rule|
    profiles << rule.daySchedule
  end

  if include_design_days

    if schedule_ruleset.isSummerDesignDayScheduleDefaulted
      OpenStudio.logFree(OpenStudio::Warning, 'openstudio.standards.Schedules.Information', "#{__method__} called for #{schedule_ruleset.name.get} with include_design_days: true, but the summer design day is defaulted. Duplicate design day will not be added.")
    else
      profiles << rule.summerDesignDaySchedule
    end

    if schedule_ruleset.isWinterDesignDayScheduleDefaulted
      OpenStudio.logFree(OpenStudio::Warning, 'openstudio.standards.Schedules.Information', "#{__method__} called for #{schedule_ruleset.name.get} with include_design_days: true, but the winter design day is defaulted. Duplicate design day will not be added.")
    else
      profiles << rule.winterDesignDaySchedule
    end

  end

  return profiles
end

.schedule_ruleset_get_design_day_min_max(schedule_ruleset, type = 'winter') ⇒ Object

Returns the ScheduleRuleset minimum and maximum values during the winter or summer design day.

return [Hash] returns a hash with ‘min’ and ‘max’ values

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • type (String) (defaults to: 'winter')

    ‘winter’ for the winter design day, ‘summer’ for the summer design day



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/openstudio-standards/schedules/information.rb', line 490

def self.schedule_ruleset_get_design_day_min_max(schedule_ruleset, type = 'winter')
  # validate schedule
  unless schedule_ruleset.to_ScheduleRuleset.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} failed because object #{schedule_ruleset.name.get} is not a ScheduleRuleset.")
    return nil
  end

  if type == 'winter'
    schedule = schedule_ruleset.winterDesignDaySchedule
  elsif type == 'summer'
    schedule = schedule_ruleset.summerDesignDaySchedule
  end

  if !schedule
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Information', "#{schedule_ruleset.name.get} is missing #{type} design day schedule, use default day schedule to process the min max search")
    schedule = schedule_ruleset.defaultDaySchedule
  end

  min = nil
  max = nil
  values = schedule.values
  values.each do |value|
    if min.nil?
      min = value
    else
      min = value if min > value
    end
    if max.nil?
      max = value
    else
      max = value if max < value
    end
  end
  result = { 'min' => min, 'max' => max }

  return result
end

.schedule_ruleset_get_equivalent_full_load_hours(schedule_ruleset) ⇒ Object

Returns SheduleRuleset equivalent full load hours (EFLH). For example a fractional schedule of 0.5, 24/7, 365 would return a value of 4380. This method includes leap days on leap years.

return [Double] The total equivalent full load hours for this schedule

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/openstudio-standards/schedules/information.rb', line 534

def self.schedule_ruleset_get_equivalent_full_load_hours(schedule_ruleset)
  # validate schedule
  unless schedule_ruleset.to_ScheduleRuleset.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} failed because object #{schedule_ruleset.name.get} is not a ScheduleRuleset.")
    return nil
  end

  # define the start and end date
  year_start_date = nil
  year_end_date = nil
  if schedule_ruleset.model.yearDescription.is_initialized
    year_description = schedule_ruleset.model.yearDescription.get
    year = year_description.assumedYear
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Information', 'Year description is not specified. Full load hours calculation will assume 2009, the default year OS uses.')
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, 2009)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, 2009)
  end

  # Get the ordered list of all the day schedules
  day_schs = schedule_ruleset.getDaySchedules(year_start_date, year_end_date)

  # Get the array of which schedule is used on each day of the year
  day_schs_used_each_day = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date)

  # Create a map that shows how many days each schedule is used
  day_sch_freq = day_schs_used_each_day.group_by { |n| n }

  # Build a hash that maps schedule day index to schedule day
  schedule_index_to_day = {}
  day_schs.each_with_index do |day_sch, i|
    schedule_index_to_day[day_schs_used_each_day[i]] = day_sch
  end

  # Loop through each of the schedules that is used, figure out the
  # full load hours for that day, then multiply this by the number
  # of days that day schedule applies and add this to the total.
  annual_flh = 0.0
  max_daily_flh = 0.0
  default_day_sch = schedule_ruleset.defaultDaySchedule
  day_sch_freq.each do |freq|
    sch_index = freq[0]
    number_of_days_sch_used = freq[1].size

    # Get the day schedule at this index
    day_sch = nil
    if sch_index == -1 # If index = -1, this day uses the default day schedule (not a rule)
      day_sch = default_day_sch
    else
      day_sch = schedule_index_to_day[sch_index]
    end
    daily_flh = OpenstudioStandards::Schedules.schedule_day_get_equivalent_full_load_hours(day_sch)

    # Multiply the daily EFLH by the number
    # of days this schedule is used per year
    # and add this to the overall total
    annual_flh += daily_flh * number_of_days_sch_used
  end

  # Warn if the max daily EFLH is more than 24,
  # which would indicate that this isn't a fractional schedule.
  if max_daily_flh > 24
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Information', "#{schedule_ruleset.name.get} has more than 24 EFLH in one day schedule, indicating that it is not a fractional schedule.")
  end

  return annual_flh
end

.schedule_ruleset_get_hourly_values(schedule_ruleset) ⇒ Array<Double>

Returns an array of average hourly values from a ScheduleRuleset object Returns 8760 values, 8784 for leap years.

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Returns:

  • (Array<Double>)

    Array of hourly values for the year



609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/openstudio-standards/schedules/information.rb', line 609

def self.schedule_ruleset_get_hourly_values(schedule_ruleset)
  # validate schedule
  unless schedule_ruleset.to_ScheduleRuleset.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} failed because object #{schedule_ruleset.name.get} is not a ScheduleRuleset.")
    return nil
  end

  model = schedule_ruleset.model

  # define the start and end date
  year_start_date = nil
  year_end_date = nil
  if model.yearDescription.is_initialized
    year_description = model.yearDescription.get
    year = year_description.assumedYear
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Information', 'Year description is not specified. Annual hours above value calculation will assume 2009, the default year OS uses.')
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, 2009)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, 2009)
  end

  # Get the ordered list of all the day schedules
  day_schs = schedule_ruleset.getDaySchedules(year_start_date, year_end_date)

  # Loop through each day schedule and add its hours to total
  # @todo store the 24 hourly average values for each day schedule instead of recalculating for all days
  annual_hourly_values = []
  day_schs.each do |day_sch|
    # add daily average hourly values to annual hourly values array
    daily_hours = OpenstudioStandards::Schedules.schedule_day_get_hourly_values(day_sch, model)
    annual_hourly_values += daily_hours
  end

  return annual_hourly_values
end

.schedule_ruleset_get_hours_above_value(schedule_ruleset, lower_limit) ⇒ Double

Returns the total number of hours where the schedule is greater than the specified value. This method includes leap days on leap years.

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • lower_limit (Double)

    the lower limit. Values equal to the limit will not be counted.

Returns:

  • (Double)

    The total number of hours this schedule is above the specified value.



653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
# File 'lib/openstudio-standards/schedules/information.rb', line 653

def self.schedule_ruleset_get_hours_above_value(schedule_ruleset, lower_limit)
  # validate schedule
  unless schedule_ruleset.to_ScheduleRuleset.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} failed because object #{schedule_ruleset.name.get} is not a ScheduleRuleset.")
    return nil
  end

  # define the start and end date
  year_start_date = nil
  year_end_date = nil
  if schedule_ruleset.model.yearDescription.is_initialized
    year_description = schedule_ruleset.model.yearDescription.get
    year = year_description.assumedYear
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)
  else
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Information', 'Year description is not specified. Annual hours above value calculation will assume 2009, the default year OS uses.')
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, 2009)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, 2009)
  end

  # Get the ordered list of all the day schedules
  day_schs = schedule_ruleset.getDaySchedules(year_start_date, year_end_date)

  # Get the array of which schedule is used on each day of the year
  day_schs_used_each_day = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date)

  # Create a map that shows how many days each schedule is used
  day_sch_freq = day_schs_used_each_day.group_by { |n| n }

  # Build a hash that maps schedule day index to schedule day
  schedule_index_to_day = {}
  day_schs.each_with_index do |day_sch, i|
    schedule_index_to_day[day_schs_used_each_day[i]] = day_sch
  end

  # Loop through each of the schedules that is used, figure out the
  # hours for that day, then multiply this by the number
  # of days that day schedule applies and add this to the total.
  annual_hrs = 0.0
  default_day_sch = schedule_ruleset.defaultDaySchedule
  day_sch_freq.each do |freq|
    sch_index = freq[0]
    number_of_days_sch_used = freq[1].size

    # Get the day schedule at this index
    day_sch = nil
    day_sch = if sch_index == -1 # If index = -1, this day uses the default day schedule (not a rule)
                default_day_sch
              else
                schedule_index_to_day[sch_index]
              end

    # Determine the hours for just one day
    daily_hrs = 0.0
    values = day_sch.values
    times = day_sch.times

    previous_time_decimal = 0.0
    times.each_with_index do |time, i|
      time_decimal = (time.days * 24.0) + time.hours + (time.minutes / 60.0) + (time.seconds / 3600.0)
      duration_of_value = time_decimal - previous_time_decimal
      if values[i] > lower_limit
        daily_hrs += duration_of_value
      end
      previous_time_decimal = time_decimal
    end

    # Multiply the daily hours by the number
    # of days this schedule is used per year
    # and add this to the overall total
    annual_hrs += daily_hrs * number_of_days_sch_used
  end

  return annual_hrs
end

.schedule_ruleset_get_min_max(schedule_ruleset, only_run_period_values: false) ⇒ Hash

Returns the ScheduleRuleset minimum and maximum values. This method does not include summer and winter design day values. By default the method reports values from all component day schedules even if unused, but can optionally report values encountered only during the run period.

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • only_run_period_values (Bool) (defaults to: false)

    check values encountered only during the run period Default to false. This will ignore ScheduleRules or the DefaultDaySchedule if never used.

Returns:

  • (Hash)

    returns a hash with ‘min’ and ‘max’ values



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/openstudio-standards/schedules/information.rb', line 392

def self.schedule_ruleset_get_min_max(schedule_ruleset, only_run_period_values: false)
  # validate schedule
  unless schedule_ruleset.to_ScheduleRuleset.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} failed because object #{schedule_ruleset.name.get} is not a ScheduleRuleset.")
    return nil
  end

  # day schedules
  day_schedules = []

  # check only day schedules in the run period
  if only_run_period_values
    # get year
    if schedule_ruleset.model.yearDescription.is_initialized
      year_description = schedule_ruleset.model.yearDescription.get
      year = year_description.assumedYear
    else
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Schedules.Information', 'Year description is not specified. Full load hours calculation will assume 2009, the default year OS uses.')
      year = 2009
    end

    # get start and end month and day
    run_period = schedule_ruleset.model.getRunPeriod
    start_month = run_period.getBeginMonth
    start_day = run_period.getBeginDayOfMonth
    end_month = run_period.getEndMonth
    end_day = run_period.getEndDayOfMonth

    # set the start and end date
    start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(start_month), start_day, year)
    end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(end_month), end_day, year)

    # Get the ordered list of all the day schedules
    day_schs = schedule_ruleset.getDaySchedules(start_date, end_date)

    # Get the array of which schedule is used on each day of the year
    day_schs_used_each_day = schedule_ruleset.getActiveRuleIndices(start_date, end_date)

    # Create a map that shows how many days each schedule is used
    day_sch_freq = day_schs_used_each_day.group_by { |n| n }

    # Build a hash that maps schedule day index to schedule day
    schedule_index_to_day = {}
    day_schs.each_with_index do |day_sch, i|
      schedule_index_to_day[day_schs_used_each_day[i]] = day_sch
    end

    # Loop through each of the schedules and record which ones are used
    day_sch_freq.each do |freq|
      sch_index = freq[0]
      number_of_days_sch_used = freq[1].size
      next unless number_of_days_sch_used > 0

      # Get the day schedule at this index
      day_sch = nil
      if sch_index == -1 # If index = -1, this day uses the default day schedule (not a rule)
        day_sch = schedule_ruleset.defaultDaySchedule
      else
        day_sch = schedule_index_to_day[sch_index]
      end

      # add day schedule to array
      day_schedules << day_sch
    end
  else
    # use all day schedules
    day_schedules << schedule_ruleset.defaultDaySchedule
    schedule_ruleset.scheduleRules.each { |rule| day_schedules << rule.daySchedule }
  end

  # get min and max from day schedules array
  min = nil
  max = nil
  day_schedules.each do |day_schedule|
    values = day_schedule.values
    values.each do |value|
      if min.nil?
        min = value
      else
        if min > value then min = value end
      end
      if max.nil?
        max = value
      else
        if max < value then max = value end
      end
    end
  end
  result = { 'min' => min, 'max' => max }

  return result
end

.schedule_ruleset_get_parametric_inputs(schedule_ruleset, space_load_instance, parametric_inputs, hours_of_operation, ramp: true, min_ramp_dur_hr: 2.0, gather_data_only: false, hoo_var_method: 'hours') ⇒ Hash

Method to process space load instance schedules for model_setup_parametric_schedules

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • space_load_instance (OpenStudio::Model::SpaceLoadInstance)

    OpenStudio SpaceLoadInstance object

  • parametric_inputs (Hash)
  • hours_of_operation (Hash)

    hash, example: {

    profile_index: {
      hoo_start: [float] rule operation start hour,
      hoo_end: [float] rule operation end hour,
      hoo_hours: [float] rule operation duration hours,
      days_used: [Array] annual day indices
    }
    

    }

  • ramp (Boolean) (defaults to: true)

    flag to add intermediate values ramp between input schedule values

  • min_ramp_dur_hr (Double) (defaults to: 2.0)

    minimum time difference to ramp between

  • gather_data_only (Boolean) (defaults to: false)

    if true, no changes are made to schedules

  • hoo_var_method (String) (defaults to: 'hours')

    accepts hours and fractional. Any other value value will result in hoo variables not being applied

Returns:

  • (Hash)

    parametric inputs hash of ScheduleRuleset, example:

    floor: schedule floor,
    ceiling: schedule ceiling,
    target: load instance,
    hoo_inputs: hours_of_operation hash
    

Author:

  • David Goldwasser



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
# File 'lib/openstudio-standards/schedules/parametric.rb', line 491

def self.schedule_ruleset_get_parametric_inputs(schedule_ruleset, space_load_instance, parametric_inputs, hours_of_operation,
                                                ramp: true,
                                                min_ramp_dur_hr: 2.0,
                                                gather_data_only: false,
                                                hoo_var_method: 'hours')
  if parametric_inputs.key?(schedule_ruleset) && (hours_of_operation != parametric_inputs[schedule_ruleset][:hoo_inputs]) # don't warn if the hours of operation between old and new schedule are equivalent
    # OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.Schedule', "#{space_load_instance.name} uses #{schedule_ruleset.name} but parametric inputs have already been setup based on hours of operation for #{parametric_inputs[schedule_ruleset][:target].name}.")
    return nil
  end

  # gather and store data for scheduleRuleset
  min_max = OpenstudioStandards::Schedules.schedule_ruleset_get_min_max(schedule_ruleset)
  ruleset_hash = { floor: min_max['min'], ceiling: min_max['max'], target: space_load_instance, hoo_inputs: hours_of_operation }
  parametric_inputs[schedule_ruleset] = ruleset_hash

  # stop here if only gathering information otherwise will continue and generate additional parametric properties for schedules and rules
  if gather_data_only then return parametric_inputs end

  # set scheduleRuleset properties
  props = schedule_ruleset.additionalProperties

  # don't need to gather more than once
  return parametric_inputs if props.getFeatureAsString('param_sch_ver') == '0.0.1'

  props.setFeature('param_sch_ver', '0.0.1') # this is needed to see if formulas are in sync with version of standards that processes them also used to flag schedule as parametric
  props.setFeature('param_sch_floor', min_max['min'])
  props.setFeature('param_sch_ceiling', min_max['max'])

  # cleanup existing profiles
  OpenstudioStandards::Schedules.schedule_ruleset_cleanup_profiles(schedule_ruleset)

  # get initial hash of schedule days => rule index values
  schedule_days = OpenstudioStandards::Schedules.schedule_ruleset_get_schedule_day_rule_indices(schedule_ruleset)
  # get all day schedule equivalent full load hours to tag
  daily_flhs = schedule_days.keys.map { |day_sch| OpenstudioStandards::Schedules.schedule_day_get_equivalent_full_load_hours(day_sch) }
  # collect initial rule index => array of days used hash
  sch_ruleset_days_used = OpenstudioStandards::Schedules.schedule_ruleset_get_annual_days_used(schedule_ruleset)

  # match up schedule rule days with hours of operation days
  # sch_day_map is a hash where keys are the rule index values of the schedule
  # and values are hashes where keys are the hours of operation rule index, and values are arrays of days that the schedule
  sch_day_map = {}
  sch_ruleset_days_used.each do |sch_index, sch_days|
    # first create a hash that maps each day index to the hoo index that covers that day
    day_map = {}
    sch_days.each do |day|
      # find the hour of operation rule that contains the day number
      hoo_keys = hours_of_operation.find { |_, val| val[:days_used].include?(day) }
      if hoo_keys.nil?
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Parametric.Schedule', "In #{__method__}, cannot find schedule #{schedule_days.key(sch_index).name.get} day #{day} in hour of operation profiles. Something went wrong.")
      end

      hoo_key = hoo_keys.first
      day_map[day] = hoo_key
    end
    # group days with the same hour of operation index
    grouped_days = Hash.new { |h, k| h[k] = [] }
    day_map.each { |day, hoo_idx| grouped_days[hoo_idx] << day }
    # group by schedule rule index
    sch_day_map[sch_index] = grouped_days
  end

  # create new rule corresponding to the hour of operation rules
  new_rule_ct = 0
  rule_idxs_to_keep = []
  sch_day_map.each do |sch_index, hoo_group|
    hoo_group.each do |hoo_index, day_group|
      # skip common default days
      next if sch_index == -1 && hoo_index == -1

      # skip if rules already match
      if (sch_ruleset_days_used[sch_index] - day_group).empty?
        # OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.Schedules', "in #{__method__}: #{schedule_ruleset.name} rule #{sch_index} already matches hours of operation rule #{hoo_index}; new rule won't be created.")
        # keep these rules index values to avoid deleting later
        rule_idxs_to_keep << sch_index unless sch_index == -1
        next
      end
      # create new rules
      new_rules = OpenstudioStandards::Schedules.schedule_ruleset_create_rules_from_day_list(schedule_ruleset, day_group, schedule_day: schedule_days.key(sch_index))
      new_rule_ct += new_rules.size
    end
  end
  # new rules are created at top of list - cleanup old rules that have been replaced
  if !(new_rule_ct == 0 || new_rule_ct == schedule_ruleset.scheduleRules.size)
    # increase index values by the number of new rules
    rule_idxs_adjusted = rule_idxs_to_keep.map { |v| v + new_rule_ct }
    rules_to_remove = []
    schedule_ruleset.scheduleRules.each_with_index do |rule, i|
      # don't remove new rules or rules that already match
      if (rule.ruleIndex > new_rule_ct - 1) && !rule_idxs_adjusted.include?(rule.ruleIndex)
        rules_to_remove << rule
      end
    end
    rules_to_remove.each(&:remove)
  end

  # re-collect new schedule rules
  schedule_days = OpenstudioStandards::Schedules.schedule_ruleset_get_schedule_day_rule_indices(schedule_ruleset)
  # re-collect new rule index => days used array
  sch_ruleset_days_used = OpenstudioStandards::Schedules.schedule_ruleset_get_annual_days_used(schedule_ruleset)

  # step through profiles and add additional properties to describe profiles
  schedule_days.each_with_index do |(schedule_day, current_rule_index), i|
    hoo_target_index = nil

    days_used = sch_ruleset_days_used[current_rule_index]

    # find days_used in hoo profiles that contains all days used from this profile
    hoo_profile_match_hash = {}
    best_fit_check = {}

    # loop through indices looking of rule in hoo that contains all days in the rule
    hours_of_operation.each do |profile_index, value|
      if (days_used - value[:days_used]).empty?
        hoo_target_index = profile_index
      end
    end

    # if schedule day days used can't be mapped to single hours of operation then do not use hoo variables, otherwise would have to split rule and alter model
    if hoo_target_index.nil?

      hoo_start = nil
      hoo_end = nil
      occ = nil
      vac = nil
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Parametric.Schedules', "In #{__method__}, schedule #{schedule_day.name} has no hours_of_operation target index. Won't be modified")
      # OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.Schedules', "In #{__method__}, schedule #{schedule_day.name} has no hours_of_operation target index. Won't be modified")
    else
      # get hours of operation for this specific profile
      hoo_start = hours_of_operation[hoo_target_index][:hoo_start]
      hoo_end = hours_of_operation[hoo_target_index][:hoo_end]
      occ = hours_of_operation[hoo_target_index][:hoo_hours]
      vac = 24.0 - hours_of_operation[hoo_target_index][:hoo_hours]
    end

    props = schedule_day.additionalProperties
    par_val_time_hash = {} # time is key, value is value in and optional value out as a one or two object array
    times = schedule_day.times
    values = schedule_day.values
    values.each_with_index do |value, j|
      # don't add value until 24 if it is the same as first value for non constant profiles
      if values.size > 1 && j == values.size - 1 && value == values.first
        next
      end

      current_time = times[j].totalHours
      # if step height goes floor to ceiling then do not ramp.
      if !ramp || (values.uniq.size < 3)
        # this will result in steps like old profiles, update to ramp in most cases
        if j == values.size - 1
          par_val_time_hash[current_time] = [value, values.first]
        else
          par_val_time_hash[current_time] = [value, values[j + 1]]
        end
      else
        if j == 0
          prev_time = times.last.totalHours - 24 # e.g. 24 would show as until 0
        else
          prev_time = times[j - 1].totalHours
        end
        if j == values.size - 1
          next_time = times.first.totalHours + 24 # e.g. 6 would show as until 30
          next_value = values.first

          # do nothing if value is same as first value
          if value == next_value
            next
          end

        else
          next_time = times[j + 1].totalHours
          next_value = values[j + 1]
        end
        # delta time is min min_ramp_dur_hr, half of previous dur, half of next dur
        # todo - would be nice to change to 0.25 for vally less than 2 hours
        multiplier = 0.5
        delta = [min_ramp_dur_hr, (current_time - prev_time) * multiplier, (next_time - current_time) * multiplier].min
        # add value to left if not already added
        if !par_val_time_hash.key?(current_time - delta)
          time_left = current_time - delta
          if time_left < 0.0 then time_left += 24.0 end
          par_val_time_hash[time_left] = [value]
        end
        # add value to right
        time_right = current_time + delta
        if time_right > 24.0 then time_right -= 24.0 end
        par_val_time_hash[time_right] = [next_value]
      end
    end

    # sort hash by keys
    par_val_time_hash.sort.to_h

    # calculate estimated value (not including any secondary logic)
    est_daily_flh = 0.0
    prev_time = par_val_time_hash.keys.max - 24.0
    prev_value = par_val_time_hash.values.last.last # last value in last optional pair of values
    par_val_time_hash.sort.each do |time, value_array|
      segment_length = time - prev_time
      avg_value = (value_array.first + prev_value) * 0.5
      est_daily_flh += segment_length * avg_value
      prev_time = time
      prev_value = value_array.last
    end

    # test expected value against estimated value
    daily_flh = OpenstudioStandards::Schedules.schedule_day_get_equivalent_full_load_hours(schedule_day)
    percent_change = ((daily_flh - est_daily_flh) / daily_flh) * 100.0
    if percent_change.abs > 0.05
      # @todo this estimation can have flaws. Fix or remove it, make sure to update for secondary logic (if we implement that here)
      # post application checks compares against actual instead of estimated values
      OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Parametric.Schedule', "For day schedule #{schedule_day.name} in #{schedule_ruleset.name} there was a #{percent_change.round(4)}% change. Expected full load hours is #{daily_flh.round(4)}, but estimated value is #{est_daily_flh.round(4)}")
    end

    # puts "#{schedule_day.name}: par_val_time_hash: #{par_val_time_hash}"

    raw_string = []
    # flags to control variable settings for tstats
    start_set = false
    end_set = false
    par_val_time_hash.sort.each do |time, value_array|
      # add in value variables
      # not currently using range, only using min max for constant schedules or schedules with just two values
      value_array_var = []
      value_array.each do |val|
        if val == min_max['min'] && values.uniq.size < 3
          value_array_var << 'val_flr'
        elsif val == min_max['max'] && values.uniq.size < 3
          value_array_var << 'val_clg'
        else
          value_array_var << val
        end
      end

      # add in hoo variables when matching profile found
      if !hoo_start.nil?

        # identify which identifier (star,mid,end) time is closest to, which will impact formula structure
        # includes code to identify delta for wrap around of 24
        formula_identifier = {}
        start_delta_array = [hoo_start - time, hoo_start - time + 24, hoo_start - time - 24]
        start_delta_array_abs = [(hoo_start - time).abs, (hoo_start - time + 24).abs, (hoo_start - time - 24).abs]
        start_delta_h = start_delta_array[start_delta_array_abs.index(start_delta_array_abs.min)]
        formula_identifier['start'] = start_delta_h
        mid_calc = hoo_start + (occ * 0.5)
        mid_delta_array = [mid_calc - time, mid_calc - time + 24, mid_calc - time - 24]
        mid_delta_array_abs = [(mid_calc - time).abs, (mid_calc - time + 24).abs, (mid_calc - time - 24).abs]
        mid_delta_h = mid_delta_array[mid_delta_array_abs.index(mid_delta_array_abs.min)]
        formula_identifier['mid'] = mid_delta_h
        end_delta_array = [hoo_end - time, hoo_end - time + 24, hoo_end - time - 24]
        end_delta_array_abs = [(hoo_end - time).abs, (hoo_end - time + 24).abs, (hoo_end - time - 24).abs]
        end_delta_h = end_delta_array[end_delta_array_abs.index(end_delta_array_abs.min)]
        formula_identifier['end'] = end_delta_h

        # need to store min absolute value to pick the best fit
        formula_identifier_min_abs = {}
        formula_identifier.each do |k, v|
          formula_identifier_min_abs[k] = v.abs
        end
        # puts formula_identifier
        # puts formula_identifier_min_abs
        # pick from possible formula approaches for any datapoint where x is hour value
        min_key = formula_identifier_min_abs.key(formula_identifier_min_abs.values.min)
        min_value = formula_identifier[min_key]

        case hoo_var_method
        when 'hours'
          # minimize x, which should be no greater than 12, see if rounding to 2 decimal places works
          min_value = min_value.round(2)
          if min_key == 'start'
            if min_value == 0
              time = 'hoo_start'
            elsif min_value < 0
              time = "hoo_start + #{min_value.abs}"
            else # greater than 0
              time = "hoo_start - #{min_value}"
            end
            # puts time
          elsif min_key == 'mid'
            if min_value == 0
              time = 'mid'
              # converted to variable for simplicity but could also be described like this
              # time = "hoo_start + occ * 0.5"
            elsif min_value < 0
              time = "mid + #{min_value.abs}"
            else # greater than 0
              time = "mid - #{min_value}"
            end
            # puts time
          else # min_key == "end"
            if min_value == 0
              time = 'hoo_end'
            elsif min_value < 0
              time = "hoo_end + #{min_value.abs}"
            else # greater than 0
              time = "hoo_end - #{min_value}"
            end
            # puts time
          end

        when 'fractional'

          # minimize x(hour before converted to fraction), which should be no greater than 0.5 as fraction, see if rounding to 3 decimal places works
          if occ > 0
            min_value_occ_fract = min_value.abs / occ
          else
            min_value_occ_fract = 0.0
          end
          if vac > 0
            min_value_vac_fract = min_value.abs / vac
          else
            min_value_vac_fract = 0.0
          end
          if min_key == 'start'
            if min_value == 0
              time = 'hoo_start'
            elsif min_value < 0
              time = "hoo_start + occ * #{min_value_occ_fract.round(3)}"
            else # greater than 0
              time = "hoo_start - vac * #{min_value_vac_fract.round(3)}"
            end
          elsif min_key == 'mid'
            # @todo see what is going wrong with after mid in formula
            if min_value == 0
              time = 'mid'
              # converted to variable for simplicity but could also be described like this
              # time = "hoo_start + occ * 0.5"
            elsif min_value < 0
              time = "mid + occ * #{min_value_occ_fract.round(3)}"
            else # greater than 0
              time = "mid - occ * #{min_value_occ_fract.round(3)}"
            end
          else # min_key == "end"
            if min_value == 0
              time = 'hoo_end'
            elsif min_value < 0
              time = "hoo_end + vac * #{min_value_vac_fract.round(3)}"
            else # greater than 0
              time = "hoo_end - occ * #{min_value_occ_fract.round(3)}"
            end
          end

        when 'tstat'
          # puts formula_identifier
          if min_key == 'start' && !start_set
            time = 'hoo_start + 0'
            start_set = true
          else
            time = 'hoo_end + 0'
          end
        end
      end

      # populate string
      if value_array_var.size == 1
        raw_string << "#{time} ~ #{value_array_var.first}"
      else # should only have 1 or two values (value in and optional value out)
        raw_string << "#{time} ~ #{value_array_var.first} ~ #{value_array_var.last}"
      end
    end

    # puts "#{schedule_day.name}: param_day_profile: #{raw_string.join(' | ')}"

    # store profile formula with hoo and value variables
    props.setFeature('param_day_profile', raw_string.join(' | '))

    # @todo not used yet, but will add methods described below and others
    # @todo lower infiltration based on air loop hours of operation if air loop has outdoor air object
    # @todo lower lighting or plug loads based on occupancy at given time steps in a space
    # @todo set elevator fraction based multiple factors such as trips, occupants per trip, and elevator type to determine floor consumption when not in use.
    props.setFeature('param_day_secondary_logic', '') # secondary logic method such as occupancy impacting schedule values
    props.setFeature('param_day_secondary_logic_arg_val', '') # optional argument used for some secondary logic applied to values

    # tag profile type
    # may be useful for parametric changes to tag typical, medium, minimal, or same ones with off_peak prefix
    # todo - I would like to use these same tags for hours of operation and have parametric tags then ignore the days of week and date range from the rule object
    # tagging min/max makes sense in fractional schedules but not temperature schedules like thermostats (specifically cooling setpoints)
    # todo - I think these tags should come from occpancy schedule for space(s) schedule. That way all schedules in a space will refer to same profile from hours of operation
    # todo - add school specific logic hear or in post processing, currently default profile for school may not be most prevalent one
    if current_rule_index == -1
      props.setFeature('param_day_tag', 'typical_operation')
    elsif daily_flh == daily_flhs.min
      props.setFeature('param_day_tag', 'minimal_operation')
    elsif daily_flh == daily_flhs.max
      props.setFeature('param_day_tag', 'maximum_operation') # normally this should not be used as typical should be the most active day
    else
      props.setFeature('param_day_tag', 'medium_operation') # not min max or typical
    end
  end

  return parametric_inputs
end

.schedule_ruleset_get_schedule_day_rule_indices(schedule_ruleset) ⇒ Hash

Returns the rule indices associated with defaultDay and Rule days for a given ScheduleRuleset

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Returns:

  • (Hash)

    hash of ScheduleDay => rule index. Default day has rule index of -1



906
907
908
909
910
911
# File 'lib/openstudio-standards/schedules/information.rb', line 906

def self.schedule_ruleset_get_schedule_day_rule_indices(schedule_ruleset)
  schedule_day_hash = {}
  schedule_day_hash[schedule_ruleset.defaultDaySchedule] = -1
  schedule_ruleset.scheduleRules.each { |rule| schedule_day_hash[rule.daySchedule] = rule.ruleIndex }
  return schedule_day_hash
end

.schedule_ruleset_get_start_and_end_times(schedule_ruleset) ⇒ Hash<OpenStudio:Time>

Determine the hour when the schedule first exceeds the starting value and when it goes back down to the ending value at the end of the day.

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Returns:

  • (Hash<OpenStudio:Time>)

    returns as hash with ‘start_time’, ‘end time’]



765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
# File 'lib/openstudio-standards/schedules/information.rb', line 765

def self.schedule_ruleset_get_start_and_end_times(schedule_ruleset)
  # validate schedule
  unless schedule_ruleset.to_ScheduleRuleset.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{__method__} failed because object #{schedule_ruleset.name.get} is not a ScheduleRuleset.")
    return [nil, nil]
  end

  # Define the start and end date
  if schedule_ruleset.model.yearDescription.is_initialized
    year_description = schedule_ruleset.model.yearDescription.get
    year = year_description.assumedYear
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, year)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, year)
  else
    year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, 2009)
    year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, 2009)
  end

  # Get the ordered list of all the day schedules that are used by this schedule ruleset
  day_schs = schedule_ruleset.getDaySchedules(year_start_date, year_end_date)

  # Get a 365-value array of which schedule is used on each day of the year,
  day_schs_used_each_day = schedule_ruleset.getActiveRuleIndices(year_start_date, year_end_date)

  # Create a map that shows how many days each schedule is used
  day_sch_freq = day_schs_used_each_day.group_by { |n| n }
  day_sch_freq = day_sch_freq.sort_by { |freq| freq[1].size }
  common_day_freq = day_sch_freq.last

  # Build a hash that maps schedule day index to schedule day
  schedule_index_to_day = {}
  day_schs.each_with_index do |day_sch, i|
    schedule_index_to_day[day_schs_used_each_day[i]] = day_sch
  end

  # Get the most common day schedule
  sch_index = common_day_freq[0]
  number_of_days_sch_used = common_day_freq[1].size

  # Get the day schedule at this index
  day_sch = if sch_index == -1 # If index = -1, this day uses the default day schedule (not a rule)
              schedule_ruleset.defaultDaySchedule
            else
              schedule_index_to_day[sch_index]
            end

  # Determine the full load hours for just one day
  values = []
  times = []
  day_sch.times.each_with_index do |time, i|
    times << day_sch.times[i]
    values << day_sch.values[i]
  end

  # Get the minimum value
  start_val = values.first
  end_val = values.last

  # Get the start time (first time value goes above minimum)
  start_time = nil
  values.each_with_index do |val, i|
    break if i == values.size - 1 # Stop if we reach end of array

    if val == start_val && values[i + 1] > start_val
      start_time = times[i]
      break
    end
  end

  # Get the end time (first time value goes back down to minimum)
  end_time = nil
  values.each_with_index do |val, i|
    if i < values.size - 1
      if val > end_val && values[i + 1] == end_val
        end_time = times[i]
        break
      end
    else
      if val > end_val && values[0] == start_val # Check first hour of day for schedules that end at midnight
        end_time = OpenStudio::Time.new(0, 24, 0, 0)
        break
      end
    end
  end

  return { 'start_time' => start_time, 'end_time' => end_time }
end

.schedule_ruleset_get_timeseries(schedule_ruleset) ⇒ OpenStudio::TimeSeries

create OpenStudio TimeSeries object from ScheduleRuleset values

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

Returns:

  • (OpenStudio::TimeSeries)

    OpenStudio TimeSeries object of schedule values



734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
# File 'lib/openstudio-standards/schedules/information.rb', line 734

def self.schedule_ruleset_get_timeseries(schedule_ruleset)
  # validate schedule
  unless schedule_ruleset.to_ScheduleRuleset.is_initialized
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Schedules.Information', "#{method} failed because object #{schedule_ruleset.name.get} is not a ScheduleRuleset.")
    return nil
  end

  yd = schedule_ruleset.model.getYearDescription
  start_date = yd.makeDate(1, 1)
  end_date = yd.makeDate(12, 31)

  values = OpenStudio::DoubleVector.new
  day = OpenStudio::Time.new(1.0)
  interval = OpenStudio::Time.new(1.0 / 48.0)
  day_schedules = schedule_ruleset.getDaySchedules(start_date, end_date)
  day_schedules.each do |day_schedule|
    time = interval
    while time < day
      values << day_schedule.getValue(time)
      time += interval
    end
  end
  timeseries = OpenStudio::TimeSeries.new(start_date, interval, OpenStudio.createVector(values), '')
  return timeseries
end

.schedule_ruleset_set_hours_of_operation(schedule_ruleset, wkdy_start_time: nil, wkdy_end_time: nil, sat_start_time: nil, sat_end_time: nil, sun_start_time: nil, sun_end_time: nil) ⇒ Boolean

Apply specified hours of operation values to rules in this schedule. Weekday values will be applied to the default profile. Weekday values will be applied to any rules that are used on a weekday. Saturday values will be applied to any rules that are used on a Saturday. Sunday values will be applied to any rules that are used on a Sunday. If a rule applies to Weekdays, Saturdays, and/or Sundays, values will be applied in that order of precedence. If a rule does not apply to any of these days, it is unused and will not be modified.

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    schedule ruleset object

  • wkdy_start_time (OpenStudio::Time) (defaults to: nil)

    Weekday start time. If nil, no change will be made to this day.

  • wkdy_end_time (OpenStudio::Time) (defaults to: nil)

    Weekday end time. If greater than 24:00, hours of operation will wrap over midnight.

  • sat_start_time (OpenStudio::Time) (defaults to: nil)

    Saturday start time. If nil, no change will be made to this day.

  • sat_end_time (OpenStudio::Time) (defaults to: nil)

    Saturday end time. If greater than 24:00, hours of operation will wrap over midnight.

  • sun_start_time (OpenStudio::Time) (defaults to: nil)

    Sunday start time. If nil, no change will be made to this day.

  • sun_end_time (OpenStudio::Time) (defaults to: nil)

    Sunday end time. If greater than 24:00, hours of operation will wrap over midnight.

Returns:

  • (Boolean)

    returns true if successful, false if not



904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
# File 'lib/openstudio-standards/schedules/parametric.rb', line 904

def self.schedule_ruleset_set_hours_of_operation(schedule_ruleset,
                                                 wkdy_start_time: nil,
                                                 wkdy_end_time: nil,
                                                 sat_start_time: nil,
                                                 sat_end_time: nil,
                                                 sun_start_time: nil,
                                                 sun_end_time: nil)
  # Default day is assumed to represent weekdays
  if wkdy_start_time && wkdy_end_time
    schedule_day_set_hours_of_operation(schedule_ruleset.defaultDaySchedule, wkdy_start_time, wkdy_end_time)
    # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set default operating hours to #{wkdy_start_time}-#{wkdy_end_time}.")
  end

  # Modify each rule
  schedule_ruleset.scheduleRules.each do |rule|
    if rule.applyMonday || rule.applyTuesday || rule.applyWednesday || rule.applyThursday || rule.applyFriday
      if wkdy_start_time && wkdy_end_time
        schedule_day_set_hours_of_operation(rule.daySchedule, wkdy_start_time, wkdy_end_time)
        # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set Saturday rule operating hours to #{wkdy_start_time}-#{wkdy_end_time}.")
      end
    elsif rule.applySaturday
      if sat_start_time && sat_end_time
        schedule_day_set_hours_of_operation(rule.daySchedule, sat_start_time, sat_end_time)
        # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set Saturday rule operating hours to #{sat_start_time}-#{sat_end_time}.")
      end
    elsif rule.applySunday
      if sun_start_time && sun_end_time
        schedule_day_set_hours_of_operation(rule.daySchedule, sun_start_time, sun_end_time)
        # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ScheduleRuleset', "For #{schedule_ruleset.name}, set Sunday rule operating hours to #{sun_start_time}-#{sun_end_time}.")
      end
    end
  end

  return true
end

.schedule_ruleset_simple_value_adjust(schedule_ruleset, value, modification_type = 'Multiplier') ⇒ OpenStudio::Model::ScheduleRuleset

TODO:

add in design day adjustments, maybe as an optional argument

TODO:

provide option to clone existing schedule

Increase/decrease by percentage or static value. If the schedule has a scheduleTypeLimits object, the adjusted values will subject to the lower and upper bounds of the schedule type limits object.

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • value (Double)

    Hash of name and time value pairs

  • modification_type (String) (defaults to: 'Multiplier')

    Options are ‘Multiplier’, which multiples by the value, and ‘Sum’ which adds by the value

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/openstudio-standards/schedules/modify.rb', line 153

def self.schedule_ruleset_simple_value_adjust(schedule_ruleset, value, modification_type = 'Multiplier')
  # gather profiles
  profiles = []
  # positive infinity
  upper_bound = Float::INFINITY
  # negative infinity
  lower_bound = -upper_bound
  if schedule_ruleset.scheduleTypeLimits.is_initialized
    schedule_type_limits = schedule_ruleset.scheduleTypeLimits.get
    if schedule_type_limits.lowerLimitValue.is_initialized
      lower_bound = schedule_type_limits.lowerLimitValue.get
    end
    if schedule_type_limits.upperLimitValue.is_initialized
      upper_bound = schedule_type_limits.upperLimitValue.get
    end
  end
  default_profile = schedule_ruleset.to_ScheduleRuleset.get.defaultDaySchedule
  profiles << default_profile
  rules = schedule_ruleset.scheduleRules
  rules.each do |rule|
    profiles << rule.daySchedule
  end

  # alter profiles
  profiles.each do |profile|
    times = profile.times
    i = 0
    profile.values.each do |sch_value|
      case modification_type
      when 'Multiplier', 'Percentage'
        # percentage was used early on but Multiplier is preferable
        new_value = [lower_bound, [upper_bound, sch_value * value].min].max
        profile.addValue(times[i], new_value)
      when 'Sum', 'Value'
        # value was used early on but Sum is preferable
        new_value = [lower_bound, [upper_bound, sch_value + value].min].max
        profile.addValue(times[i], new_value)
      end
      i += 1
    end
  end

  return schedule_ruleset
end

.schedule_ruleset_time_conditional_adjust_value(schedule_ruleset, hhmm_before, hhmm_after, inside_value, outside_value, modification_type = 'Sum') ⇒ OpenStudio::Model::ScheduleRuleset

Increase/decrease by percentage or static value change value when time passes test

Parameters:

  • schedule_ruleset (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object

  • hhmm_before (String)

    time before string in hhmm format, e.g. 1530

  • hhmm_after (String)

    string in hhmm format, e.g. 1530

  • inside_value (Double)
  • outside_value (Double)
  • modification_type (String) (defaults to: 'Sum')

    Options are ‘Sum’, which adds to the value, and ‘Replace’ which replaces the value

Returns:

  • (OpenStudio::Model::ScheduleRuleset)

    OpenStudio ScheduleRuleset object



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/openstudio-standards/schedules/modify.rb', line 263

def self.schedule_ruleset_time_conditional_adjust_value(schedule_ruleset, hhmm_before, hhmm_after, inside_value, outside_value, modification_type = 'Sum')
  # setup variables
  array = hhmm_before.to_s.split('')
  before_hour = "#{array[0]}#{array[1]}".to_i
  before_min = "#{array[2]}#{array[3]}".to_i
  array = hhmm_after.to_s.split('')
  after_hour = "#{array[0]}#{array[1]}".to_i
  after_min = "#{array[2]}#{array[3]}".to_i

  # gather profiles
  profiles = []
  schedule = schedule_ruleset.to_ScheduleRuleset.get
  default_profile = schedule_ruleset.defaultDaySchedule
  profiles << default_profile
  rules = schedule_ruleset.scheduleRules
  rules.each do |rule|
    profiles << rule.daySchedule
  end

  # alter profiles
  profiles.each do |day_sch|
    times = day_sch.times
    i = 0

    # set times special times needed for methods below
    before_time = OpenStudio::Time.new(0, before_hour, before_min, 0)
    after_time = OpenStudio::Time.new(0, after_hour, after_min, 0)
    # day_end_time = OpenStudio::Time.new(0, 24, 0, 0)

    # add datapoint at before and after time
    original_value_at_before_time = day_sch.getValue(before_time)
    original_value_at_after_time = day_sch.getValue(after_time)
    day_sch.addValue(before_time, original_value_at_before_time)
    day_sch.addValue(after_time, original_value_at_after_time)

    # make arrays for original times and values
    times = day_sch.times
    sch_values = day_sch.values
    day_sch.clearValues

    # make arrays for new values
    new_times = []
    new_values = []

    # loop through original time/value pairs to populate new array
    for i in 0..(sch_values.length - 1)
      new_times << times[i]

      if times[i] > before_time && times[i] <= after_time
        # updated this so times[i] == before_time goes into the else
        if inside_value.nil?
          new_values << sch_values[i]
        elsif modification_type == 'Sum'
          new_values << (inside_value + sch_values[i])
        elsif modification_type == 'Replace'
          new_values << inside_value
        else # should be Multiplier
          new_values << (inside_value * sch_values[i])
        end
      else
        if outside_value.nil?
          new_values << sch_values[i]
        elsif modification_type == 'Sum'
          new_values << (outside_value + sch_values[i])
        elsif modification_type == 'Replace'
          new_values << outside_value
        else # should be Multiplier
          new_values << (outside_value * sch_values[i])
        end
      end

    end

    # generate new day_sch values
    for i in 0..(new_values.length - 1)
      day_sch.addValue(new_times[i], new_values[i])
    end
  end

  return schedule_ruleset
end

.spaces_space_types_get_parametric_schedule_inputs(spaces_space_types, parametric_inputs, gather_data_only) ⇒ Hash

Gathers parametric inputs for all loads objects associated with spaces/space types in provided array. Parametric formulas are encoded in AdditionalProperties objects attached to the ScheduleRuleset.

Parameters:

  • spaces_space_types (Array)

    array of OpenStudio::Model::Space or OpenStudio::Model::SpaceType objects

  • parametric_inputs (Hash)

    parametric inputs hash of ScheduleRuleset, example:

    floor: schedule floor,
    ceiling: schedule ceiling,
    target: load instance,
    hoo_inputs: hours_of_operation hash
    

  • gather_data_only (Boolean)

    if true, no changes will be made to schedules

Returns:

  • (Hash)

    parametric inputs hash of ScheduleRuleset, example:

    floor: schedule floor,
    ceiling: schedule ceiling,
    target: load instance,
    hoo_inputs: hours_of_operation hash
    

Author:

  • David Goldwasser



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/openstudio-standards/schedules/parametric.rb', line 410

def self.spaces_space_types_get_parametric_schedule_inputs(spaces_space_types, parametric_inputs, gather_data_only)
  spaces_space_types.each do |space_type|
    # get hours of operation for space type once
    next if space_type.instance_of?(OpenStudio::Model::SpaceType) && space_type.floorArea == 0

    hours_of_operation = Space.space_hours_of_operation(space_type)
    if hours_of_operation.nil?
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Parametric.Space', "Can't evaluate schedules for #{space_type.name}, doesn't have hours of operation.")
      next
    end
    # loop through internal load instances
    space_type.lights.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    space_type.luminaires.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    space_type.electricEquipment.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    space_type.gasEquipment.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    space_type.steamEquipment.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    space_type.otherEquipment.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    space_type.people.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
      if space_load_instance.activityLevelSchedule.is_initialized && space_load_instance.activityLevelSchedule.get.to_ScheduleRuleset.is_initialized
        act_sch = space_load_instance.activityLevelSchedule.get.to_ScheduleRuleset.get
        OpenstudioStandards::Schedules.schedule_ruleset_get_parametric_inputs(act_sch, space_load_instance, parametric_inputs, hours_of_operation, gather_data_only: gather_data_only, hoo_var_method: 'hours')
      end
    end
    space_type.spaceInfiltrationDesignFlowRates.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    space_type.spaceInfiltrationEffectiveLeakageAreas.each do |space_load_instance|
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(space_load_instance, parametric_inputs, hours_of_operation, gather_data_only)
    end
    dsgn_spec_oa = space_type.designSpecificationOutdoorAir
    if dsgn_spec_oa.is_initialized
      OpenstudioStandards::Space.space_load_instance_get_parametric_schedule_inputs(dsgn_spec_oa.get, parametric_inputs, hours_of_operation, gather_data_only)
    end
  end

  return parametric_inputs
end