openstudio-geb 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -3
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +7 -17
  5. data/files/seasonal_shedding_peak_hours.json +1 -0
  6. data/files/seasonal_shifting_peak_hours.json +1 -0
  7. data/files/seasonal_shifting_take_hours.json +1 -0
  8. data/lib/measures/AddElectricVehicleChargingLoad/measure.rb +247 -21
  9. data/lib/measures/AddElectricVehicleChargingLoad/tests/CZ06RV2.epw +8768 -0
  10. data/lib/measures/AddElectricVehicleChargingLoad/tests/add_electric_vehicle_charging_load_test.rb +40 -141
  11. data/lib/measures/AddElectricVehicleChargingLoad/tests/test.osm +55 -55
  12. data/lib/measures/AdjustThermostatSetpointsByDegreesForPeakHours/measure.rb +963 -442
  13. data/lib/measures/add_ceiling_fan/measure.rb +2 -2
  14. data/lib/measures/add_chilled_water_storage_tank/measure.rb +2 -2
  15. data/lib/measures/add_exterior_blinds_and_control/measure.rb +6 -5
  16. data/lib/measures/add_interior_blinds_and_control/measure.rb +5 -5
  17. data/lib/measures/add_rooftop_pv_simple/measure.rb +13 -10
  18. data/lib/measures/apply_dynamic_coating_to_roof_wall/measure.rb +2 -2
  19. data/lib/measures/average_ventilation_for_peak_hours/measure.rb +5 -5
  20. data/lib/measures/enable_occupancy_driven_lighting/measure.rb +2 -2
  21. data/lib/measures/precooling/measure.rb +964 -354
  22. data/lib/measures/preheating/measure.rb +940 -356
  23. data/lib/measures/reduce_epd_by_percentage_for_peak_hours/measure.rb +509 -300
  24. data/lib/measures/reduce_lpd_by_percentage_for_peak_hours/measure.rb +534 -309
  25. data/lib/openstudio/geb/run.rb +5 -1
  26. data/lib/openstudio/geb/utilities.rb +3 -2
  27. data/lib/openstudio/geb/version.rb +1 -1
  28. data/openstudio-geb.gemspec +7 -6
  29. metadata +24 -48
@@ -4,6 +4,8 @@
4
4
  # *******************************************************************************
5
5
 
6
6
  # start the measure
7
+ require 'json'
8
+ require 'set'
7
9
  class Preheating < OpenStudio::Measure::ModelMeasure
8
10
  # define the name that a user will see
9
11
  def name
@@ -24,63 +26,131 @@ class Preheating < OpenStudio::Measure::ModelMeasure
24
26
  # make an argument for adjustment to cooling setpoint
25
27
  heating_adjustment = OpenStudio::Measure::OSArgument.makeDoubleArgument('heating_adjustment', true)
26
28
  heating_adjustment.setDisplayName('Degrees Fahrenheit to Adjust Heating Setpoint By')
29
+ heating_adjustment.setDescription('Use positive value for increasing heating setpoint during preheating period')
27
30
  heating_adjustment.setDefaultValue(4.0)
28
31
  args << heating_adjustment
29
-
30
- # make an argument for the start date of cooling adjustment
31
- heating_startdate1 = OpenStudio::Measure::OSArgument.makeStringArgument('heating_startdate1', false)
32
- heating_startdate1.setDisplayName('First Start Date for Pre-heating')
33
- heating_startdate1.setDescription('In MM-DD format')
34
- heating_startdate1.setDefaultValue('01-01')
35
- args << heating_startdate1
36
-
37
- # make an argument for the end date of cooling adjustment
38
- heating_enddate1 = OpenStudio::Measure::OSArgument.makeStringArgument('heating_enddate1', false)
39
- heating_enddate1.setDisplayName('First End Date for Pre-heating')
40
- heating_enddate1.setDescription('In MM-DD format')
41
- heating_enddate1.setDefaultValue('05-31')
42
- args << heating_enddate1
43
-
44
-
45
- # make an argument for the start date of cooling adjustment
46
- heating_startdate2 = OpenStudio::Measure::OSArgument.makeStringArgument('heating_startdate2', false)
47
- heating_startdate2.setDisplayName('Second Start Date for Pre-heating')
48
- heating_startdate2.setDescription('In MM-DD format')
49
- heating_startdate2.setDefaultValue('10-01')
50
- args << heating_startdate2
51
-
52
- # make an argument for the end date of cooling adjustment
53
- heating_enddate2 = OpenStudio::Measure::OSArgument.makeStringArgument('heating_enddate2', false)
54
- heating_enddate2.setDisplayName('First End Date for Pre-heating')
55
- heating_enddate2.setDescription('In MM-DD format')
56
- heating_enddate2.setDefaultValue('12-31')
57
- args << heating_enddate2
58
-
59
- starttime_heating = OpenStudio::Measure::OSArgument.makeStringArgument('starttime_heating', true)
60
- starttime_heating.setDisplayName('Start Time for Pre-heating')
61
- starttime_heating.setDescription('In HH:MM:SS format')
62
- starttime_heating.setDefaultValue('00:01:00')
63
- args << starttime_heating
64
-
65
- # make an argument for the end time of pre-cooling/heating
66
- endtime_heating = OpenStudio::Measure::OSArgument.makeStringArgument('endtime_heating', true)
67
- endtime_heating.setDisplayName('End Time for Pre-heating')
68
- endtime_heating.setDescription('In HH:MM:SS format')
69
- endtime_heating.setDefaultValue('04:59:00')
70
- args << endtime_heating
71
-
72
- # alter_design_days = OpenStudio::Measure::OSArgument.makeBoolArgument('alter_design_days', true)
73
- # alter_design_days.setDisplayName('Alter Design Day Thermostats')
74
- # alter_design_days.setDefaultValue(false)
75
- # args << alter_design_days
76
-
77
- auto_date = OpenStudio::Measure::OSArgument.makeBoolArgument('auto_date', true)
78
- auto_date.setDisplayName('Enable Climate-specific Periods Setting ?')
79
- auto_date.setDefaultValue(true)
80
- args << auto_date
81
32
 
33
+ start_date1 = OpenStudio::Ruleset::OSArgument.makeStringArgument('start_date1', true)
34
+ start_date1.setDisplayName('First start date for preheating')
35
+ start_date1.setDescription('In MM-DD format')
36
+ start_date1.setDefaultValue('06-01')
37
+ args << start_date1
38
+ end_date1 = OpenStudio::Ruleset::OSArgument.makeStringArgument('end_date1', true)
39
+ end_date1.setDisplayName('First end date for preheating')
40
+ end_date1.setDescription('In MM-DD format')
41
+ end_date1.setDefaultValue('09-30')
42
+ args << end_date1
43
+
44
+ start_date2 = OpenStudio::Ruleset::OSArgument.makeStringArgument('start_date2', false)
45
+ start_date2.setDisplayName('Second start date for preheating (optional)')
46
+ start_date2.setDescription('Specify a date in MM-DD format if you want a second season of preheating; leave blank if not needed.')
47
+ start_date2.setDefaultValue('')
48
+ args << start_date2
49
+ end_date2 = OpenStudio::Ruleset::OSArgument.makeStringArgument('end_date2', false)
50
+ end_date2.setDisplayName('Second end date for preheating')
51
+ end_date2.setDescription('Specify a date in MM-DD format if you want a second season of preheating; leave blank if not needed. If either the start or end date is blank, the period is considered not used.')
52
+ end_date2.setDefaultValue('')
53
+ args << end_date2
54
+
55
+
56
+ start_date3 = OpenStudio::Ruleset::OSArgument.makeStringArgument('start_date3', false)
57
+ start_date3.setDisplayName('Third start date for preheating (optional)')
58
+ start_date3.setDescription('Specify a date in MM-DD format if you want a third season of preheating; leave blank if not needed.')
59
+ start_date3.setDefaultValue('')
60
+ args << start_date3
61
+ end_date3 = OpenStudio::Ruleset::OSArgument.makeStringArgument('end_date3', false)
62
+ end_date3.setDisplayName('Third end date for preheating')
63
+ end_date3.setDescription('Specify a date in MM-DD format if you want a third season of preheating; leave blank if not needed. If either the start or end date is blank, the period is considered not used.')
64
+ end_date3.setDefaultValue('')
65
+ args << end_date3
66
+
67
+ start_date4 = OpenStudio::Ruleset::OSArgument.makeStringArgument('start_date4', false)
68
+ start_date4.setDisplayName('Fourth start date for preheating (optional)')
69
+ start_date4.setDescription('Specify a date in MM-DD format if you want a fourth season of preheating; leave blank if not needed.')
70
+ start_date4.setDefaultValue('')
71
+ args << start_date4
72
+ end_date4 = OpenStudio::Ruleset::OSArgument.makeStringArgument('end_date4', false)
73
+ end_date4.setDisplayName('Fourth end date for preheating')
74
+ end_date4.setDescription('Specify a date in MM-DD format if you want a fourth season of preheating; leave blank if not needed. If either the start or end date is blank, the period is considered not used.')
75
+ end_date4.setDefaultValue('')
76
+ args << end_date4
77
+
78
+
79
+ start_date5 = OpenStudio::Ruleset::OSArgument.makeStringArgument('start_date5', false)
80
+ start_date5.setDisplayName('Fifth start date for preheating (optional)')
81
+ start_date5.setDescription('Specify a date in MM-DD format if you want a fifth season of preheating; leave blank if not needed.')
82
+ start_date5.setDefaultValue('')
83
+ args << start_date5
84
+ end_date5 = OpenStudio::Ruleset::OSArgument.makeStringArgument('end_date5', false)
85
+ end_date5.setDisplayName('Fifth end date for preheating')
86
+ end_date5.setDescription('Specify a date in MM-DD format if you want a fifth season of preheating; leave blank if not needed. If either the start or end date is blank, the period is considered not used.')
87
+ end_date5.setDefaultValue('')
88
+ args << end_date5
89
+
90
+ start_time1 = OpenStudio::Measure::OSArgument.makeStringArgument('start_time1', true)
91
+ start_time1.setDisplayName('Start time of preheating for the first season')
92
+ start_time1.setDescription('In HH:MM:SS format')
93
+ start_time1.setDefaultValue('17:00:00')
94
+ args << start_time1
95
+ end_time1 = OpenStudio::Measure::OSArgument.makeStringArgument('end_time1', true)
96
+ end_time1.setDisplayName('End time of preheating for the first season')
97
+ end_time1.setDescription('In HH:MM:SS format')
98
+ end_time1.setDefaultValue('21:00:00')
99
+ args << end_time1
100
+
101
+
102
+ start_time2 = OpenStudio::Measure::OSArgument.makeStringArgument('start_time2', false)
103
+ start_time2.setDisplayName('Start time of preheating for the second season (optional)')
104
+ start_time2.setDescription('In HH:MM:SS format')
105
+ start_time2.setDefaultValue('')
106
+ args << start_time2
107
+ end_time2 = OpenStudio::Measure::OSArgument.makeStringArgument('end_time2', false)
108
+ end_time2.setDisplayName('End time of preheating for the second season (optional)')
109
+ end_time2.setDescription('In HH:MM:SS format')
110
+ end_time2.setDefaultValue('')
111
+ args << end_time2
112
+
113
+
114
+ start_time3 = OpenStudio::Measure::OSArgument.makeStringArgument('start_time3', false)
115
+ start_time3.setDisplayName('Start time of preheating for the third season (optional)')
116
+ start_time3.setDescription('In HH:MM:SS format')
117
+ start_time3.setDefaultValue('')
118
+ args << start_time3
119
+ end_time3 = OpenStudio::Measure::OSArgument.makeStringArgument('end_time3', false)
120
+ end_time3.setDisplayName('End time of preheating for the third season (optional)')
121
+ end_time3.setDescription('In HH:MM:SS format')
122
+ end_time3.setDefaultValue('')
123
+ args << end_time3
124
+
125
+
126
+ start_time4 = OpenStudio::Measure::OSArgument.makeStringArgument('start_time4', false)
127
+ start_time4.setDisplayName('Start time of preheating for the fourth season (optional)')
128
+ start_time4.setDescription('In HH:MM:SS format')
129
+ start_time4.setDefaultValue('')
130
+ args << start_time4
131
+ end_time4 = OpenStudio::Measure::OSArgument.makeStringArgument('end_time4', false)
132
+ end_time4.setDisplayName('End time of preheating for the fourth season (optional)')
133
+ end_time4.setDescription('In HH:MM:SS format')
134
+ end_time4.setDefaultValue('')
135
+ args << end_time4
136
+
137
+
138
+ start_time5 = OpenStudio::Measure::OSArgument.makeStringArgument('start_time5', false)
139
+ start_time5.setDisplayName('Start time of preheating for the fifth season (optional)')
140
+ start_time5.setDescription('In HH:MM:SS format')
141
+ start_time5.setDefaultValue('')
142
+ args << start_time5
143
+ end_time5 = OpenStudio::Measure::OSArgument.makeStringArgument('end_time5', false)
144
+ end_time5.setDisplayName('End time of preheating for the fifth season (optional)')
145
+ end_time5.setDescription('In HH:MM:SS format')
146
+ end_time5.setDefaultValue('')
147
+ args << end_time5
148
+
149
+
150
+ # Use alternative default start and end time for different climate zone
82
151
  alt_periods = OpenStudio::Measure::OSArgument.makeBoolArgument('alt_periods', true)
83
- alt_periods.setDisplayName('Alternate Peak and Take Periods ?')
152
+ alt_periods.setDisplayName('Use alternative default start and end time based on the state of the model from the Cambium load profile peak period?')
153
+ alt_periods.setDescription('This will overwrite the start and end time and date provided by the user')
84
154
  alt_periods.setDefaultValue(false)
85
155
  args << alt_periods
86
156
 
@@ -98,152 +168,180 @@ class Preheating < OpenStudio::Measure::ModelMeasure
98
168
 
99
169
  # assign the user inputs to variables
100
170
  heating_adjustment = runner.getDoubleArgumentValue('heating_adjustment', user_arguments)
101
- # alter_design_days = runner.getBoolArgumentValue('alter_design_days', user_arguments)
102
- starttime_heating = runner.getStringArgumentValue('starttime_heating', user_arguments)
103
- endtime_heating = runner.getStringArgumentValue('endtime_heating', user_arguments)
104
-
105
- heating_startdate1 = runner.getStringArgumentValue('heating_startdate1', user_arguments)
106
- heating_enddate1 = runner.getStringArgumentValue('heating_enddate1', user_arguments)
107
- heating_startdate2 = runner.getStringArgumentValue('heating_startdate2', user_arguments)
108
- heating_enddate2 = runner.getStringArgumentValue('heating_enddate2', user_arguments)
109
- auto_date = runner.getBoolArgumentValue('auto_date', user_arguments)
171
+ start_time1 = runner.getStringArgumentValue('start_time1', user_arguments)
172
+ end_time1 = runner.getStringArgumentValue('end_time1', user_arguments)
173
+ start_time2 = runner.getStringArgumentValue('start_time2', user_arguments)
174
+ end_time2 = runner.getStringArgumentValue('end_time2', user_arguments)
175
+ start_time3 = runner.getStringArgumentValue('start_time3', user_arguments)
176
+ end_time3 = runner.getStringArgumentValue('end_time3', user_arguments)
177
+ start_time4 = runner.getStringArgumentValue('start_time4', user_arguments)
178
+ end_time4 = runner.getStringArgumentValue('end_time4', user_arguments)
179
+ start_time5 = runner.getStringArgumentValue('start_time5', user_arguments)
180
+ end_time5 = runner.getStringArgumentValue('end_time5', user_arguments)
181
+ start_date1 = runner.getStringArgumentValue('start_date1', user_arguments)
182
+ end_date1 = runner.getStringArgumentValue('end_date1', user_arguments)
183
+ start_date2 = runner.getStringArgumentValue('start_date2', user_arguments)
184
+ end_date2 = runner.getStringArgumentValue('end_date2', user_arguments)
185
+ start_date3 = runner.getStringArgumentValue('start_date3', user_arguments)
186
+ end_date3 = runner.getStringArgumentValue('end_date3', user_arguments)
187
+ start_date4 = runner.getStringArgumentValue('start_date4', user_arguments)
188
+ end_date4 = runner.getStringArgumentValue('end_date4', user_arguments)
189
+ start_date5 = runner.getStringArgumentValue('start_date5', user_arguments)
190
+ end_date5 = runner.getStringArgumentValue('end_date5', user_arguments)
110
191
  alt_periods = runner.getBoolArgumentValue('alt_periods', user_arguments)
111
192
 
112
-
113
- heating_start_month1 = nil
114
- heating_start_day1 = nil
115
- md = /(\d\d)-(\d\d)/.match(heating_startdate1)
116
- if md
117
- heating_start_month1 = md[1].to_i
118
- heating_start_day1 = md[2].to_i
119
- else
120
- runner.registerError('Start date must be in MM-DD format.')
121
- return false
122
- end
123
-
124
- heating_end_month1 = nil
125
- heating_end_day1 = nil
126
- md = /(\d\d)-(\d\d)/.match(heating_enddate1)
127
- if md
128
- heating_end_month1 = md[1].to_i
129
- heating_end_day1 = md[2].to_i
130
- else
131
- runner.registerError('End date must be in MM-DD format.')
132
- return false
133
- end
134
-
135
- heating_start_month2 = nil
136
- heating_start_day2 = nil
137
- md = /(\d\d)-(\d\d)/.match(heating_startdate2)
138
- if md
139
- heating_start_month2 = md[1].to_i
140
- heating_start_day2 = md[2].to_i
141
- else
142
- runner.registerError('Start date must be in MM-DD format.')
143
- return false
144
- end
145
-
146
- heating_end_month2 = nil
147
- heating_end_day2 = nil
148
- md = /(\d\d)-(\d\d)/.match(heating_enddate2)
149
- if md
150
- heating_end_month2 = md[1].to_i
151
- heating_end_day2 = md[2].to_i
152
- else
153
- runner.registerError('End date must be in MM-DD format.')
154
- return false
155
- end
156
-
157
-
158
- winterStartDate1 = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(heating_start_month1), heating_start_day1)
159
- winterEndDate1 = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(heating_end_month1), heating_end_day1)
160
- winterStartDate2 = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(heating_start_month2), heating_start_day2)
161
- winterEndDate2 = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(heating_end_month2), heating_end_day2)
162
- summerStartDate = winterEndDate1 + OpenStudio::Time.new(1)
163
- summerEndDate = winterStartDate2 - OpenStudio::Time.new(1)
164
-
165
-
166
- ######### GET CLIMATE ZONES ################
167
- if auto_date
168
- ashraeClimateZone = nil
169
- #climateZoneNUmber = ''
170
- climateZones = model.getClimateZones
171
- climateZones.climateZones.each do |climateZone|
172
- if climateZone.institution == 'ASHRAE'
173
- ashraeClimateZone = climateZone.value
174
- runner.registerInfo("Using ASHRAE Climate zone #{ashraeClimateZone}.")
175
- end
193
+ if alt_periods
194
+ state = model.getWeatherFile.stateProvinceRegion
195
+ if state == ''
196
+ runner.registerError('Unable to find state in model WeatherFile. The measure cannot be applied.')
197
+ return false
176
198
  end
177
-
178
- unless ashraeClimateZone # should this be not applicable or error?
179
- runner.registerError("If you select to use alternative default start and end time based on the climate zone, please assign an ASHRAE climate zone to your model.")
180
- return false # note - for this to work need to check for false in measure.rb and add return false there as well.
199
+ file = File.open(File.join(File.dirname(__FILE__), "../../../files/seasonal_shifting_take_hours.json"))
200
+ default_peak_periods = JSON.load(file)
201
+ file.close
202
+ unless default_peak_periods.key?state
203
+ runner.registerAsNotApplicable("No default inputs for the state of the WeatherFile #{state}")
204
+ return false
181
205
  end
206
+ peak_periods = default_peak_periods[state]
207
+ start_time1 = peak_periods["winter_take_start"].split[1]
208
+ end_time1 = peak_periods["winter_take_end"].split[1]
209
+ start_time2 = peak_periods["intermediate_take_start"].split[1]
210
+ end_time2 = peak_periods["intermediate_take_end"].split[1]
211
+ start_time3 = peak_periods["summer_take_start"].split[1]
212
+ end_time3 = peak_periods["summer_take_end"].split[1]
213
+ start_time4 = peak_periods["intermediate_take_start"].split[1]
214
+ end_time4 = peak_periods["intermediate_take_end"].split[1]
215
+ start_time5 = peak_periods["winter_take_start"].split[1]
216
+ end_time5 = peak_periods["winter_take_end"].split[1]
217
+ start_date1 = '01-01'
218
+ end_date1 = '03-31'
219
+ start_date2 = '04-01'
220
+ end_date2 = '05-31'
221
+ start_date3 = '06-01'
222
+ end_date3 = '09-30'
223
+ start_date4 = '10-01'
224
+ end_date4 = '11-30'
225
+ start_date5 = '12-01'
226
+ end_date5 = '12-31'
227
+ end
182
228
 
183
- if alt_periods
184
- case ashraeClimateZone
185
- when '3A','4A'
186
- starttime_heating = '14:01:00'
187
- endtime_heating = '17:59:00'
188
- when '5A'
189
- starttime_heating = '10:01:00'
190
- endtime_heating = '13:59:00'
191
- when '6A'
192
- starttime_heating = '9:01:00'
193
- endtime_heating = '12:59:00'
229
+ def validate_time_format(star_time, end_time, runner)
230
+ time1 = /(\d\d):(\d\d):(\d\d)/.match(star_time)
231
+ time2 = /(\d\d):(\d\d):(\d\d)/.match(end_time)
232
+ if time1 and time2
233
+ os_starttime = OpenStudio::Time.new(star_time)
234
+ os_endtime = OpenStudio::Time.new(end_time)
235
+ if star_time >= end_time
236
+ runner.registerError('The start time needs to be earlier than the end time.')
237
+ return false
238
+ else
239
+ return os_starttime, os_endtime
194
240
  end
241
+ else
242
+ runner.registerError('The provided time is not in HH-MM-SS format.')
243
+ return false
244
+ end
245
+ end
195
246
 
247
+ def validate_date_format(start_date1, end_date1, runner, model)
248
+ smd = /(\d\d)-(\d\d)/.match(start_date1)
249
+ emd = /(\d\d)-(\d\d)/.match(end_date1)
250
+ if smd.nil? or emd.nil?
251
+ runner.registerError('The provided date is not in MM-DD format.')
252
+ return false
196
253
  else
197
- case ashraeClimateZone
198
- when '2A', '2B', '4B', '4C', '5B', '5C', '6B'
199
- starttime_heating = '13:01:00'
200
- endtime_heating = '16:59:00'
201
- when '3A', '3C'
202
- starttime_heating = '15:01:00'
203
- endtime_heating = '18:59:00'
204
- when '3B'
205
- starttime_heating = '14:01:00'
206
- endtime_heating = '17:59:00'
207
- when '4A'
208
- starttime_heating = '8:01:00'
209
- endtime_heating = '11:59:00'
210
- when '5A'
211
- starttime_heating = '16:01:00'
212
- endtime_heating = '19:59:00'
213
- when '6A', '7A'
214
- starttime_heating = '12:01:00'
215
- endtime_heating = '15:59:00'
254
+ start_month = smd[1].to_i
255
+ start_day = smd[2].to_i
256
+ end_month = emd[1].to_i
257
+ end_day = emd[2].to_i
258
+ if start_date1 > end_date1
259
+ runner.registerError('The start date cannot be later date the end time.')
260
+ return false
261
+ else
262
+ # os_start_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(start_month), start_day)
263
+ # os_end_date = OpenStudio::Date.new(OpenStudio::MonthOfYear.new(end_month), end_day)
264
+ os_start_date = model.getYearDescription.makeDate(start_month, start_day)
265
+ os_end_date = model.getYearDescription.makeDate(end_month, end_day)
266
+ return os_start_date, os_end_date
216
267
  end
217
-
218
268
  end
219
269
  end
220
270
 
221
- # shift_time1 = OpenStudio::Time.new(starttime_heating)
222
- # shift_time2 = OpenStudio::Time.new(endtime_heating)
223
- # shift_time3 = OpenStudio::Time.new(0,6,0,0)
224
- # shift_time4 = OpenStudio::Time.new(starttime_heating)
225
- # shift_time5 = OpenStudio::Time.new(endtime_heating)
226
-
227
- if /(\d\d):(\d\d):(\d\d)/.match(starttime_heating)
228
- shift_time1 = OpenStudio::Time.new(starttime_heating)
271
+ # First time period
272
+ time_result1 = validate_time_format(start_time1, end_time1, runner)
273
+ if time_result1
274
+ shift_time_start1, shift_time_end1 = time_result1
229
275
  else
230
- runner.registerError('Start time must be in HH-MM-SS format.')
276
+ runner.registerError('The required time period for the reduction is not in correct format!')
231
277
  return false
232
278
  end
279
+ # The other optional time periods
280
+ shift_time_start2,shift_time_end2,shift_time_start3,shift_time_end3,shift_time_start4,shift_time_end4,shift_time_start5,shift_time_end5 = [nil]*8
281
+ if (not start_time2.empty?) and (not end_time2.empty?)
282
+ time_result2 = validate_time_format(start_time2, end_time2, runner)
283
+ if time_result2
284
+ shift_time_start2, shift_time_end2 = time_result2
285
+ end
286
+ end
287
+ if (not start_time3.empty?) and (not end_time3.empty?)
288
+ time_result3 = validate_time_format(start_time3, end_time3, runner)
289
+ if time_result3
290
+ shift_time_start3, shift_time_end3 = time_result3
291
+ end
292
+ end
293
+ if (not start_time4.empty?) and (not end_time4.empty?)
294
+ time_result4 = validate_time_format(start_time4, end_time4, runner)
295
+ if time_result4
296
+ shift_time_start4, shift_time_end4 = time_result4
297
+ end
298
+ end
299
+ if (not start_time5.empty?) and (not end_time5.empty?)
300
+ time_result5 = validate_time_format(start_time5, end_time5, runner)
301
+ if time_result5
302
+ shift_time_start5, shift_time_end5 = time_result5
303
+ end
304
+ end
233
305
 
234
- if /(\d\d):(\d\d):(\d\d)/.match(endtime_heating)
235
- shift_time2 = OpenStudio::Time.new(endtime_heating)
306
+ # First date period
307
+ date_result1 = validate_date_format(start_date1, end_date1, runner, model)
308
+ if date_result1
309
+ os_start_date1, os_end_date1 = date_result1
236
310
  else
237
- runner.registerError('End time must be in HH-MM-SS format.')
311
+ runner.registerError('The required date period for the reduction is not in correct format!')
238
312
  return false
239
313
  end
314
+ # Other optional date period
315
+ os_start_date2, os_end_date2, os_start_date3, os_end_date3, os_start_date4, os_end_date4, os_start_date5, os_end_date5 = [nil]*8
316
+ if (not start_date2.empty?) and (not end_date2.empty?)
317
+ date_result2 = validate_date_format(start_date2, end_date2, runner, model)
318
+ if date_result2
319
+ os_start_date2, os_end_date2 = date_result2
320
+ end
321
+ end
240
322
 
241
- if starttime_heating.to_f > endtime_heating.to_f
242
- runner.registerError('The end time should be larger than the start time.')
243
- return false
323
+ if (not start_date3.empty?) and (not end_date3.empty?)
324
+ date_result3 = validate_date_format(start_date3, end_date3, runner, model)
325
+ if date_result3
326
+ os_start_date3, os_end_date3 = date_result3
327
+ end
328
+ end
329
+
330
+ if (not start_date4.empty?) and (not end_date4.empty?)
331
+ date_result4 = validate_date_format(start_date4, end_date4, runner, model)
332
+ if date_result4
333
+ os_start_date4, os_end_date4 = date_result4
334
+ end
335
+ end
336
+
337
+ if (not start_date5.empty?) and (not end_date5.empty?)
338
+ date_result5 = validate_date_format(start_date5, end_date5, runner, model)
339
+ if date_result5
340
+ os_start_date5, os_end_date5 = date_result5
341
+ end
244
342
  end
245
343
 
246
- # ruby test to see if first charter of string is uppercase letter
344
+
247
345
  if heating_adjustment < 0
248
346
  runner.registerWarning('Lowering heating setpoint will not do pre-heating.')
249
347
  elsif heating_adjustment.abs > 500
@@ -254,171 +352,471 @@ class Preheating < OpenStudio::Measure::ModelMeasure
254
352
  end
255
353
 
256
354
  # setup OpenStudio units that we will need
257
- temperature_ip_unit = OpenStudio.createUnit('F').get
258
- temperature_si_unit = OpenStudio.createUnit('C').get
355
+ # temperature_ip_unit = OpenStudio.createUnit('F').get
356
+ # temperature_si_unit = OpenStudio.createUnit('C').get
259
357
  # define starting units
260
358
  # heating_adjustment_ip = OpenStudio::Quantity.new(heating_adjustment, temperature_ip_unit)
261
359
  heating_adjustment_si = heating_adjustment*5/9
262
360
 
263
361
  # update the availability schedule
264
- air_loop_avail_schs = {}
265
- air_loops = model.getAirLoopHVACs
266
- air_loops.each do |air_loop|
267
- avail_sch = air_loop.availabilitySchedule
268
-
269
- if air_loop_avail_schs.key?(avail_sch.name.to_s)
270
- new_avail_sch = air_loop_avail_schs[avail_sch.name.to_s]
271
- else
272
- new_avail_sch = avail_sch.clone(model).to_Schedule.get
273
- new_avail_sch.setName("#{avail_sch.name.to_s} adjusted")
274
- # add to the hash
275
- air_loop_avail_schs[avail_sch.name.to_s] = new_avail_sch
276
- end
277
- air_loop.setAvailabilitySchedule(new_avail_sch)
278
-
279
- end
280
-
281
- air_loop_avail_schs.each do |sch_name, air_loop_sch|
282
- runner.registerInfo("Air Loop Schedule #{sch_name}:")
283
- if air_loop_sch.to_ScheduleRuleset.empty?
284
- runner.registerWarning("Schedule #{sch_name} isn't a ScheduleRuleset object and won't be altered by this measure.")
285
- air_loop_sch.remove # remove un-used clone
286
- else
287
- schedule = air_loop_sch.to_ScheduleRuleset.get
288
- default_rule = schedule.defaultDaySchedule
289
- rules = schedule.scheduleRules
290
- days_covered = Array.new(7, false)
291
-
292
- rules.each do |rule|
293
- winter_avail_rule1 = copy_sch_rule_for_period(model, rule, rule.daySchedule, winterStartDate1, winterEndDate1)
294
- winter_avail_rule2 = copy_sch_rule_for_period(model, rule, rule.daySchedule, winterStartDate2, winterEndDate2)
295
- runner.registerInfo(" ------------ time: #{rule.daySchedule.times.map {|os_time| os_time.toString}}")
296
- runner.registerInfo(" ------------ values: #{rule.daySchedule.values}")
297
- summer_avail_rule = rule.clone(model).to_ScheduleRule.get
298
- summer_avail_rule.setStartDate(summerStartDate)
299
- summer_avail_rule.setEndDate(summerEndDate)
300
-
301
- checkDaysCovered(summer_avail_rule, days_covered)
302
-
303
- summer_avail_day = summer_avail_rule.daySchedule
304
- day_time_vector = summer_avail_day.times
305
- day_value_vector = summer_avail_day.values
306
- summer_avail_day.clearValues
307
-
308
- summer_avail_day = updateAvailDaySchedule(summer_avail_day, day_time_vector, day_value_vector, shift_time1, shift_time2)
309
- runner.registerInfo(" ------------ updated time: #{summer_avail_day.times.map {|os_time| os_time.toString}}")
310
- runner.registerInfo(" ------------ uodated values: #{summer_avail_day.values}")
311
-
312
- end
313
-
314
- if days_covered.include?(false)
315
-
316
- winter_rule1 = create_sch_rule_from_default(model, schedule, default_rule, winterStartDate1, winterEndDate1)
317
- winter_rule2 = create_sch_rule_from_default(model, schedule, default_rule, winterStartDate2, winterEndDate2)
318
-
319
- coverMissingDays(winter_rule1, days_covered)
320
- checkDaysCovered(winter_rule1, days_covered)
321
-
322
- summer_rule = copy_sch_rule_for_period(model, winter_rule1, default_rule, summerStartDate, summerEndDate)
323
-
324
- summer_day = summer_rule.daySchedule
325
- day_time_vector = summer_day.times
326
- day_value_vector = summer_day.values
327
- summer_day.clearValues
328
-
329
- summer_day = updateAvailDaySchedule(summer_day, day_time_vector, day_value_vector, shift_time1, shift_time2)
330
-
331
- end
332
- end
333
- end
334
-
335
-
336
- applicable = false
337
- # push schedules to hash to avoid making unnecessary duplicates
338
- htg_set_schs = {}
339
- # get spaces
362
+ # air_loop_avail_schs = {}
363
+ # air_loops = model.getAirLoopHVACs
364
+ # air_loops.each do |air_loop|
365
+ # avail_sch = air_loop.availabilitySchedule
366
+ #
367
+ # if air_loop_avail_schs.key?(avail_sch.name.to_s)
368
+ # new_avail_sch = air_loop_avail_schs[avail_sch.name.to_s]
369
+ # else
370
+ # new_avail_sch = avail_sch.clone(model).to_Schedule.get
371
+ # new_avail_sch.setName("#{avail_sch.name.to_s} adjusted")
372
+ # # add to the hash
373
+ # air_loop_avail_schs[avail_sch.name.to_s] = new_avail_sch
374
+ # end
375
+ # air_loop.setAvailabilitySchedule(new_avail_sch)
376
+ #
377
+ # end
378
+ #
379
+ # air_loop_avail_schs.each do |sch_name, air_loop_sch|
380
+ # runner.registerInfo("Air Loop Schedule #{sch_name}:")
381
+ # if air_loop_sch.to_ScheduleRuleset.empty?
382
+ # runner.registerWarning("Schedule #{sch_name} isn't a ScheduleRuleset object and won't be altered by this measure.")
383
+ # air_loop_sch.remove # remove un-used clone
384
+ # else
385
+ # schedule = air_loop_sch.to_ScheduleRuleset.get
386
+ # default_rule = schedule.defaultDaySchedule
387
+ # rules = schedule.scheduleRules
388
+ # days_covered = Array.new(7, false)
389
+ #
390
+ # rules.each do |rule|
391
+ # winter_avail_rule1 = copy_sch_rule_for_period(model, rule, rule.daySchedule, winterStartDate1, winterEndDate1)
392
+ # winter_avail_rule2 = copy_sch_rule_for_period(model, rule, rule.daySchedule, winterStartDate2, winterEndDate2)
393
+ # runner.registerInfo(" ------------ time: #{rule.daySchedule.times.map {|os_time| os_time.toString}}")
394
+ # runner.registerInfo(" ------------ values: #{rule.daySchedule.values}")
395
+ # summer_avail_rule = rule.clone(model).to_ScheduleRule.get
396
+ # summer_avail_rule.setStartDate(summerStartDate)
397
+ # summer_avail_rule.setEndDate(summerEndDate)
398
+ #
399
+ # checkDaysCovered(summer_avail_rule, days_covered)
400
+ #
401
+ # summer_avail_day = summer_avail_rule.daySchedule
402
+ # day_time_vector = summer_avail_day.times
403
+ # day_value_vector = summer_avail_day.values
404
+ # summer_avail_day.clearValues
405
+ #
406
+ # summer_avail_day = updateAvailDaySchedule(summer_avail_day, day_time_vector, day_value_vector, shift_time1, shift_time2)
407
+ # runner.registerInfo(" ------------ updated time: #{summer_avail_day.times.map {|os_time| os_time.toString}}")
408
+ # runner.registerInfo(" ------------ uodated values: #{summer_avail_day.values}")
409
+ #
410
+ # end
411
+ #
412
+ # if days_covered.include?(false)
413
+ #
414
+ # winter_rule1 = create_sch_rule_from_default(model, schedule, default_rule, winterStartDate1, winterEndDate1)
415
+ # winter_rule2 = create_sch_rule_from_default(model, schedule, default_rule, winterStartDate2, winterEndDate2)
416
+ #
417
+ # coverMissingDays(winter_rule1, days_covered)
418
+ # checkDaysCovered(winter_rule1, days_covered)
419
+ #
420
+ # summer_rule = copy_sch_rule_for_period(model, winter_rule1, default_rule, summerStartDate, summerEndDate)
421
+ #
422
+ # summer_day = summer_rule.daySchedule
423
+ # day_time_vector = summer_day.times
424
+ # day_value_vector = summer_day.values
425
+ # summer_day.clearValues
426
+ #
427
+ # summer_day = updateAvailDaySchedule(summer_day, day_time_vector, day_value_vector, shift_time1, shift_time2)
428
+ #
429
+ # end
430
+ # end
431
+ # end
432
+
433
+
434
+ ## push schedules to hash to avoid making unnecessary duplicates
435
+ ## one heating schedule set can correspond to multiple different cooling schedule set
436
+ ## each unique pair would be cloned into a new heating schedule set because of the potential deadband conflict
437
+ sch_set_mapping = {}
438
+ ## sch_set_mapping = {old_heat_sch_name: {corresponding_cool_sch_handle1: [cloned_heat_sch1, corresponding_cool_sch1],
439
+ ## corresponding_cool_sch_handle2: [cloned_heat_sch2, corresponding_cool_sch2]} }
340
440
  thermostats = model.getThermostatSetpointDualSetpoints
341
441
  thermostats.each do |thermostat|
342
- # setup new cooling setpoint schedule
442
+ ## setup new cooling setpoint schedule
343
443
  htg_set_sch = thermostat.heatingSetpointTemperatureSchedule
444
+ clg_set_sch = thermostat.coolingSetpointTemperatureSchedule
344
445
  if htg_set_sch.empty?
345
446
  runner.registerWarning("Thermostat '#{thermostat.name}' doesn't have a heating setpoint schedule")
346
447
  else
347
- # clone of not alredy in hash
348
- if htg_set_schs.key?(htg_set_sch.get.name.to_s)
349
- new_htg_set_sch = htg_set_schs[htg_set_sch.get.name.to_s]
448
+ old_htg_schedule = htg_set_sch.get
449
+ old_schedule_name = old_htg_schedule.name.to_s
450
+ if old_htg_schedule.to_ScheduleRuleset.is_initialized
451
+ if sch_set_mapping.key?(old_schedule_name)
452
+ if clg_set_sch.empty? || clg_set_sch.get.to_ScheduleRuleset.empty?
453
+ if sch_set_mapping[old_schedule_name].key?"nil"
454
+ new_htg_set_sch = sch_set_mapping[old_schedule_name]["nil"][0]
455
+ else
456
+ new_htg_set_sch = old_htg_schedule.clone(model).to_Schedule.get
457
+ n_new_heat_sch = sch_set_mapping[old_schedule_name].size
458
+ new_htg_set_sch.setName("#{old_schedule_name} adjusted by #{heating_adjustment}F_#{n_new_heat_sch}")
459
+ sch_set_mapping[old_schedule_name]["nil"] = [new_htg_set_sch, nil]
460
+ end
461
+ else
462
+ clg_sch_handle = clg_set_sch.get.handle.to_s
463
+ if sch_set_mapping[old_schedule_name].key?clg_sch_handle
464
+ new_htg_set_sch = sch_set_mapping[old_schedule_name][clg_sch_handle][0]
465
+ else
466
+ new_htg_set_sch = old_htg_schedule.clone(model).to_Schedule.get
467
+ n_new_heat_sch = sch_set_mapping[old_schedule_name].size
468
+ new_htg_set_sch.setName("#{old_schedule_name} adjusted by #{heating_adjustment}F_#{n_new_heat_sch}")
469
+ sch_set_mapping[old_schedule_name][clg_sch_handle] = [new_htg_set_sch, clg_set_sch.get]
470
+ end
471
+ end
472
+ else
473
+ new_htg_set_sch = old_htg_schedule.clone(model).to_Schedule.get
474
+ new_htg_set_sch.setName("#{old_schedule_name} adjusted by #{heating_adjustment}F")
475
+ if clg_set_sch.is_initialized
476
+ sch_set_mapping[old_schedule_name] = {clg_set_sch.get.handle.to_s => [new_htg_set_sch, clg_set_sch.get]}
477
+ else
478
+ sch_set_mapping[old_schedule_name] = {"nil" => [new_htg_set_sch, nil]}
479
+ end
480
+ end
481
+ ## hook up clone to thermostat
482
+ thermostat.setHeatingSetpointTemperatureSchedule(new_htg_set_sch)
350
483
  else
351
- new_htg_set_sch = htg_set_sch.get.clone(model).to_Schedule.get
352
- new_htg_set_sch.setName("#{htg_set_sch.get.name.to_s} adjusted by #{heating_adjustment}")
353
- htg_set_schs[htg_set_sch.get.name.to_s] = new_htg_set_sch
484
+ runner.registerWarning("Schedule '#{old_schedule_name}' isn't a ScheduleRuleset object and won't be altered by this measure.")
354
485
  end
355
- # hook up clone to thermostat
356
- thermostat.setHeatingSetpointTemperatureSchedule(new_htg_set_sch)
357
486
  end
358
-
487
+ end
488
+ sch_set_mapping.each do |old_sch_name, new_sch_hash|
489
+ runner.registerInfo("The original heating schedule ruleset #{old_sch_name} is paired with #{new_sch_hash.size} unique cooling schedules, so it will be cloned as #{new_sch_hash.size} new schedules")
359
490
  end
360
491
 
361
-
362
- # consider issuing a warning if the model has un-conditioned thermal zones (no ideal air loads or hvac)
492
+ ## consider issuing a warning if the model has un-conditioned thermal zones (no ideal air loads or hvac)
363
493
  zones = model.getThermalZones
364
494
  zones.each do |zone|
365
- # if you have a thermostat but don't have ideal air loads or zone equipment then issue a warning
495
+ ## if you have a thermostat but don't have ideal air loads or zone equipment then issue a warning
366
496
  if !zone.thermostatSetpointDualSetpoint.empty? && !zone.useIdealAirLoads && (zone.equipment.size <= 0)
367
497
  runner.registerWarning("Thermal zone '#{zone.name}' has a thermostat but does not appear to be conditioned.")
368
498
  end
369
499
  end
370
500
 
371
- # make cooling schedule adjustments and rename. Put in check to skip and warn if schedule not ruleset
372
- htg_set_schs.each do |sch_name, heating_schedule| # old name and new object for schedule
373
- if heating_schedule.to_ScheduleRuleset.empty?
374
- runner.registerWarning("Schedule #{sch_name} isn't a ScheduleRuleset object and won't be altered by this measure.")
375
- heating_schedule.remove # remove un-used clone
376
- else
377
- schedule = heating_schedule.to_ScheduleRuleset.get
378
- default_rule = schedule.defaultDaySchedule
501
+ heating_adjust_period_inputs = { "period1" => {"date_start"=>os_start_date1, "date_end"=>os_end_date1,
502
+ "time_start"=>shift_time_start1, "time_end"=>shift_time_end1},
503
+ "period2" => {"date_start"=>os_start_date2, "date_end"=>os_end_date2,
504
+ "time_start"=>shift_time_start2, "time_end"=>shift_time_end2},
505
+ "period3" => {"date_start"=>os_start_date3, "date_end"=>os_end_date3,
506
+ "time_start"=>shift_time_start3, "time_end"=>shift_time_end3},
507
+ "period4" => {"date_start"=>os_start_date4, "date_end"=>os_end_date4,
508
+ "time_start"=>shift_time_start4, "time_end"=>shift_time_end4},
509
+ "period5" => {"date_start"=>os_start_date5, "date_end"=>os_end_date5,
510
+ "time_start"=>shift_time_start5, "time_end"=>shift_time_end5} }
511
+
512
+ applicable = false
513
+
514
+ sch_set_mapping.each do |old_sch_name, new_sch_hash|
515
+ new_sch_hash.each do |paired_cool_sch_handle, os_schs|
516
+ schedule = os_schs[0].to_ScheduleRuleset.get
517
+ if os_schs[1].nil?
518
+ cooling_set = nil
519
+ else
520
+ cooling_set = os_schs[1].to_ScheduleRuleset.get
521
+ end
379
522
  rules = schedule.scheduleRules
380
523
  days_covered = Array.new(7, false)
381
- if rules.length > 0
382
- runner.registerInfo("Schedule #{sch_name} has #{rules.length} rules.")
524
+ current_index = 0
525
+ if rules.length <= 0
526
+ runner.registerWarning("Heating setpoint schedule '#{old_sch_name}' is a ScheduleRuleSet, but has no ScheduleRules associated. It won't be altered by this measure.")
527
+ else
528
+ runner.registerInfo("Heating schedule ruleset #{schedule.name.to_s} cloned from #{old_sch_name} has #{rules.length} rules.")
383
529
  rules.each do |rule|
384
- runner.registerInfo("------------ Rule #{rule.ruleIndex}: #{rule.daySchedule.name.to_s}")
385
- # Use the original schedule rule to cover the rest of the year
386
- summer_rule = copy_sch_rule_for_period(model, rule, rule.daySchedule, summerStartDate, summerEndDate)
387
- winter_rule1 = rule
388
- checkDaysCovered(winter_rule1, days_covered)
389
- winter_rule1.setStartDate(winterStartDate1)
390
- winter_rule1.setEndDate(winterEndDate1)
391
-
392
- winter_day1 = winter_rule1.daySchedule
393
- day_time_vector = winter_day1.times
394
- day_value_vector = winter_day1.values
395
- unless day_value_vector.empty?
396
- applicable = true
530
+ runner.registerInfo("---- Rule No.#{rule.ruleIndex}: #{rule.name.to_s}")
531
+ if rule.dateSpecificationType == "SpecificDates"
532
+ ## if the rule applies to SpecificDates, collect the dates that fall into each adjustment date period,
533
+ ## and create a new rule for each date period with covered specific dates
534
+ runner.registerInfo("-------- The rule #{rule.name.to_s} only covers specific dates.")
535
+ ## the specificDates cannot be modified in place because it's a frozen array
536
+ all_specific_dates = []
537
+ rule.specificDates.each { |date| all_specific_dates << date }
538
+ heating_adjust_period_inputs.each do |period, period_inputs|
539
+ period_inputs["specific_dates"] = []
540
+ os_start_date = period_inputs["date_start"]
541
+ os_end_date = period_inputs["date_end"]
542
+ shift_time_start = period_inputs["time_start"]
543
+ shift_time_end = period_inputs["time_end"]
544
+ if [os_start_date, os_end_date, shift_time_start, shift_time_end].all?
545
+ rule.specificDates.each do |covered_date|
546
+ if covered_date >= os_start_date and covered_date <= os_end_date
547
+ period_inputs["specific_dates"] << covered_date
548
+ all_specific_dates.delete(covered_date)
549
+ end
550
+ end
551
+ runner.registerInfo("-------- Specific dates within date range #{os_start_date.to_s} to #{os_end_date.to_s}: #{period_inputs["specific_dates"].map(&:to_s)}")
552
+ # runner.registerInfo("!!! Specific dates haven't been covered: #{all_specific_dates.map(&:to_s)}")
553
+ next if period_inputs["specific_dates"].empty?
554
+ ## If there's no corresponding cooling set, no need to compare the cooling & heating setpoint to avoid deadband issue
555
+ if cooling_set.nil?
556
+ runner.registerInfo("---- Before, heating schedule: #{rule.daySchedule.times.map(&:to_s)}, #{rule.daySchedule.values}")
557
+ runner.registerInfo(" NO PAIRED COOLING SCHEDULE!")
558
+ rule_period = modify_rule_for_specific_dates(rule, os_start_date, os_end_date, shift_time_start, shift_time_end,
559
+ heating_adjustment_si, period_inputs["specific_dates"], compared_day_sch=nil)
560
+ runner.registerInfo("---- After, adjusted schedule: #{rule_period.daySchedule.times.map(&:to_s)}, #{rule_period.daySchedule.values}")
561
+ if rule_period
562
+ applicable = true
563
+ schedule.setScheduleRuleIndex(rule_period, current_index)
564
+ current_index += 1
565
+ runner.registerInfo("-------- The rule #{rule_period.name.to_s} for #{rule_period.dateSpecificationType} is added as priority #{current_index}")
566
+ end
567
+ else
568
+ ## Otherwise, compare each cooling dayschedule within the applied date range to create a new heating schedule
569
+ ## to avoid deadband conflicts
570
+ cool_day_mapping = {}
571
+ period_inputs["specific_dates"].each do |date|
572
+ corresponding_cool_day = cooling_set.getDaySchedules(date, date)[0]
573
+ if cool_day_mapping.key?(corresponding_cool_day.name.to_s)
574
+ cool_day_mapping[corresponding_cool_day.name.to_s]["dates"] << date
575
+ else
576
+ cool_day_mapping[corresponding_cool_day.name.to_s] = {"cool_day_schedule" => corresponding_cool_day, "dates" => [date]}
577
+ end
578
+ end
579
+ runner.registerInfo("#{period} has #{cool_day_mapping.size} cooling setpoint days. ")
580
+ cool_day_mapping.each do |schedule_day_name, day_info|
581
+ runner.registerInfo("Cooling sch #{schedule_day_name} applies to #{day_info["dates"].size} days")
582
+ cool_day = day_info["cool_day_schedule"]
583
+ runner.registerInfo("---- Before, cooling schedule: #{cool_day.times.map(&:to_s)}, #{cool_day.values}")
584
+ runner.registerInfo(" heating schedule: #{rule.daySchedule.times.map(&:to_s)}, #{rule.daySchedule.values}")
585
+ rule_period = modify_rule_for_specific_dates(rule, os_start_date, os_end_date, shift_time_start, shift_time_end,
586
+ heating_adjustment_si, day_info["dates"], compared_day_sch=cool_day)
587
+ runner.registerInfo("---- After, adjusted schedule: #{rule_period.daySchedule.times.map(&:to_s)}, #{rule_period.daySchedule.values}")
588
+ if rule_period
589
+ applicable = true
590
+ schedule.setScheduleRuleIndex(rule_period, current_index)
591
+ current_index += 1
592
+ runner.registerInfo("-------- The rule #{rule_period.name.to_s} for #{rule_period.dateSpecificationType} is added as priority #{current_index}")
593
+ end
594
+ end
595
+ end
596
+
597
+ end
598
+ end
599
+ if all_specific_dates.empty?
600
+ ## if all specific dates have been covered by new rules for each adjustment date period, remove the original rule
601
+ runner.registerInfo("The original rule is removed since no specific date left")
602
+ else
603
+ ## if there's still dates left to be covered, modify the original rule to only cover these dates
604
+ ## (this is just in case that the rule order was not set correctly, and the original rule is still applied to all specific dates;
605
+ ## also to make the logic in OSM more clearer)
606
+ ## the specificDates cannot be modified in place, so create a new rule with the left dates to replace the original rule
607
+ original_rule_update = clone_rule_with_new_dayschedule(rule, rule.name.to_s + " - dates left")
608
+ schedule.setScheduleRuleIndex(original_rule_update, current_index)
609
+ current_index += 1
610
+ all_specific_dates.each do |date|
611
+ original_rule_update.addSpecificDate(date)
612
+ end
613
+ runner.registerInfo("-------- The original rule #{rule.name.to_s} is modified to only cover the rest of the dates: #{all_specific_dates.map(&:to_s)}")
614
+ runner.registerInfo("-------- and is shifted to priority #{current_index}")
615
+ end
616
+ rule.remove
617
+
618
+
619
+ else
620
+ ## If the rule applies to a DateRange, check if the DateRange overlaps with each adjustment date period
621
+ ## if so, create a new rule for that adjustment date period
622
+ runner.registerInfo("******* The rule #{rule.name.to_s} covers date range #{rule.startDate.get} - #{rule.endDate.get}.")
623
+ heating_adjust_period_inputs.each do |period, period_inputs|
624
+ os_start_date = period_inputs["date_start"]
625
+ os_end_date = period_inputs["date_end"]
626
+ shift_time_start = period_inputs["time_start"]
627
+ shift_time_end = period_inputs["time_end"]
628
+ if [os_start_date, os_end_date, shift_time_start, shift_time_end].all?
629
+ ## check if the original rule applied DateRange overlaps with the adjustment date period
630
+ overlapped, new_start_dates, new_end_dates = check_date_ranges_overlap(rule, os_start_date, os_end_date)
631
+ next unless overlapped
632
+ #############################################################
633
+
634
+ ## If there's no corresponding cooling set, no need to compare the cooling & heating setpoint to avoid deadband issue
635
+ if cooling_set.nil?
636
+ runner.registerInfo("**** Before, heating schedule: #{rule.daySchedule.times.map(&:to_s)}, #{rule.daySchedule.values}")
637
+ runner.registerInfo(" NO PAIRED COOLING SCHEDULE!")
638
+ new_start_dates.each_with_index do |start_date, i|
639
+ rule_period = modify_rule_for_date_period(rule, start_date, new_end_dates[i], shift_time_start, shift_time_end,
640
+ heating_adjustment_si,
641
+ applied_dow=nil, compared_day_sch=nil)
642
+ runner.registerInfo("A new rule is cloned from the original heating rule:")
643
+ runner.registerInfo("#{rule_period.daySchedule.times.map(&:to_s)}, #{rule_period.daySchedule.values}")
644
+ if rule_period
645
+ applicable = true
646
+ if period == "period1"
647
+ checkDaysCovered(rule_period, days_covered)
648
+ end
649
+ schedule.setScheduleRuleIndex(rule_period, current_index)
650
+ current_index += 1
651
+ runner.registerInfo("******* The rule #{rule_period.name.to_s} is added as priority #{current_index}")
652
+ end
653
+ end
654
+ else
655
+ heat_sch_applied_dates = get_applied_dates_in_range(os_start_date, os_end_date, rule)
656
+ ## Get all dayschedules for the corresponding cooling schedule set for the dates where the heating schedule is applies
657
+ ## cool_day_mapping = {cooling_dayschedule_name => {"cool_day_schedule"=> cooling_dayschedule,
658
+ ## "dates"=>[applied_date1, applied_date2]}}
659
+ cool_day_mapping = {}
660
+ ## Get the day of week for all applicable dates
661
+ day_of_week_mapping = {}
662
+ heat_sch_applied_dates.each do |date|
663
+ corresponding_cool_day = cooling_set.getDaySchedules(date, date)[0]
664
+ if cool_day_mapping.key?(corresponding_cool_day.name.to_s)
665
+ cool_day_mapping[corresponding_cool_day.name.to_s]["dates"] << date
666
+ else
667
+ cool_day_mapping[corresponding_cool_day.name.to_s] = {"cool_day_schedule" => corresponding_cool_day, "dates" => [date]}
668
+ end
669
+ if day_of_week_mapping.key?date.dayOfWeek.valueName
670
+ day_of_week_mapping[date.dayOfWeek.valueName] << date
671
+ else
672
+ day_of_week_mapping[date.dayOfWeek.valueName] = [date]
673
+ end
674
+ end
675
+ runner.registerInfo("#{period} has #{cool_day_mapping.size} cooling setpoint days.")
676
+ cool_day_mapping.each do |schedule_day_name, day_info|
677
+ runner.registerInfo("Cooling sch #{schedule_day_name} applies to #{day_info["dates"].size} days")
678
+ if cool_day_mapping.size == 1
679
+ ## If there's only one corresponding cooling scheduleday, no need to clone multiple heating schedules
680
+ applied_day_of_week = nil
681
+ cool_sch_dates = []
682
+ else
683
+ cool_sch_dates = day_info["dates"].map(&:to_s)
684
+ applied_day_of_week = []
685
+ day_of_week_mapping.each do |day_of_week, dates|
686
+ week_day_dates = dates.map(&:to_s)
687
+ if (week_day_dates - cool_sch_dates).empty?
688
+ applied_day_of_week << day_of_week
689
+ cool_sch_dates -= week_day_dates
690
+ runner.registerInfo("Cooling sch #{schedule_day_name} applies to #{day_of_week}")
691
+ if cool_sch_dates.empty?
692
+ break
693
+ end
694
+ end
695
+ end
696
+ end
697
+ cool_day = day_info["cool_day_schedule"]
698
+ runner.registerInfo("**** Before, cooling schedule: #{cool_day.times.map(&:to_s)}, #{cool_day.values}")
699
+ runner.registerInfo(" heating schedule: #{rule.daySchedule.times.map(&:to_s)}, #{rule.daySchedule.values}")
700
+ new_start_dates.each_with_index do |start_date, i|
701
+ ## If applied_day_of_week is nil (only one cooling scheduleday), or the applied_day_of_week is not empty
702
+ unless applied_day_of_week&.empty?
703
+ rule_period = modify_rule_for_date_period(rule, start_date, new_end_dates[i], shift_time_start, shift_time_end, heating_adjustment_si,
704
+ applied_dow=applied_day_of_week, compared_day_sch=cool_day)
705
+ runner.registerInfo("A new rule is created for #{applied_day_of_week}:")
706
+ runner.registerInfo("#{rule_period.daySchedule.times.map(&:to_s)}, #{rule_period.daySchedule.values}")
707
+ if rule_period
708
+ applicable = true
709
+ if period == "period1"
710
+ checkDaysCovered(rule_period, days_covered)
711
+ end
712
+ schedule.setScheduleRuleIndex(rule_period, current_index)
713
+ current_index += 1
714
+ runner.registerInfo("******* The rule #{rule_period.name.to_s} is added as priority #{current_index}")
715
+ end
716
+ end
717
+
718
+ unless cool_sch_dates.empty?
719
+ left_os_dates = cool_sch_dates.map {|str_date| OpenStudio::Date.new(str_date)}
720
+ runner.registerInfo("Cooling sch #{schedule_day_name} still covers other days. These days will be added as a rule for specific dates.")
721
+ rule_for_left_dates = modify_rule_for_specific_dates(rule, os_start_date, os_end_date, shift_time_start, shift_time_end,
722
+ heating_adjustment_si, left_os_dates, compared_day_sch=cool_day)
723
+ schedule.setScheduleRuleIndex(rule_for_left_dates, current_index)
724
+ current_index += 1
725
+ runner.registerInfo("******* The rule #{rule_for_left_dates.name.to_s} is added as priority #{current_index}")
726
+ end
727
+ end
728
+ end
729
+ end
730
+
731
+ end
732
+ end
733
+ ## The original rule will be shifted to the currently lowest priority
734
+ ## Setting the rule to an existing index will automatically push all other rules after it down
735
+ schedule.setScheduleRuleIndex(rule, current_index)
736
+ runner.registerInfo("========== The original rule #{rule.name.to_s} is shifted to priority #{current_index}")
737
+ current_index += 1
397
738
  end
398
- winter_day1.clearValues
399
-
400
- winter_day1 = updateDaySchedule(winter_day1, day_time_vector, day_value_vector, shift_time1, shift_time2, heating_adjustment_si)
401
- winter_rule2 = copy_sch_rule_for_period(model, winter_rule1, winter_rule1.daySchedule, winterStartDate2, winterEndDate2)
402
739
 
403
740
  end
404
- else
405
- runner.registerWarning("Heating setpoint schedule #{sch_name} is a ScheduleRuleSet, but has no ScheduleRules associated. It won't be altered by this measure.")
406
741
  end
407
742
 
743
+ default_day = schedule.defaultDaySchedule
408
744
  if days_covered.include?(false)
409
- summer_rule = create_sch_rule_from_default(model, schedule, default_rule, summerStartDate, summerEndDate)
410
- coverMissingDays(summer_rule, days_covered)
411
- checkDaysCovered(summer_rule, days_covered)
745
+ runner.registerInfo("Some days use default day. Adding new scheduleRule from defaultDaySchedule for applicable date period.")
746
+ heating_adjust_period_inputs.each do |period, period_inputs|
747
+ os_start_date = period_inputs["date_start"]
748
+ os_end_date = period_inputs["date_end"]
749
+ shift_time_start = period_inputs["time_start"]
750
+ shift_time_end = period_inputs["time_end"]
751
+ if [os_start_date, os_end_date, shift_time_start, shift_time_end].all?
752
+ modify_default_day_for_date_period(schedule, default_day, days_covered, os_start_date, os_end_date, shift_time_start, shift_time_end,
753
+ heating_adjustment_si, current_index, cooling_set, runner)
754
+ # schedule.setScheduleRuleIndex(new_default_rule_period, current_index)
755
+ applicable = true
756
+ end
757
+ end
412
758
 
413
- winter_rule1 = copy_sch_rule_for_period(model, summer_rule, default_rule, winterStartDate1, winterEndDate1)
414
- winter_day1 = winter_rule1.daySchedule
415
- day_time_vector = winter_day1.times
416
- day_value_vector = winter_day1.values
417
- winter_day1.clearValues
759
+ end
760
+ end
418
761
 
419
- winter_day1 = updateDaySchedule(winter_day1, day_time_vector, day_value_vector, shift_time1, shift_time2, heating_adjustment_si)
420
- winter_rule2 = copy_sch_rule_for_period(model, winter_rule1, winter_rule1.daySchedule, winterStartDate2, winterEndDate2)
762
+ end
763
+
764
+ # affected_actuators = {original_affected actuator_handle: [added_actuator_for_original_sch_copy1, added_actuator_for_original_sch_copy2]}
765
+ affected_actuators = {}
766
+ model.getEnergyManagementSystemActuators.each do |ems_actuator|
767
+ next unless ems_actuator.actuatedComponent.is_initialized
768
+ actuated_comp_name = ems_actuator.actuatedComponent.get.name.to_s
769
+ if sch_set_mapping.key?actuated_comp_name
770
+ ## If the actuator works on a heating schedule that has been modified, the actuator needs to be hooked up with the modified (cloned) schedule
771
+ ## Also, if there's extra cloned copies of the original schedule, each of them needs an actuator (as well as a corresponding EMS program as well)
772
+ new_sch_hash = sch_set_mapping[actuated_comp_name]
773
+ new_sch_hash.each_with_index do |(key, schs), index|
774
+ if index == 0
775
+ ems_actuator.setActuatedComponent(schs[0])
776
+ runner.registerInfo("The actuator component for EMS actuator #{ems_actuator.name.to_s} has been changed from #{actuated_comp_name} to #{schs[0].name.to_s}")
777
+ affected_actuators[ems_actuator.handle.to_s] = []
778
+ else
779
+ ## Clone a new actuator for each extra copy of the original schedule and hook them up
780
+ new_actuator = ems_actuator.clone(model).to_EnergyManagementSystemActuator.get
781
+ new_actuator.setActuatedComponent(schs[0])
782
+ runner.registerInfo("A new actuator #{new_actuator.name.to_s} has been added for the newly added schedule #{schs[0].name.to_s}")
783
+ affected_actuators[ems_actuator.handle.to_s] << new_actuator.handle.to_s
784
+ end
785
+ end
786
+ end
787
+ end
421
788
 
789
+ model.getEnergyManagementSystemPrograms.each do |ems_program|
790
+ next unless ems_program.name.to_s.include?"OptimumStart"
791
+ referenced_obj_names = (ems_program.referencedObjects.map {|obj| obj.handle.to_s}).uniq
792
+ common_actuator = referenced_obj_names & affected_actuators.keys.map(&:to_s)
793
+ next if common_actuator.empty?
794
+ runner.registerInfo("EMS program #{ems_program.name.to_s} is associated with affected EMS actuators")
795
+ # runner.registerInfo("#{ems_program.lines}")
796
+ common_actuator.each do |actuator_handle|
797
+ heating_adjust_period_inputs.each do |period, period_inputs|
798
+ os_start_date = period_inputs["date_start"]
799
+ os_end_date = period_inputs["date_end"]
800
+ shift_time_start = period_inputs["time_start"]
801
+ shift_time_end = period_inputs["time_end"]
802
+ if [os_start_date, os_end_date, shift_time_start, shift_time_end].all?
803
+ ## The OptimumStartProg EMS is applied to hour 5 and 7 during non-daylight saving time, and hour 4 and 6 during daylight saving time
804
+ ## It's because the Hour function doesn't take daylight saving time into consideration, while the schedule objects do
805
+ ## So the actual time EMS functions is 5:00 and 7:00 (if you output the thermostat setpoint variable to check)
806
+ if shift_time_start.totalHours <= 7 and shift_time_end.totalHours > 5
807
+ ## If the EMS program conflicts with the precooling adjustment, disable the program by setting the actuator to null
808
+ ems_program.addLine("IF DayOfYear >= #{os_start_date.dayOfYear} && DayOfYear <= #{os_end_date.dayOfYear}")
809
+ ems_program.addLine("SET #{actuator_handle} = NULL")
810
+ ems_program.addLine("ENDIF")
811
+ end
812
+ end
813
+ end
814
+ ## For each extra copy of cloned heating schedule (actuator), clone an EMS program for it, and modify the corresponding handle in the program
815
+ affected_actuators[actuator_handle].each do |new_actuator_handle|
816
+ new_program = ems_program.clone(model).to_EnergyManagementSystemProgram.get
817
+ new_program_body = new_program.body.gsub(actuator_handle, new_actuator_handle)
818
+ new_program.setBody(new_program_body)
819
+ runner.registerInfo("A new EMS program #{new_program.name.to_s} has been created for the newly added actuator #{new_actuator_handle}")
422
820
  end
423
821
  end
424
822
  end
@@ -432,67 +830,250 @@ class Preheating < OpenStudio::Measure::ModelMeasure
432
830
  end
433
831
 
434
832
 
833
+ def get_applied_dates_in_range(os_start_date, os_end_date, rule)
834
+ total_dates = (os_end_date - os_start_date).totalDays.to_i
835
+ dates_vector = (0..total_dates).map{|delta| os_start_date + OpenStudio::Time.new(delta)}
836
+ apply_flag = rule.containsDates(dates_vector)
837
+ rule_applied_dates = dates_vector.select.with_index { |value, index| apply_flag[index] }
838
+ return rule_applied_dates
839
+ end
435
840
 
436
- def updateAvailDaySchedule(sch_day, vec_time, vec_value, time_begin, time_end)
437
- count = 0
438
- vec_time.each_with_index do |exist_timestamp, i|
439
- adjusted_value = 1
440
- if exist_timestamp > time_begin && exist_timestamp < time_end && count == 0
441
- sch_day.addValue(time_begin, vec_value[i])
442
- sch_day.addValue(exist_timestamp, adjusted_value)
443
- count = 1
444
- elsif exist_timestamp == time_end && count == 0
445
- sch_day.addValue(time_begin, vec_value[i])
446
- sch_day.addValue(exist_timestamp, adjusted_value)
447
- count = 2
448
- elsif exist_timestamp == time_begin && count == 0
449
- sch_day.addValue(exist_timestamp, vec_value[i])
450
- count = 1
451
- elsif exist_timestamp > time_end && count == 0
452
- sch_day.addValue(time_begin, vec_value[i])
453
- sch_day.addValue(time_end, adjusted_value)
454
- sch_day.addValue(exist_timestamp, vec_value[i])
455
- count = 2
456
- elsif exist_timestamp > time_begin && exist_timestamp < time_end && count==1
457
- sch_day.addValue(exist_timestamp, adjusted_value)
458
- elsif exist_timestamp == time_end && count==1
459
- sch_day.addValue(exist_timestamp, adjusted_value)
460
- count = 2
461
- elsif exist_timestamp > time_end && count == 1
462
- sch_day.addValue(time_end, adjusted_value)
463
- sch_day.addValue(exist_timestamp, vec_value[i])
464
- count = 2
465
- else
466
- sch_day.addValue(exist_timestamp, vec_value[i])
841
+ def merge_day_sch_with_min(day_schedule_left, day_schedule_right)
842
+
843
+ def value_at_time(target_time, times, values)
844
+ times.each_with_index do |until_time, index|
845
+ if target_time <= until_time
846
+ return values[index] # Return the corresponding value for the interval
847
+ end
467
848
  end
468
849
  end
469
-
470
- return sch_day
850
+ combined_times = (day_schedule_left.times+day_schedule_right.times).map(&:toString).uniq.sort.map {|str_time| OpenStudio::Time.new(str_time)}
851
+ original_day_times = day_schedule_left.times
852
+ original_day_values = day_schedule_left.values
853
+ # new_day_schedule = OpenStudio::Model::ScheduleDay.new(model)
854
+
855
+ day_schedule_left.clearValues
856
+ updated_times, updated_values = [], []
857
+ combined_times.each do |time|
858
+ # Get the current value from each series
859
+ value_left = value_at_time(time, original_day_times, original_day_values)
860
+ value_right = value_at_time(time, day_schedule_right.times, day_schedule_right.values)
861
+
862
+ # Calculate the maximum value at this time
863
+ max_value = [value_left, value_right].min
864
+ updated_times << time
865
+ updated_values << max_value
866
+ end
867
+ # Add to result only if the value changes to avoid redundant values
868
+ updated_values.each_with_index do |entry, index|
869
+ if index == updated_values.size || entry != updated_values[index + 1]
870
+ day_schedule_left.addValue(updated_times[index], entry)
871
+ end
872
+ end
873
+ return day_schedule_left
471
874
  end
472
875
 
876
+ def check_date_ranges_overlap(rule, adjust_start_date, adjust_end_date)
877
+ ## check if the original rule applied DateRange overlaps with the adjustment date period
878
+ overlapped = false
879
+ new_start_dates = []
880
+ new_end_dates = []
881
+ if rule.endDate.get >= rule.startDate.get and rule.startDate.get <= adjust_end_date and rule.endDate.get >= adjust_start_date
882
+ overlapped = true
883
+ new_start_dates << [adjust_start_date, rule.startDate.get].max
884
+ new_end_dates << [adjust_end_date, rule.endDate.get].min
885
+ elsif rule.endDate.get < rule.startDate.get
886
+ ## If the DateRange has a endDate < startDate, the range wraps around the year.
887
+ if rule.endDate.get >= adjust_start_date
888
+ overlapped = true
889
+ new_start_dates << adjust_start_date
890
+ new_end_dates << rule.endDate.get
891
+ end
892
+ if rule.startDate.get <= adjust_end_date
893
+ overlapped = true
894
+ new_start_dates << rule.startDate.get
895
+ new_end_dates << adjust_end_date
896
+ end
897
+ end
898
+ return overlapped, new_start_dates, new_end_dates
899
+ end
473
900
 
474
- def copy_sch_rule_for_period(model, sch_rule, sch_day, start_date, end_date)
475
- new_rule = sch_rule.clone(model).to_ScheduleRule.get
476
- new_rule.setStartDate(start_date)
477
- new_rule.setEndDate(end_date)
901
+ def clone_rule_with_new_dayschedule(original_rule, new_rule_name)
902
+ ## Cloning a scheduleRule will automatically clone the daySchedule associated with it, but it's a shallow copy,
903
+ ## because the daySchedule is a resource that can be used by many scheduleRule
904
+ ## Therefore, once the daySchedule is modified for the cloned scheduleRule, the original daySchedule is also changed
905
+ ## Also, there's no function to assign a new daySchedule to the existing scheduleRule,
906
+ ## so the only way to clone the scheduleRule but change the daySchedule is to construct a new scheduleRule with a daySchedule passed in
907
+ ## and copy all other settings from the original scheduleRule
908
+ rule_period = OpenStudio::Model::ScheduleRule.new(original_rule.scheduleRuleset, original_rule.daySchedule)
909
+ rule_period.setName(new_rule_name)
910
+ rule_period.setApplySunday(original_rule.applySunday)
911
+ rule_period.setApplyMonday(original_rule.applyMonday)
912
+ rule_period.setApplyTuesday(original_rule.applyTuesday)
913
+ rule_period.setApplyWednesday(original_rule.applyWednesday)
914
+ rule_period.setApplyThursday(original_rule.applyThursday)
915
+ rule_period.setApplyFriday(original_rule.applyFriday)
916
+ rule_period.setApplySaturday(original_rule.applySaturday)
917
+ return rule_period
918
+ end
478
919
 
479
- new_day_sch = sch_day.clone(model)
480
- new_day_sch.setParent(new_rule)
920
+ def modify_rule_for_date_period(original_rule, os_start_date, os_end_date, shift_time_start, shift_time_end,
921
+ adjustment, applied_dow=nil, compared_day_sch=nil)
922
+ # The cloned scheduleRule will automatically belongs to the originally scheduleRuleSet
923
+ # rule_period = original_rule.clone(model).to_ScheduleRule.get
924
+ # rule_period.daySchedule = original_rule.daySchedule.clone(model)
925
+ new_rule_name = "#{original_rule.name.to_s} with DF for #{os_start_date.to_s} to #{os_end_date.to_s}"
926
+ rule_period = clone_rule_with_new_dayschedule(original_rule, new_rule_name)
927
+ ## if applied_dow is nil, do not change the original applied day of week
928
+ ## Otherwise, overwrite if the rule is applied to a certain day of week
929
+ unless applied_dow.nil?
930
+ rule_period.setApplySunday(applied_dow.include?"Sunday")
931
+ rule_period.setApplyMonday(applied_dow.include?"Monday")
932
+ rule_period.setApplyTuesday(applied_dow.include?"Tuesday")
933
+ rule_period.setApplyWednesday(applied_dow.include?"Wednesday")
934
+ rule_period.setApplyThursday(applied_dow.include?"Monday")
935
+ rule_period.setApplyFriday(applied_dow.include?"Friday")
936
+ rule_period.setApplySaturday(applied_dow.include?"Saturday")
937
+ end
938
+ day_rule_period = rule_period.daySchedule
939
+ day_time_vector = day_rule_period.times
940
+ day_value_vector = day_rule_period.values
941
+ if day_time_vector.empty?
942
+ return false
943
+ end
944
+ day_rule_period.clearValues
945
+ updateDaySchedule(day_rule_period, day_time_vector, day_value_vector, shift_time_start, shift_time_end, adjustment)
946
+ unless compared_day_sch.nil?
947
+ merge_day_sch_with_min(day_rule_period, compared_day_sch)
948
+ end
949
+ if rule_period
950
+ rule_period.setStartDate(os_start_date)
951
+ rule_period.setEndDate(os_end_date)
952
+ end
953
+ return rule_period
954
+ end
481
955
 
482
- return new_rule
956
+ def modify_rule_for_specific_dates(original_rule, os_start_date, os_end_date, shift_time_start, shift_time_end,
957
+ adjustment, applied_dates, compared_day_sch = nil)
958
+ new_rule_name = "#{original_rule.name.to_s} with DF for #{os_start_date.to_s} to #{os_end_date.to_s}"
959
+ rule_period = clone_rule_with_new_dayschedule(original_rule, new_rule_name)
960
+ day_rule_period = rule_period.daySchedule
961
+ day_time_vector = day_rule_period.times
962
+ day_value_vector = day_rule_period.values
963
+ if day_time_vector.empty?
964
+ return false
965
+ end
966
+ day_rule_period.clearValues
967
+ updateDaySchedule(day_rule_period, day_time_vector, day_value_vector, shift_time_start, shift_time_end, adjustment)
968
+ unless compared_day_sch.nil?
969
+ merge_day_sch_with_min(day_rule_period, compared_day_sch)
970
+ end
971
+ if rule_period
972
+ applied_dates.each do |date|
973
+ rule_period.addSpecificDate(date)
974
+ end
975
+ end
976
+ return rule_period
483
977
  end
484
978
 
485
- def create_sch_rule_from_default(model, sch_ruleset, default_sch_fule, start_date, end_date)
486
- new_rule = OpenStudio::Model::ScheduleRule.new(sch_ruleset)
487
- new_rule.setStartDate(start_date)
488
- new_rule.setEndDate(end_date)
979
+ def modify_default_day_for_date_period(schedule_set, default_day, days_covered, os_start_date, os_end_date,
980
+ shift_time_start, shift_time_end, adjustment, current_index,
981
+ corresponding_cool_set, runner)
982
+ # the new rule created for the ScheduleRuleSet by default has the highest priority (ruleIndex=0)
983
+ new_default_rule = OpenStudio::Model::ScheduleRule.new(schedule_set, default_day)
984
+ new_default_rule.setName("#{schedule_set.name.to_s} default day with DF for #{os_start_date.to_s} to #{os_end_date.to_s}")
985
+ new_default_rule.setStartDate(os_start_date)
986
+ new_default_rule.setEndDate(os_end_date)
987
+ coverMissingDays(new_default_rule, days_covered)
988
+ # days_of_week = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
989
+ # days_of_week.select.with_index { |value, index| !days_covered[index] }
990
+ if corresponding_cool_set.nil?
991
+ runner.registerInfo("==== Before, heating schedule: #{default_day.times.map(&:to_s)}, #{default_day.values}")
992
+ runner.registerInfo(" NO PAIRED COOLING SCHEDULE!")
993
+ new_default_day = new_default_rule.daySchedule
994
+ day_time_vector = new_default_day.times
995
+ day_value_vector = new_default_day.values
996
+ new_default_day.clearValues
997
+ updateDaySchedule(new_default_day, day_time_vector, day_value_vector, shift_time_start, shift_time_end, adjustment)
998
+ runner.registerInfo("The default rule is created:")
999
+ runner.registerInfo("#{new_default_day.times.map(&:to_s)}, #{new_default_day.values}")
1000
+ else
1001
+ heat_sch_applied_dates = get_applied_dates_in_range(os_start_date, os_end_date, new_default_rule)
1002
+ cool_day_mapping = {}
1003
+ day_of_week_mapping = {}
1004
+ heat_sch_applied_dates.each do |date|
1005
+ corresponding_cool_day = corresponding_cool_set.getDaySchedules(date, date)[0]
1006
+ if cool_day_mapping.key?(corresponding_cool_day.name.to_s)
1007
+ cool_day_mapping[corresponding_cool_day.name.to_s]["dates"] << date
1008
+ else
1009
+ cool_day_mapping[corresponding_cool_day.name.to_s] = {"cool_day_schedule" => corresponding_cool_day, "dates" => [date]}
1010
+ end
1011
+ if day_of_week_mapping.key?date.dayOfWeek.valueName
1012
+ day_of_week_mapping[date.dayOfWeek.valueName] << date
1013
+ else
1014
+ day_of_week_mapping[date.dayOfWeek.valueName] = [date]
1015
+ end
1016
+ end
1017
+ if cool_day_mapping.size == 1
1018
+ cool_day = cool_day_mapping.values.first["cool_day_schedule"]
1019
+ runner.registerInfo("==== Before, cooling schedule: #{cool_day.times.map(&:to_s)}, #{cool_day.values}")
1020
+ runner.registerInfo(" heating schedule: #{default_day.times.map(&:to_s)}, #{default_day.values}")
1021
+ new_default_day = new_default_rule.daySchedule
1022
+ day_time_vector = new_default_day.times
1023
+ day_value_vector = new_default_day.values
1024
+ new_default_day.clearValues
1025
+ updateDaySchedule(new_default_day, day_time_vector, day_value_vector, shift_time_start, shift_time_end, adjustment)
1026
+ merge_day_sch_with_min(new_default_day, cool_day)
1027
+ runner.registerInfo("The default rule is created:")
1028
+ runner.registerInfo("#{new_default_day.times.map(&:to_s)}, #{new_default_day.values}")
1029
+ else
1030
+ cool_day_mapping.each do |schedule_day_name, day_info|
1031
+ cool_day = day_info["cool_day_schedule"]
1032
+ runner.registerInfo("==== Before, heating schedule: #{cool_day.times.map(&:to_s)}, #{cool_day.values}")
1033
+ runner.registerInfo(" cooling schedule: #{default_day.times.map(&:to_s)}, #{default_day.values}")
1034
+ applied_day_of_week = []
1035
+ cool_sch_dates = day_info["dates"].map(&:to_s)
1036
+ day_of_week_mapping.each do |day_of_week, dates|
1037
+ week_day_dates = dates.map(&:to_s)
1038
+ if (week_day_dates - cool_sch_dates).empty?
1039
+ applied_day_of_week << day_of_week
1040
+ cool_sch_dates -= week_day_dates
1041
+ if cool_sch_dates.empty?
1042
+ break
1043
+ end
1044
+ end
1045
+ end
1046
+ unless applied_day_of_week.empty?
1047
+ new_default_rule_dow = modify_rule_for_date_period(new_default_rule, os_start_date, os_end_date, shift_time_start, shift_time_end, adjustment,
1048
+ applied_dow=applied_day_of_week, compared_day_sch=cool_day)
1049
+ runner.registerInfo("A new rule is created for #{applied_day_of_week}:")
1050
+ runner.registerInfo("#{new_default_rule_dow.daySchedule.times.map(&:to_s)}, #{new_default_rule_dow.daySchedule.values}")
1051
+ schedule_set.setScheduleRuleIndex(new_default_rule_dow, current_index)
1052
+ current_index += 1
1053
+ runner.registerInfo("======== The rule #{new_default_rule_dow.name.to_s} is added as priority #{current_index}")
489
1054
 
490
- new_day_sch = default_sch_fule.clone(model)
491
- new_day_sch.setParent(new_rule)
1055
+ end
1056
+
1057
+ unless cool_sch_dates.empty?
1058
+ left_os_dates = cool_sch_dates.map {|str_date| OpenStudio::Date.new(str_date)}
1059
+ runner.registerInfo("Cooling sch #{schedule_day_name} still covers other days. These days will be added as a rule for specific dates.")
1060
+ new_default_rule_specific_dates = modify_rule_for_specific_dates(new_default_rule, os_start_date, os_end_date, shift_time_start, shift_time_end,
1061
+ adjustment, left_os_dates, compared_day_sch=cool_day)
1062
+ runner.registerInfo("A new rule is created for specific dates:")
1063
+ runner.registerInfo("#{new_default_rule_specific_dates.daySchedule.times.map(&:to_s)}, #{new_default_rule_specific_dates.daySchedule.values}")
1064
+ schedule_set.setScheduleRuleIndex(new_default_rule_specific_dates, current_index)
1065
+ current_index += 1
1066
+ runner.registerInfo("======== The rule #{new_default_rule_specific_dates.name.to_s} is added as priority #{current_index}")
1067
+ end
1068
+ end
1069
+ new_default_rule.remove
1070
+ end
1071
+ end
492
1072
 
493
- return new_rule
1073
+ # TODO: if the scheduleRuleSet has holidaySchedule (which is a ScheduleDay), it cannot be altered
494
1074
  end
495
1075
 
1076
+
496
1077
  def updateDaySchedule(sch_day, vec_time, vec_value, time_begin, time_end, adjustment)
497
1078
  count = 0
498
1079
  vec_time.each_with_index do |exist_timestamp, i|
@@ -531,7 +1112,7 @@ class Preheating < OpenStudio::Measure::ModelMeasure
531
1112
  end
532
1113
 
533
1114
  def checkDaysCovered(sch_rule, sch_day_covered)
534
- if sch_rule.applySunday
1115
+ if sch_rule.applySunday
535
1116
  sch_day_covered[0] = true
536
1117
  end
537
1118
  if sch_rule.applyMonday
@@ -576,8 +1157,11 @@ class Preheating < OpenStudio::Measure::ModelMeasure
576
1157
  if sch_day_covered[6] == false
577
1158
  sch_rule.setApplySaturday(true)
578
1159
  end
579
-
580
1160
  end
1161
+
1162
+
1163
+
1164
+
581
1165
 
582
1166
 
583
1167
  end