-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathschedules.rb
More file actions
1521 lines (1334 loc) · 64.5 KB
/
schedules.rb
File metadata and controls
1521 lines (1334 loc) · 64.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
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
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
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
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
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
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
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
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
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
# frozen_string_literal: true
# Annual constant schedule object.
class ScheduleConstant
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param val [Double] the constant schedule value
# @param schedule_type_limits_name [String] data type for the values contained in the schedule
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
def initialize(model, sch_name, val = 1.0, schedule_type_limits_name = nil, unavailable_periods: [])
@schedule = create_schedule(model, sch_name, val, schedule_type_limits_name, unavailable_periods)
end
attr_accessor(:schedule)
private
# Create the constant OpenStudio Schedule object.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param val [Double] the constant schedule value
# @param schedule_type_limits_name [String] data type for the values contained in the schedule
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
# @return [OpenStudio::Model::ScheduleConstant or OpenStudio::Model::ScheduleRuleset] the OpenStudio Schedule object with constant schedule
def create_schedule(model, sch_name, val, schedule_type_limits_name, unavailable_periods)
if unavailable_periods.empty?
if val == 1.0 && (schedule_type_limits_name.nil? || schedule_type_limits_name == EPlus::ScheduleTypeLimitsOnOff)
schedule = model.alwaysOnDiscreteSchedule
elsif val == 0.0 && (schedule_type_limits_name.nil? || schedule_type_limits_name == EPlus::ScheduleTypeLimitsOnOff)
schedule = model.alwaysOffDiscreteSchedule
else
schedule = Model.add_schedule_constant(
model,
name: sch_name,
value: val,
limits: schedule_type_limits_name
)
end
else
schedule = Model.add_schedule_ruleset(
model,
name: sch_name,
limits: schedule_type_limits_name
)
default_day_sch = schedule.defaultDaySchedule
default_day_sch.clearValues
default_day_sch.addValue(OpenStudio::Time.new(0, 24, 0, 0), val)
Schedule.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
end
return schedule
end
end
# Annual schedule object defined by 12 24-hour values for weekdays and weekends.
class HourlyByMonthSchedule
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param weekday_month_by_hour_values [Array<Array<Double>>] a 12-element array of 24-element arrays of numbers
# @param weekday_month_by_hour_values [Array<Array<Double>>] a 12-element array of 24-element arrays of numbers
# @param schedule_type_limits_name [String] data type for the values contained in the schedule
# @param normalize_values [Boolean] whether to divide schedule values by the max value
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
def initialize(model, sch_name, weekday_month_by_hour_values, weekend_month_by_hour_values,
schedule_type_limits_name = nil, normalize_values = true, unavailable_periods: nil)
@weekday_month_by_hour_values = validate_values(weekday_month_by_hour_values, 12, 24)
@weekend_month_by_hour_values = validate_values(weekend_month_by_hour_values, 12, 24)
if normalize_values
@maxval = calc_max_val()
else
@maxval = 1.0
end
@schedule = create_schedule(model, sch_name, schedule_type_limits_name, unavailable_periods)
end
attr_accessor(:schedule, :maxval)
private
# Ensure that defined schedule value arrays are the correct lengths.
#
# @param vals [Array<Array<Double>>] a num_outer_values-element array of num_inner_values-element arrays of numbers
# @param num_outer_values [Integer] expected number of values in the outer array
# @param num_inner_values [Integer] expected number of values in the inner arrays
# @return [Array<Array<Double>>] a num_outer_values-element array of num_inner_values-element arrays of numbers
def validate_values(vals, num_outer_values, num_inner_values)
err_msg = "A #{num_outer_values}-element array with #{num_inner_values}-element arrays of numbers must be entered for the schedule."
if not vals.is_a?(Array)
fail err_msg
end
begin
if vals.length != num_outer_values
fail err_msg
end
vals.each do |val|
if not val.is_a?(Array)
fail err_msg
end
if val.length != num_inner_values
fail err_msg
end
end
rescue
fail err_msg
end
return vals
end
# Get the max weekday/weekend schedule value.
#
# @return [Double] the max hourly schedule value
def calc_max_val()
maxval = [@weekday_month_by_hour_values.flatten.max, @weekend_month_by_hour_values.flatten.max].max
if maxval == 0.0
maxval = 1.0 # Prevent divide by zero
end
return maxval
end
# Create the ruleset OpenStudio Schedule object.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param schedule_type_limits_name [String] data type for the values contained in the schedule
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
# @return [OpenStudio::Model::Ruleset] the OpenStudio Schedule object with rules
def create_schedule(model, sch_name, schedule_type_limits_name, unavailable_periods)
year = model.getYearDescription.assumedYear
day_startm = Calendar.day_start_months(year)
day_endm = Calendar.day_end_months(year)
schedule = Model.add_schedule_ruleset(
model,
name: sch_name,
limits: schedule_type_limits_name
)
prev_wkdy_vals, prev_wkdy_rule = nil, nil
prev_wknd_vals, prev_wknd_rule = nil, nil
for m in 0..11
date_s = OpenStudio::Date::fromDayOfYear(day_startm[m], year)
date_e = OpenStudio::Date::fromDayOfYear(day_endm[m], year)
wkdy_vals, wknd_vals = [], []
for h in 0..23
wkdy_vals[h] = (@weekday_month_by_hour_values[m][h]) / @maxval
wknd_vals[h] = (@weekend_month_by_hour_values[m][h]) / @maxval
end
if (wkdy_vals == prev_wkdy_vals) && (wknd_vals == prev_wknd_vals)
# Extend end date of current rule(s)
prev_wkdy_rule.setEndDate(date_e) unless prev_wkdy_rule.nil?
prev_wknd_rule.setEndDate(date_e) unless prev_wknd_rule.nil?
elsif wkdy_vals == wknd_vals
alld_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
hourly_values: wkdy_vals
)
prev_wkdy_rule, prev_wknd_rule = alld_rule, nil
else
wkdy_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
apply_to_days: [0, 1, 1, 1, 1, 1, 0],
hourly_values: wkdy_vals
)
wknd_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
apply_to_days: [1, 0, 0, 0, 0, 0, 1],
hourly_values: wknd_vals
)
prev_wkdy_rule, prev_wknd_rule = wkdy_rule, wknd_rule
end
prev_wkdy_vals, prev_wknd_vals = wkdy_vals, wknd_vals
end
Schedule.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
return schedule
end
end
# Annual schedule object defined by 365 24-hour values for weekdays and weekends.
class HourlyByDaySchedule
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param weekday_day_by_hour_values [Array<Array<Double>>] a 365-element array of 24-element arrays of numbers
# @param weekend_day_by_hour_values [Array<Array<Double>>] a 365-element array of 24-element arrays of numbers
# @param normalize_values [Boolean] whether to divide schedule values by the max value
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
def initialize(model, sch_name, weekday_day_by_hour_values, weekend_day_by_hour_values,
schedule_type_limits_name = nil, normalize_values = true, unavailable_periods: nil)
num_days = Calendar.num_days_in_year(model.getYearDescription.assumedYear)
@weekday_day_by_hour_values = validate_values(weekday_day_by_hour_values, num_days, 24)
@weekend_day_by_hour_values = validate_values(weekend_day_by_hour_values, num_days, 24)
if normalize_values
@maxval = calc_max_val()
else
@maxval = 1.0
end
@schedule = create_schedule(model, sch_name, num_days, schedule_type_limits_name, unavailable_periods)
end
attr_accessor(:schedule, :maxval)
private
# Ensure that defined schedule value arrays are the correct lengths.
#
# @param vals [Array<Array<Double>>] a num_outer_values-element array of num_inner_values-element arrays of numbers
# @param num_outer_values [Integer] expected number of values in the outer array
# @param num_inner_values [Integer] expected number of values in the inner arrays
# @return [Array<Array<Double>>] a num_outer_values-element array of num_inner_values-element arrays of numbers
def validate_values(vals, num_outer_values, num_inner_values)
err_msg = "A #{num_outer_values}-element array with #{num_inner_values}-element arrays of numbers must be entered for the schedule."
if not vals.is_a?(Array)
fail err_msg
end
begin
if vals.length != num_outer_values
fail err_msg
end
vals.each do |val|
if not val.is_a?(Array)
fail err_msg
end
if val.length != num_inner_values
fail err_msg
end
end
rescue
fail err_msg
end
return vals
end
# Get the max weekday/weekend schedule value.
#
# @return [Double] the max hourly schedule value
def calc_max_val()
maxval = [@weekday_day_by_hour_values.flatten.max, @weekend_day_by_hour_values.flatten.max].max
if maxval == 0.0
maxval = 1.0 # Prevent divide by zero
end
return maxval
end
# Create the ruleset OpenStudio Schedule object.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param num_days [Integer] the number of days in the calendar year
# @param schedule_type_limits_name [String] data type for the values contained in the schedule
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
# @return [OpenStudio::Model::Ruleset] the OpenStudio Schedule object with rules
def create_schedule(model, sch_name, num_days, schedule_type_limits_name, unavailable_periods)
schedule = Model.add_schedule_ruleset(
model,
name: sch_name,
limits: schedule_type_limits_name
)
year = model.getYearDescription.assumedYear
prev_wkdy_vals, prev_wkdy_rule = nil, nil
prev_wknd_vals, prev_wknd_rule = nil, nil
for d in 0..num_days - 1
date_s = OpenStudio::Date::fromDayOfYear(d + 1, year)
date_e = OpenStudio::Date::fromDayOfYear(d + 1, year)
wkdy_vals, wknd_vals = [], []
for h in 0..23
wkdy_vals[h] = (@weekday_day_by_hour_values[d][h]) / @maxval
wknd_vals[h] = (@weekend_day_by_hour_values[d][h]) / @maxval
end
if (wkdy_vals == prev_wkdy_vals) && (wknd_vals == prev_wknd_vals)
# Extend end date of current rule(s)
prev_wkdy_rule.setEndDate(date_e) unless prev_wkdy_rule.nil?
prev_wknd_rule.setEndDate(date_e) unless prev_wknd_rule.nil?
elsif wkdy_vals == wknd_vals
alld_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
hourly_values: wkdy_vals
)
prev_wkdy_rule, prev_wknd_rule = alld_rule, nil
else
wkdy_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
apply_to_days: [0, 1, 1, 1, 1, 1, 0],
hourly_values: wkdy_vals
)
wknd_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
apply_to_days: [1, 0, 0, 0, 0, 0, 1],
hourly_values: wknd_vals
)
prev_wkdy_rule, prev_wknd_rule = wkdy_rule, wknd_rule
end
prev_wkdy_vals, prev_wknd_vals = wkdy_vals, wknd_vals
end
Schedule.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
return schedule
end
end
# Annual schedule object defined by 24 weekday hourly values, 24 weekend hourly values, and 12 monthly values.
class MonthWeekdayWeekendSchedule
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param weekday_hourly_values [String or Array<Double>] a comma-separated string of 24 numbers or a 24-element array of numbers
# @param weekend_hourly_values [String or Array<Double>] a comma-separated string of 24 numbers or a 24-element array of numbers
# @param monthly_values [String or Array<Double>] a comma-separated string of 12 numbers or a 12-element array of numbers
# @param schedule_type_limits_name [String] data type for the values contained in the schedule
# @param normalize_values [Boolean] whether to divide schedule values by the max value
# @param begin_month [Integer] the begin month of the year
# @param begin_day [Integer] the begin day of the begin month
# @param end_month [Integer] the end month of the year
# @param end_day [Integer] the end day of the end month
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
def initialize(model, sch_name, weekday_hourly_values, weekend_hourly_values, monthly_values,
schedule_type_limits_name = nil, normalize_values = true, begin_month = 1,
begin_day = 1, end_month = 12, end_day = 31, unavailable_periods: nil)
@weekday_hourly_values = Schedule.validate_values(weekday_hourly_values, 24, 'weekday')
@weekend_hourly_values = Schedule.validate_values(weekend_hourly_values, 24, 'weekend')
@monthly_values = Schedule.validate_values(monthly_values, 12, 'monthly')
if normalize_values
@weekday_hourly_values = normalize_sum_to_one(@weekday_hourly_values)
@weekend_hourly_values = normalize_sum_to_one(@weekend_hourly_values)
@monthly_values = normalize_avg_to_one(@monthly_values)
@maxval = calc_max_val()
@schadjust = calc_sch_adjust()
else
@maxval = 1.0
@schadjust = 1.0
end
@schedule = create_schedule(model, sch_name, begin_month, begin_day, end_month, end_day,
schedule_type_limits_name, unavailable_periods)
end
attr_accessor(:schedule)
# Calculate the design level from daily kWh.
#
# @param daily_kwh [Double] daily energy use (kWh)
# @return [Double] design level used to represent maximum input (W)
def calc_design_level_from_daily_kwh(daily_kwh)
design_level_kw = daily_kwh * @maxval * @schadjust
return UnitConversions.convert(design_level_kw, 'kW', 'W')
end
# Calculate the design level from daily therm.
#
# @param daily_therm [Double] daily energy use (therm)
# @return [Double] design level used to represent maximum input (W)
def calc_design_level_from_daily_therm(daily_therm)
return calc_design_level_from_daily_kwh(UnitConversions.convert(daily_therm, 'therm', 'kWh'))
end
# Calculate the water design level from daily use.
#
# @param daily_water [Double] daily water use (gal/day)
# @return [Double] design level used to represent maximum input (m3/s)
def calc_design_level_from_daily_gpm(daily_water)
water_gpm = daily_water * @maxval * @schadjust / 60.0
return UnitConversions.convert(water_gpm, 'gal/min', 'm^3/s')
end
private
# Divide each value in the array by the sum of all values in the array.
#
# @param values [Array<Double>] an array of numbers
# @return [Array<Double>] normalized values that sum to one
def normalize_sum_to_one(values)
sum = values.sum.to_f
if sum == 0.0
return values
end
return values.map { |val| val / sum }
end
# Divide each value in the array by the average all values in the array.
#
# @param values [Array<Double>] an array of numbers
# @return [Array<Double>] normalized values that average to one
def normalize_avg_to_one(values)
avg = values.sum.to_f / values.size
if avg == 0.0
return values
end
return values.map { |val| val / avg }
end
# Get the max weekday/weekend schedule value.
#
# @return [Double] the max hourly schedule value
def calc_max_val()
if @weekday_hourly_values.max > @weekend_hourly_values.max
maxval = @monthly_values.max * @weekday_hourly_values.max
else
maxval = @monthly_values.max * @weekend_hourly_values.max
end
if maxval == 0.0
maxval = 1.0 # Prevent divide by zero
end
return maxval
end
# If sum != 1, normalize to get correct max val.
#
# @return [Double] the calculated schedule adjustment
def calc_sch_adjust()
sum_wkdy = 0
sum_wknd = 0
@weekday_hourly_values.each do |v|
sum_wkdy += v
end
@weekend_hourly_values.each do |v|
sum_wknd += v
end
if sum_wkdy < sum_wknd
return 1 / sum_wknd
end
return 1 / sum_wkdy
end
# Create the constant OpenStudio Schedule object.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param begin_month [Integer] the begin month of the year
# @param begin_day [Integer] the begin day of the begin month
# @param end_month [Integer] the end month of the year
# @param end_day [Integer] the end day of the end month
# @param schedule_type_limits_name [String] data type for the values contained in the schedule
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
# @return [OpenStudio::Model::ScheduleRuleset] the OpenStudio Schedule object with rules
def create_schedule(model, sch_name, begin_month, begin_day, end_month, end_day,
schedule_type_limits_name, unavailable_periods)
year = model.getYearDescription.assumedYear
month_num_days = Calendar.num_days_in_months(year)
month_num_days[end_month - 1] = end_day
day_startm = Calendar.day_start_months(year)
day_startm[begin_month - 1] += begin_day - 1
day_endm = [Calendar.day_start_months(year), month_num_days].transpose.map { |i| i.sum - 1 }
schedule = Model.add_schedule_ruleset(
model,
name: sch_name,
limits: schedule_type_limits_name
)
prev_wkdy_vals, prev_wkdy_rule = nil, nil
prev_wknd_vals, prev_wknd_rule = nil, nil
periods = []
if begin_month <= end_month # contiguous period
periods << [begin_month - 1, end_month - 1]
else # non-contiguous period
periods << [0, end_month - 1]
periods << [begin_month - 1, 11]
end
periods.each do |period|
for m in period[0]..period[1]
date_s = OpenStudio::Date::fromDayOfYear(day_startm[m], year)
date_e = OpenStudio::Date::fromDayOfYear(day_endm[m], year)
wkdy_vals, wknd_vals = [], []
for h in 0..23
wkdy_vals[h] = (@monthly_values[m] * @weekday_hourly_values[h]) / @maxval
wknd_vals[h] = (@monthly_values[m] * @weekend_hourly_values[h]) / @maxval
end
if (wkdy_vals == prev_wkdy_vals) && (wknd_vals == prev_wknd_vals)
# Extend end date of current rule(s)
prev_wkdy_rule.setEndDate(date_e) unless prev_wkdy_rule.nil?
prev_wknd_rule.setEndDate(date_e) unless prev_wknd_rule.nil?
elsif wkdy_vals == wknd_vals
alld_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
hourly_values: wkdy_vals
)
prev_wkdy_rule, prev_wknd_rule = alld_rule, nil
else
wkdy_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
apply_to_days: [0, 1, 1, 1, 1, 1, 0],
hourly_values: wkdy_vals
)
wknd_rule = Model.add_schedule_ruleset_rule(
schedule,
start_date: date_s,
end_date: date_e,
apply_to_days: [1, 0, 0, 0, 0, 0, 1],
hourly_values: wknd_vals
)
prev_wkdy_rule, prev_wknd_rule = wkdy_rule, wknd_rule
end
prev_wkdy_vals, prev_wknd_vals = wkdy_vals, wknd_vals
end
end
Schedule.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
return schedule
end
end
# Collection of helper methods related to schedules.
module Schedule
# Get the total number of full load hours for this schedule.
#
# @param modelYear [Integer] the calendar year
# @param schedule [OpenStudio::Model::ScheduleInterval or OpenStudio::Model::ScheduleConstant or OpenStudio::Model::ScheduleRuleset] the OpenStudio Schedule object
# @return [Double] annual equivalent full load hours
def self.annual_equivalent_full_load_hrs(modelYear, schedule)
if schedule.to_ScheduleInterval.is_initialized
timeSeries = schedule.to_ScheduleInterval.get.timeSeries
annual_flh = timeSeries.averageValue * 8760
return annual_flh
end
if schedule.to_ScheduleConstant.is_initialized
annual_flh = schedule.to_ScheduleConstant.get.value * Calendar.num_hours_in_year(modelYear)
return annual_flh
end
if not schedule.to_ScheduleRuleset.is_initialized
return
end
schedule = schedule.to_ScheduleRuleset.get
# Define the start and end date
year_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('January'), 1, modelYear)
year_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new('December'), 31, modelYear)
# Get the ordered list of all the day schedules
# that are used by this schedule ruleset
day_schs = schedule.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.getActiveRuleIndices(year_start_date, year_end_date)
if !day_schs_used_each_day.length == 365
fail "#{schedule.name} does not have 365 daily schedules accounted for, cannot accurately calculate annual EFLH."
end
# 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 = {}
for i in 0..(day_schs.length - 1)
schedule_index_to_day[day_schs_used_each_day[i]] = day_schs[i]
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
max_daily_flh = 0
default_day_sch = schedule.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
# Determine the full load hours for just one day
daily_flh = 0
values = day_sch.values
times = day_sch.times
previous_time_decimal = 0
for i in 0..(times.length - 1)
time_days = times[i].days
time_hours = times[i].hours
time_minutes = times[i].minutes
time_seconds = times[i].seconds
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
# 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
# Check if the max daily EFLH is more than 24,
# which would indicate that this isn't a
# fractional schedule.
if max_daily_flh > 24
fail "#{schedule.name} has more than 24 EFLH in one day schedule, indicating that it is not a fractional schedule."
end
return annual_flh
end
# Downselect the unavailable periods to only those that apply to the given schedule.
#
# @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings
# @param schedule_name [String] the column header of the detailed schedule
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
# @return [HPXML::UnavailablePeriods] the subset of unavailable period objects for which the ColumnName applies to the provided schedule name
def self.get_unavailable_periods(runner, schedule_name, unavailable_periods)
return unavailable_periods.select { |p| Schedule.unavailable_period_applies(runner, schedule_name, p.column_name) }
end
# Add unavailable period rules to the OpenStudio Schedule object.
# An unavailable period rule is an OpenStudio ScheduleRule object.
# Each OpenStudio ScheduleRule stores start month/day, end month/day, and day (24 hour) schedule.
# The unavailable period (i.e., number of consecutive days, whether starting/ending in the middle of the day, etc.) determines
# the number of ScheduleRule objects that are needed, as well as the start, end, and day schedule fields that are set.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param schedule [OpenStudio::Model::ScheduleRuleset] the OpenStudio Schedule object for which to set unavailable period rules
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param unavailable_periods [HPXML::UnavailablePeriods] Object that defines periods for, e.g., power outages or vacancies
# @return [nil]
def self.set_unavailable_periods(model, schedule, sch_name, unavailable_periods)
return if unavailable_periods.nil?
year = model.getYearDescription.assumedYear
# Add off rule(s), will override previous rules
unavailable_periods.each do |period|
# Special Values
# FUTURE: Assign an object type to the schedules and use that to determine what
# kind of schedule each is, rather than looking at object names. That would
# be more robust. See https://github.com/NatLabRockies/OpenStudio-HPXML/issues/1450.
if sch_name.include? Constants::ObjectTypeWaterHeaterSetpoint
# Water heater setpoint
# Temperature of tank < 2C indicates of possibility of freeze.
value = 2.0
elsif sch_name.include? Constants::ObjectTypeNaturalVentilation
if period.natvent_availability == HPXML::ScheduleRegular
next # don't change the natural ventilation availability schedule
elsif period.natvent_availability == HPXML::ScheduleAvailable
value = 1.0
elsif period.natvent_availability == HPXML::ScheduleUnavailable
value = 0.0
end
else
value = 0.0
end
day_s = Calendar.get_day_num_from_month_day(year, period.begin_month, period.begin_day)
day_e = Calendar.get_day_num_from_month_day(year, period.end_month, period.end_day)
date_s = OpenStudio::Date::fromDayOfYear(day_s, year)
date_e = OpenStudio::Date::fromDayOfYear(day_e, year)
begin_day_schedule = schedule.getDaySchedules(date_s, date_s)[0]
end_day_schedule = schedule.getDaySchedules(date_e, date_e)[0]
# [[start_date, end_date, hourly_values], ...]
schedule_ruleset_rules = []
unavail_days = day_e - day_s
if unavail_days == 0 # unavailable period is less than 1 calendar day (need 1 unavailable period rule)
schedule_ruleset_rules << [date_s, date_e, (0..23).map { |h| (h < period.begin_hour) || (h >= period.end_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }]
else # unavailable period is at least 1 calendar day
if period.begin_hour == 0 && period.end_hour == 24 # 1 unavailable period rule
schedule_ruleset_rules << [date_s, date_e, [value] * 24]
elsif (period.begin_hour == 0 && period.end_hour != 24) || (period.begin_hour != 0 && period.end_hour == 24) # 2 unavailable period rules
if period.begin_hour == 0 && period.end_hour != 24
schedule_ruleset_rules << [date_e, date_e, (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # last day
schedule_ruleset_rules << [date_s, OpenStudio::Date::fromDayOfYear(day_e - 1, year), [value] * 24] # all other days
elsif period.begin_hour != 0 && period.end_hour == 24
schedule_ruleset_rules << [date_s, date_s, (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # first day
schedule_ruleset_rules << [OpenStudio::Date::fromDayOfYear(day_s + 1, year), date_e, [value] * 24]
end
else # 2 or 3 unavailable period rules
if unavail_days == 1 # 2 unavailable period rules
schedule_ruleset_rules << [date_s, date_s, (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # first day
schedule_ruleset_rules << [date_e, date_e, (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # last day
else # 3 unavailable period rules
schedule_ruleset_rules << [date_s, date_s, (0..23).map { |h| (h < period.begin_hour) ? begin_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # first day
schedule_ruleset_rules << [OpenStudio::Date::fromDayOfYear(day_s + 1, year), OpenStudio::Date::fromDayOfYear(day_e - 1, year), [value] * 24] # all other days
schedule_ruleset_rules << [date_e, date_e, (0..23).map { |h| (h >= period.end_hour) ? end_day_schedule.getValue(OpenStudio::Time.new(0, h + 1, 0, 0)) : value }] # last day
end
end
end
schedule_ruleset_rules.each do |schedule_ruleset_rule|
start_date, end_date, hourly_values = schedule_ruleset_rule
Model.add_schedule_ruleset_rule(
schedule,
start_date: start_date,
end_date: end_date,
hourly_values: hourly_values
)
end
end
end
# Create an OpenStudio Schedule object based on a 365-element (or 366 for a leap year) daily season array.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @param values [Array<Double>] array of daily sequential load fractions
# @return [OpenStudio::Model::ScheduleRuleset] the OpenStudio Schedule object with rules
def self.create_ruleset_from_daily_season(model, sch_name, values)
schedule = Model.add_schedule_ruleset(
model,
name: sch_name,
limits: EPlus::ScheduleTypeLimitsFraction
)
year = model.getYearDescription.assumedYear
start_value = values[0]
start_date = OpenStudio::Date::fromDayOfYear(1, year)
values.each_with_index do |value, i|
i += 1
next unless value != start_value || i == values.length
i += 1 if i == values.length
Model.add_schedule_ruleset_rule(
schedule,
start_date: start_date,
end_date: OpenStudio::Date::fromDayOfYear(i - 1, year),
hourly_values: [start_value] * 24
)
break if i == values.length + 1
start_date = OpenStudio::Date::fromDayOfYear(i, year)
start_value = value
end
return schedule
end
# Return a array of maps that reflect the contents of the unavailable_periods.csv file.
#
# @return [Array<Hash>] array with maps for components that are affected by unavailable period types
def self.get_unavailable_periods_csv_data
unavailable_periods_csv = File.join(File.dirname(__FILE__), 'data', 'unavailable_periods.csv')
if not File.exist?(unavailable_periods_csv)
fail 'Could not find unavailable_periods.csv'
end
require 'csv'
unavailable_periods_csv_data = CSV.open(unavailable_periods_csv, headers: true).map(&:to_h)
return unavailable_periods_csv_data
end
# Get the unavailable period type column names from unvailable_periods.csv.
#
# @return [Array<String>] list of all defined unavailable period types in unavailable_periods.csv
def self.unavailable_period_types
if @unavailable_periods_csv_data.nil?
@unavailable_periods_csv_data = Schedule.get_unavailable_periods_csv_data
end
column_names = @unavailable_periods_csv_data[0].keys[1..-1]
return column_names
end
# Determine whether an unavailable period applies to a given detailed schedule.
#
# @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings
# @param schedule_name [String] the column header of the detailed schedule
# @param col_name [String] the unavailable period type
# @return [Boolean] true if the unavailable period type applies to the detailed schedule
def self.unavailable_period_applies(runner, schedule_name, col_name)
if @unavailable_periods_csv_data.nil?
@unavailable_periods_csv_data = get_unavailable_periods_csv_data
end
@unavailable_periods_csv_data.each do |csv_row|
next if csv_row['Schedule Name'] != schedule_name
if not csv_row.keys.include? col_name
fail "Could not find column='#{col_name}' in unavailable_periods.csv."
end
begin
applies = Integer(csv_row[col_name])
rescue
fail "Value is not a valid integer for row='#{schedule_name}' and column='#{col_name}' in unavailable_periods.csv."
end
if applies == 1
if not runner.nil?
if schedule_name == SchedulesFile::Columns[:WaterHeater].name
runner.registerWarning('It is not possible to eliminate all DHW energy use (e.g. water heater parasitics) in EnergyPlus during an unavailable period.')
end
end
return true
elsif applies == 0
return false
end
end
if not runner.nil?
runner.registerWarning("Could not find row='#{schedule_name}' in unavailable_periods.csv; it will not be affected by the '#{col_name}' unavailable period.")
end
return false
end
# Ensure that the defined schedule value array (or string of numbers) is the correct length.
#
# @param values [Array<Double> or Array<String> or String] a num_values-element array of numbers or a comma-separated string of numbers
# @param num_values [Integer] expected number of values in the outer array
# @param sch_name [String] name that is assigned to the OpenStudio Schedule object
# @return [Array<Double>] a num_values-element array of numbers
def self.validate_values(values, num_values, sch_name)
err_msg = "A comma-separated string of #{num_values} numbers must be entered for the #{sch_name} schedule."
# Check whether string is a valid float.
#
# @param str [String] string representation of a possible float
# @return [Boolean] true if valid float
def self.valid_float?(str)
!!Float(str) rescue false
end
if values.is_a?(Array)
if values.length != num_values
fail err_msg
end
values.each do |val|
if not valid_float?(val)
fail err_msg
end
end
floats = values.map { |i| i.to_f }
elsif values.is_a?(String)
begin
vals = values.split(',')
vals.each do |val|
if not valid_float?(val)
fail err_msg
end
end
floats = vals.map { |i| i.to_f }
if floats.length != num_values
fail err_msg
end
rescue
fail err_msg
end
else
fail err_msg
end
return floats
end
# Check/update emissions file references.
#
# @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file)
# @param hpxml_path [String] Path to the HPXML file
# @return [nil]
def self.check_emissions_references(hpxml_header, hpxml_path)
hpxml_header.emissions_scenarios.each do |scenario|
if hpxml_header.emissions_scenarios.count { |s| s.emissions_type == scenario.emissions_type && s.name == scenario.name } > 1
fail "Found multiple Emissions Scenarios with the Scenario Name=#{scenario.name} and Emissions Type=#{scenario.emissions_type}."
end
next if scenario.elec_schedule_filepath.nil?
scenario.elec_schedule_filepath = FilePath.check_path(scenario.elec_schedule_filepath,
File.dirname(hpxml_path),
'Emissions File')
end
end
# Check/update schedule file references.
#
# @param hpxml_bldg_header [HPXML::BuildingHeader] HPXML Building Header object
# @param hpxml_path [String] Path to the HPXML file
# @return [nil]
def self.check_schedule_references(hpxml_bldg_header, hpxml_path)
hpxml_bldg_header.schedules_filepaths = hpxml_bldg_header.schedules_filepaths.collect { |sfp|
FilePath.check_path(sfp,
File.dirname(hpxml_path),
'Schedules')
}
end
# Check that any electricity emissions schedule files contain the correct number of rows and columns.
#
# @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file)
# @return [nil]
def self.validate_emissions_files(hpxml_header)
hpxml_header.emissions_scenarios.each do |scenario|
next if scenario.elec_schedule_filepath.nil?
data = File.readlines(scenario.elec_schedule_filepath)
num_header_rows = scenario.elec_schedule_number_of_header_rows
col_index = scenario.elec_schedule_column_number - 1
if data.size != 8760 + num_header_rows
fail "Emissions File has invalid number of rows (#{data.size}). Expected 8760 plus #{num_header_rows} header row(s)."
end
if col_index > data[num_header_rows, 8760].map { |x| x.count(',') }.min
fail "Emissions File has too few columns. Cannot find column number (#{scenario.elec_schedule_column_number})."
end
end
end
# Splits a comma separated schedule string into charging (positive) and discharging (negative) schedules
#
# @param schedule_str [String] schedule with values separated by commas
# @return [Array<String, String>] 24 hourly comma-separated charging and discharging schedules
def self.split_signed_charging_schedule(schedule_str)
charge_schedule, discharge_schedule = [], []
schedule_str.split(',').map(&:strip).map(&:to_f).each do |frac|
if frac >= 0
charge_schedule << frac.to_s
discharge_schedule << 0
elsif frac < 0
charge_schedule << 0
discharge_schedule << (-frac).to_s
end
end
return charge_schedule.join(', '), discharge_schedule.join(', ')
end
end
# Object that contains information for detailed schedule CSVs.
class SchedulesFile
# Struct for storing schedule CSV column information.
class Column
# @param name [String] the column header of the detailed schedule
# @param used_by_unavailable_periods [Boolean] affected by unavailable periods
# @param can_be_stochastic [Boolean] detailed stochastic occupancy schedule can be automatically generated
# @param type [Symbol] units
def initialize(name, used_by_unavailable_periods, can_be_stochastic, type)
@name = name
@used_by_unavailable_periods = used_by_unavailable_periods
@can_be_stochastic = can_be_stochastic
@type = type
end
attr_accessor(:name, :used_by_unavailable_periods, :can_be_stochastic, :type)
end
# Define all schedule columns
# Columns may be used for A) detailed schedule CSVs (e.g., occupants), B) unavailable
# periods CSV (e.g., heating), and/or C) EnergyPlus-specific schedules (e.g., battery_charging).
Columns = {
Occupants: Column.new('occupants', true, true, :frac),
LightingInterior: Column.new('lighting_interior', true, true, :frac),
LightingExterior: Column.new('lighting_exterior', true, false, :frac),
LightingGarage: Column.new('lighting_garage', true, true, :frac),
LightingExteriorHoliday: Column.new('lighting_exterior_holiday', true, false, :frac),
CookingRange: Column.new('cooking_range', true, true, :frac),
Refrigerator: Column.new('refrigerator', true, false, :frac),
ExtraRefrigerator: Column.new('extra_refrigerator', true, false, :frac),
Freezer: Column.new('freezer', true, false, :frac),
Dishwasher: Column.new('dishwasher', true, true, :frac),
ClothesWasher: Column.new('clothes_washer', true, true, :frac),
ClothesDryer: Column.new('clothes_dryer', true, true, :frac),
CeilingFan: Column.new('ceiling_fan', true, true, :frac),
PlugLoadsOther: Column.new('plug_loads_other', true, true, :frac),
PlugLoadsTV: Column.new('plug_loads_tv', true, true, :frac),
PlugLoadsVehicle: Column.new('plug_loads_vehicle', true, false, :frac),
PlugLoadsWellPump: Column.new('plug_loads_well_pump', true, false, :frac),
FuelLoadsGrill: Column.new('fuel_loads_grill', true, false, :frac),
FuelLoadsLighting: Column.new('fuel_loads_lighting', true, false, :frac),