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