openstudio-extension 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +9 -0
  4. data/Gemfile +3 -1
  5. data/Jenkinsfile +10 -0
  6. data/README.md +230 -12
  7. data/Rakefile +88 -3
  8. data/bin/console +3 -3
  9. data/doc_templates/LICENSE.md +27 -0
  10. data/doc_templates/README.md.erb +42 -0
  11. data/doc_templates/copyright_erb.txt +36 -0
  12. data/doc_templates/copyright_js.txt +4 -0
  13. data/doc_templates/copyright_ruby.txt +34 -0
  14. data/init_templates/README.md +37 -0
  15. data/init_templates/gemspec.txt +32 -0
  16. data/init_templates/openstudio_module.rb +50 -0
  17. data/init_templates/spec.rb +47 -0
  18. data/init_templates/spec_helper.rb +49 -0
  19. data/init_templates/template_gemfile.txt +17 -0
  20. data/init_templates/template_rakefile.txt +15 -0
  21. data/init_templates/version.rb +40 -0
  22. data/lib/files/openstudio-extension-gem-test.ddy +536 -0
  23. data/lib/files/openstudio-extension-gem-test.epw +8768 -0
  24. data/lib/files/openstudio-extension-gem-test.stat +554 -0
  25. data/lib/measures/openstudio_extension_test_measure/LICENSE.md +27 -0
  26. data/lib/measures/openstudio_extension_test_measure/README.md +26 -0
  27. data/lib/measures/openstudio_extension_test_measure/README.md.erb +42 -0
  28. data/lib/measures/openstudio_extension_test_measure/measure.rb +72 -0
  29. data/lib/measures/openstudio_extension_test_measure/measure.xml +83 -0
  30. data/lib/measures/openstudio_extension_test_measure/resources/os_lib_helper_methods.rb +399 -0
  31. data/lib/measures/openstudio_extension_test_measure/tests/OpenStudioExtensionTestMeasure_Test.rb +75 -0
  32. data/lib/openstudio/extension.rb +220 -0
  33. data/lib/openstudio/extension/core/CreateResults.rb +879 -0
  34. data/lib/openstudio/extension/core/check_air_sys_temps.rb +190 -0
  35. data/lib/openstudio/extension/core/check_calibration.rb +155 -0
  36. data/lib/openstudio/extension/core/check_cond_zns.rb +84 -0
  37. data/lib/openstudio/extension/core/check_domestic_hot_water.rb +334 -0
  38. data/lib/openstudio/extension/core/check_envelope_conductance.rb +453 -0
  39. data/lib/openstudio/extension/core/check_eui_by_end_use.rb +162 -0
  40. data/lib/openstudio/extension/core/check_eui_reasonableness.rb +135 -0
  41. data/lib/openstudio/extension/core/check_fan_pwr.rb +98 -0
  42. data/lib/openstudio/extension/core/check_internal_loads.rb +393 -0
  43. data/lib/openstudio/extension/core/check_mech_sys_capacity.rb +226 -0
  44. data/lib/openstudio/extension/core/check_mech_sys_efficiency.rb +326 -0
  45. data/lib/openstudio/extension/core/check_mech_sys_part_load_eff.rb +464 -0
  46. data/lib/openstudio/extension/core/check_mech_sys_type.rb +139 -0
  47. data/lib/openstudio/extension/core/check_part_loads.rb +451 -0
  48. data/lib/openstudio/extension/core/check_placeholder.rb +75 -0
  49. data/lib/openstudio/extension/core/check_plant_cap.rb +123 -0
  50. data/lib/openstudio/extension/core/check_plant_temps.rb +159 -0
  51. data/lib/openstudio/extension/core/check_plenum_loads.rb +87 -0
  52. data/lib/openstudio/extension/core/check_pump_pwr.rb +108 -0
  53. data/lib/openstudio/extension/core/check_sch_coord.rb +241 -0
  54. data/lib/openstudio/extension/core/check_schedules.rb +311 -0
  55. data/lib/openstudio/extension/core/check_simultaneous_heating_and_cooling.rb +158 -0
  56. data/lib/openstudio/extension/core/check_supply_air_and_thermostat_temp_difference.rb +148 -0
  57. data/lib/openstudio/extension/core/check_weather_files.rb +132 -0
  58. data/lib/openstudio/extension/core/deer_vintages.rb +311 -0
  59. data/lib/openstudio/extension/core/os_lib_aedg_measures.rb +491 -0
  60. data/lib/openstudio/extension/core/os_lib_cofee.rb +259 -0
  61. data/lib/openstudio/extension/core/os_lib_constructions.rb +378 -0
  62. data/lib/openstudio/extension/core/os_lib_geometry.rb +1022 -0
  63. data/lib/openstudio/extension/core/os_lib_helper_methods.rb +399 -0
  64. data/lib/openstudio/extension/core/os_lib_hvac.rb +2171 -0
  65. data/lib/openstudio/extension/core/os_lib_lighting_and_equipment.rb +214 -0
  66. data/lib/openstudio/extension/core/os_lib_model_generation.rb +817 -0
  67. data/lib/openstudio/extension/core/os_lib_model_simplification.rb +1049 -0
  68. data/lib/openstudio/extension/core/os_lib_outdoorair_and_infiltration.rb +165 -0
  69. data/lib/openstudio/extension/core/os_lib_reporting.rb +4652 -0
  70. data/lib/openstudio/extension/core/os_lib_reporting_qaqc.rb +200 -0
  71. data/lib/openstudio/extension/core/os_lib_schedules.rb +963 -0
  72. data/lib/openstudio/extension/rake_task.rb +149 -0
  73. data/lib/openstudio/extension/runner.rb +644 -0
  74. data/lib/openstudio/extension/version.rb +40 -0
  75. data/openstudio-extension.gemspec +20 -15
  76. metadata +150 -14
  77. data/.travis.yml +0 -7
  78. data/lib/OpenStudio/Extension/rake_task.rb +0 -84
  79. data/lib/OpenStudio/Extension/version.rb +0 -33
  80. data/lib/OpenStudio/extension.rb +0 -65
@@ -0,0 +1,200 @@
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2019, Alliance for Sustainable Energy, LLC.
3
+ # All rights reserved.
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # (1) Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # (3) Neither the name of the copyright holder nor the names of any contributors
15
+ # may be used to endorse or promote products derived from this software without
16
+ # specific prior written permission from the respective party.
17
+ #
18
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
19
+ # of modifications or other derivative works may not use the "OpenStudio"
20
+ # trademark, "OS", "os", or any other confusingly similar designation without
21
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
22
+ #
23
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
24
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
25
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
27
+ # UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
28
+ # THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
30
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
33
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ # *******************************************************************************
35
+
36
+ require 'json'
37
+
38
+ module OsLib_Reporting
39
+ # setup - get model, sql, and setup web assets path
40
+ def self.setup(runner)
41
+ results = {}
42
+
43
+ # get the last model
44
+ model = runner.lastOpenStudioModel
45
+ if model.empty?
46
+ runner.registerError('Cannot find last model.')
47
+ return false
48
+ end
49
+ model = model.get
50
+
51
+ # get the last idf
52
+ workspace = runner.lastEnergyPlusWorkspace
53
+ if workspace.empty?
54
+ runner.registerError('Cannot find last idf file.')
55
+ return false
56
+ end
57
+ workspace = workspace.get
58
+
59
+ # get the last sql file
60
+ sqlFile = runner.lastEnergyPlusSqlFile
61
+ if sqlFile.empty?
62
+ runner.registerError('Cannot find last sql file.')
63
+ return false
64
+ end
65
+ sqlFile = sqlFile.get
66
+ model.setSqlFile(sqlFile)
67
+
68
+ # populate hash to pass to measure
69
+ results[:model] = model
70
+ # results[:workspace] = workspace
71
+ results[:sqlFile] = sqlFile
72
+ results[:web_asset_path] = OpenStudio.getSharedResourcesPath / OpenStudio::Path.new('web_assets')
73
+
74
+ return results
75
+ end
76
+
77
+ # cleanup - prep html
78
+ def self.gen_html(html_in_path, web_asset_path, sections, name)
79
+ # instance variables for erb
80
+ @sections = sections
81
+ @name = name
82
+
83
+ # read in template
84
+ if File.exist?(html_in_path)
85
+ html_in_path = html_in_path
86
+ else
87
+ html_in_path = "#{File.dirname(__FILE__)}/report.html.erb"
88
+ end
89
+ html_in = ''
90
+ File.open(html_in_path, 'r') do |file|
91
+ html_in = file.read
92
+ end
93
+
94
+ # configure template with variable values
95
+ renderer = ERB.new(html_in)
96
+ html_out = renderer.result(binding)
97
+
98
+ # write html file
99
+ html_out_path = './report.html'
100
+ File.open(html_out_path, 'w') do |file|
101
+ file << html_out
102
+ # make sure data is written to the disk one way or the other
103
+ begin
104
+ file.fsync
105
+ rescue StandardError
106
+ file.flush
107
+ end
108
+ end
109
+
110
+ return html_out_path
111
+ end
112
+
113
+ # developer notes
114
+ # method below is custom version of standard OpenStudio results methods. It passes an array of sections vs. a single section.
115
+ # It doesn't use the model or SQL file. It just gets data form OpenStudio attributes passed in
116
+ # It doesn't have a name_only section since it doesn't populate user arguments
117
+
118
+ def self.sections_from_check_attributes(check_elems, runner)
119
+ # inspecting check attributes
120
+ # make single table with checks.
121
+ # make second table with flag description (with column for where it came from)
122
+
123
+ # array to hold sections
124
+ sections = []
125
+
126
+ # gather data for section
127
+ qaqc_check_summary = {}
128
+ qaqc_check_summary[:title] = 'List of Checks in Measure'
129
+ qaqc_check_summary[:header] = ['Name', 'Category', 'Flags', 'Description']
130
+ qaqc_check_summary[:data] = []
131
+ qaqc_check_summary[:data_color] = []
132
+ @qaqc_check_section = {}
133
+ @qaqc_check_section[:title] = 'QAQC Check Summary'
134
+ @qaqc_check_section[:tables] = [qaqc_check_summary]
135
+
136
+ # add sections to array
137
+ sections << @qaqc_check_section
138
+
139
+ # counter for flags thrown
140
+ num_flags = 0
141
+
142
+ check_elems.each do |check|
143
+ # gather data for section
144
+ qaqc_flag_details = {}
145
+ qaqc_flag_details[:title] = "List of Flags Triggered for #{check.valueAsAttributeVector.first.valueAsString}."
146
+ qaqc_flag_details[:header] = ['Flag Detail']
147
+ qaqc_flag_details[:data] = []
148
+ @qaqc_flag_section = {}
149
+ @qaqc_flag_section[:title] = check.valueAsAttributeVector.first.valueAsString.to_s
150
+ @qaqc_flag_section[:tables] = [qaqc_flag_details]
151
+
152
+ check_name = nil
153
+ check_cat = nil
154
+ check_desc = nil
155
+ flags = []
156
+ # loop through attributes (name,category,description,then optionally one or more flag attributes)
157
+ check.valueAsAttributeVector.each_with_index do |value, index|
158
+ if index == 0
159
+ check_name = value.valueAsString
160
+ elsif index == 1
161
+ check_cat = value.valueAsString
162
+ elsif index == 2
163
+ check_desc = value.valueAsString
164
+ else # should be flag
165
+ flags << value.valueAsString
166
+ qaqc_flag_details[:data] << [value.valueAsString]
167
+ runner.registerWarning("#{check_name} - #{value.valueAsString}")
168
+ num_flags += 1
169
+ end
170
+ end
171
+
172
+ # add row to table for this check
173
+ qaqc_check_summary[:data] << [check_name, check_cat, flags.size, check_desc]
174
+
175
+ # add info message for check if no flags found (this way user still knows what ran)
176
+ if check.valueAsAttributeVector.size < 4
177
+ runner.registerInfo("#{check_name} - no flags.")
178
+ end
179
+
180
+ # color cells based and add runner register values based on flag status
181
+ if !flags.empty?
182
+ qaqc_check_summary[:data_color] << ['', '', 'indianred', '']
183
+ runner.registerValue(check_name.downcase.tr(' ', '_'), flags.size, 'flags')
184
+ else
185
+ qaqc_check_summary[:data_color] << ['', '', 'lightgreen', '']
186
+ runner.registerValue(check_name.downcase.tr(' ', '_'), flags.size, 'flags')
187
+ end
188
+
189
+ # add table for this check if there are flags
190
+ if !qaqc_flag_details[:data].empty?
191
+ sections << @qaqc_flag_section
192
+ end
193
+ end
194
+
195
+ # add total flags registerValue
196
+ runner.registerValue('total_qaqc_flags', num_flags, 'flags')
197
+
198
+ return sections
199
+ end
200
+ end
@@ -0,0 +1,963 @@
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2019, Alliance for Sustainable Energy, LLC.
3
+ # All rights reserved.
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # (1) Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # (3) Neither the name of the copyright holder nor the names of any contributors
15
+ # may be used to endorse or promote products derived from this software without
16
+ # specific prior written permission from the respective party.
17
+ #
18
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
19
+ # of modifications or other derivative works may not use the "OpenStudio"
20
+ # trademark, "OS", "os", or any other confusingly similar designation without
21
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
22
+ #
23
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
24
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
25
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
27
+ # UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
28
+ # THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
30
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
33
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+ # *******************************************************************************
35
+
36
+ module OsLib_Schedules
37
+ # create a ruleset schedule with a basic profile
38
+ def self.createSimpleSchedule(model, options = {})
39
+ defaults = {
40
+ 'name' => nil,
41
+ 'winterTimeValuePairs' => { 24.0 => 0.0 },
42
+ 'summerTimeValuePairs' => { 24.0 => 1.0 },
43
+ 'defaultTimeValuePairs' => { 24.0 => 1.0 }
44
+ }
45
+
46
+ # merge user inputs with defaults
47
+ options = defaults.merge(options)
48
+
49
+ # ScheduleRuleset
50
+ sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
51
+ if name
52
+ sch_ruleset.setName(options['name'])
53
+ end
54
+
55
+ # Winter Design Day
56
+ winter_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
57
+ sch_ruleset.setWinterDesignDaySchedule(winter_dsn_day)
58
+ winter_dsn_day = sch_ruleset.winterDesignDaySchedule
59
+ winter_dsn_day.setName("#{sch_ruleset.name} Winter Design Day")
60
+ options['winterTimeValuePairs'].each do |k, v|
61
+ hour = k.truncate
62
+ min = ((k - hour) * 60).to_i
63
+ winter_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), v)
64
+ end
65
+
66
+ # Summer Design Day
67
+ summer_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
68
+ sch_ruleset.setSummerDesignDaySchedule(summer_dsn_day)
69
+ summer_dsn_day = sch_ruleset.summerDesignDaySchedule
70
+ summer_dsn_day.setName("#{sch_ruleset.name} Summer Design Day")
71
+ options['summerTimeValuePairs'].each do |k, v|
72
+ hour = k.truncate
73
+ min = ((k - hour) * 60).to_i
74
+ summer_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), v)
75
+ end
76
+
77
+ # All Days
78
+ default_day = sch_ruleset.defaultDaySchedule
79
+ default_day.setName("#{sch_ruleset.name} Schedule Week Day")
80
+ options['defaultTimeValuePairs'].each do |k, v|
81
+ hour = k.truncate
82
+ min = ((k - hour) * 60).to_i
83
+ default_day.addValue(OpenStudio::Time.new(0, hour, min, 0), v)
84
+ end
85
+
86
+ result = sch_ruleset
87
+ return result
88
+ end
89
+
90
+ # find the maximum profile value for a schedule
91
+ def self.getMinMaxAnnualProfileValue(model, schedule)
92
+ # validate schedule
93
+ if schedule.to_ScheduleRuleset.is_initialized
94
+ schedule = schedule.to_ScheduleRuleset.get
95
+
96
+ # gather profiles
97
+ profiles = []
98
+ defaultProfile = schedule.to_ScheduleRuleset.get.defaultDaySchedule
99
+ profiles << defaultProfile
100
+ rules = schedule.scheduleRules
101
+ rules.each do |rule|
102
+ profiles << rule.daySchedule
103
+ end
104
+
105
+ # test profiles
106
+ min = nil
107
+ max = nil
108
+ profiles.each do |profile|
109
+ profile.values.each do |value|
110
+ if min.nil?
111
+ min = value
112
+ else
113
+ if min > value then min = value end
114
+ end
115
+ if max.nil?
116
+ max = value
117
+ else
118
+ if max < value then max = value end
119
+ end
120
+ end
121
+ end
122
+ result = { 'min' => min, 'max' => max } # this doesn't include summer and winter design day
123
+ else
124
+ result = nil
125
+ end
126
+
127
+ return result
128
+ end
129
+
130
+ # find the maximum profile value for a schedule
131
+ def self.simpleScheduleValueAdjust(model, schedule, double, modificationType = 'Multiplier') # can increase/decrease by percentage or static value
132
+ # TODO: - add in design days, maybe as optional argument
133
+
134
+ # give option to clone or not
135
+
136
+ # gather profiles
137
+ profiles = []
138
+ defaultProfile = schedule.to_ScheduleRuleset.get.defaultDaySchedule
139
+ profiles << defaultProfile
140
+ rules = schedule.scheduleRules
141
+ rules.each do |rule|
142
+ profiles << rule.daySchedule
143
+ end
144
+
145
+ # alter profiles
146
+ profiles.each do |profile|
147
+ times = profile.times
148
+ i = 0
149
+ profile.values.each do |value|
150
+ if modificationType == 'Multiplier' || modificationType == 'Percentage' # percentage was used early on but Multiplier is preferable
151
+ profile.addValue(times[i], value * double)
152
+ end
153
+ if modificationType == 'Sum' || modificationType == 'Value' # value was used early on but Sum is preferable
154
+ profile.addValue(times[i], value + double)
155
+ end
156
+ i += 1
157
+ end
158
+ end
159
+
160
+ result = schedule
161
+ return result
162
+ end
163
+
164
+ # change value when value passes/fails test
165
+ def self.conditionalScheduleValueAdjust(model, schedule, valueTestDouble, passDouble, failDouble, floorDouble, modificationType = 'Multiplier') # can increase/decrease by percentage or static value
166
+ # TODO: - add in design days, maybe as optional argument
167
+
168
+ # give option to clone or not
169
+
170
+ # gather profiles
171
+ profiles = []
172
+ defaultProfile = schedule.to_ScheduleRuleset.get.defaultDaySchedule
173
+ profiles << defaultProfile
174
+ rules = schedule.scheduleRules
175
+ rules.each do |rule|
176
+ profiles << rule.daySchedule
177
+ end
178
+
179
+ # alter profiles
180
+ profiles.each do |profile|
181
+ times = profile.times
182
+ i = 0
183
+
184
+ profile.values.each do |value|
185
+ # run test on this value
186
+ if value < valueTestDouble
187
+ double = passDouble
188
+ else
189
+ double = failDouble
190
+ end
191
+
192
+ # skip if value is floor or less
193
+ next if value <= floorDouble
194
+
195
+ if modificationType == 'Multiplier'
196
+ profile.addValue(times[i], [value * double, floorDouble].max) # take the max of the floor or resulting value
197
+ end
198
+ if modificationType == 'Sum'
199
+ profile.addValue(times[i], [value + double, floorDouble].max) # take the max of the floor or resulting value
200
+ end
201
+ i += 1
202
+ end
203
+ end
204
+
205
+ result = schedule
206
+ return result
207
+ end
208
+
209
+ # change value when time passes test
210
+ def self.timeConditionalScheduleValueAdjust(model, schedule, hhmm_before, hhmm__after, inside_double, outside_double, modificationType = 'Multiplier') # can increase/decrease by percentage or static value
211
+ # setup variables
212
+ array = hhmm_before.to_s.split('')
213
+ before_hour = "#{array[0]}#{array[1]}".to_i
214
+ before_min = "#{array[2]}#{array[3]}".to_i
215
+ array = hhmm__after.to_s.split('')
216
+ after_hour = "#{array[0]}#{array[1]}".to_i
217
+ after_min = "#{array[2]}#{array[3]}".to_i
218
+
219
+ # gather profiles
220
+ profiles = []
221
+ schedule = schedule.to_ScheduleRuleset.get
222
+ defaultProfile = schedule.defaultDaySchedule
223
+ profiles << defaultProfile
224
+ rules = schedule.scheduleRules
225
+ rules.each do |rule|
226
+ profiles << rule.daySchedule
227
+ end
228
+
229
+ # alter profiles
230
+ profiles.each do |day_sch|
231
+ times = day_sch.times
232
+ i = 0
233
+
234
+ # set times special times needed for methods below
235
+ before_time = OpenStudio::Time.new(0, before_hour, before_min, 0)
236
+ after_time = OpenStudio::Time.new(0, after_hour, after_min, 0)
237
+ # day_end_time = OpenStudio::Time.new(0, 24, 0, 0)
238
+
239
+ # add datapoint at before and after time
240
+ original_value_at_before_time = day_sch.getValue(before_time)
241
+ original_value_at_after_time = day_sch.getValue(after_time)
242
+ day_sch.addValue(before_time, original_value_at_before_time)
243
+ day_sch.addValue(after_time, original_value_at_after_time)
244
+
245
+ # make arrays for original times and values
246
+ times = day_sch.times
247
+ values = day_sch.values
248
+ day_sch.clearValues
249
+
250
+ # make arrays for new values
251
+ new_times = []
252
+ new_values = []
253
+
254
+ # loop through original time/value pairs to populate new array
255
+ for i in 0..(values.length - 1)
256
+ new_times << times[i]
257
+
258
+ if times[i] > before_time && times[i] <= after_time # updated this so times[i] == before_time goes into the else
259
+ if inside_double.nil?
260
+ new_values << values[i]
261
+ elsif modificationType == 'Sum'
262
+ new_values << inside_double + values[i]
263
+ elsif modificationType == 'Replace'
264
+ new_values << inside_double
265
+ else # should be Multiplier
266
+ new_values << inside_double * values[i]
267
+ end
268
+ else
269
+ if outside_double.nil?
270
+ new_values << values[i]
271
+ elsif modificationType == 'Sum'
272
+ new_values << outside_double + values[i]
273
+ elsif modificationType == 'Replace'
274
+ new_values << outside_double
275
+ else # should be Multiplier
276
+ new_values << outside_double * values[i]
277
+ end
278
+ end
279
+
280
+ end
281
+
282
+ # generate new day_sch values
283
+ for i in 0..(new_values.length - 1)
284
+ day_sch.addValue(new_times[i], new_values[i])
285
+ end
286
+ end
287
+
288
+ result = schedule
289
+ return result
290
+ end
291
+
292
+ # merge multiple schedules into one using load or other value to weight each schedules influence on the merge
293
+ def self.weightedMergeScheduleRulesets(model, scheduleWeighHash)
294
+ # WARNING NOT READY FOR GENERAL USE YET - this doesn't do anything with rules yet, just winter, summer, and default profile
295
+
296
+ # get denominator for weight
297
+ denominator = 0
298
+ scheduleWeighHash.each do |schedule, weight|
299
+ denominator += weight
300
+ end
301
+
302
+ # create new schedule
303
+ sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
304
+ sch_ruleset.setName('Merged Schedule') # TODO: - make this optional user argument
305
+
306
+ # create winter design day profile
307
+ winter_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
308
+ sch_ruleset.setWinterDesignDaySchedule(winter_dsn_day)
309
+ winter_dsn_day = sch_ruleset.winterDesignDaySchedule
310
+ winter_dsn_day.setName("#{sch_ruleset.name} Winter Design Day")
311
+
312
+ # create summer design day profile
313
+ summer_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
314
+ sch_ruleset.setSummerDesignDaySchedule(summer_dsn_day)
315
+ summer_dsn_day = sch_ruleset.summerDesignDaySchedule
316
+ summer_dsn_day.setName("#{sch_ruleset.name} Summer Design Day")
317
+
318
+ # create default profile
319
+ default_day = sch_ruleset.defaultDaySchedule
320
+ default_day.setName("#{sch_ruleset.name} Schedule Week Day")
321
+
322
+ # hash of schedule rules
323
+ rulesHash = {} # mon, tue, wed, thur, fri, sat, sun, startDate, endDate
324
+ # to avoid stacking order issues across schedules, I may need to make a rule for each day of the week for each date range
325
+
326
+ scheduleWeighHash.each do |schedule, weight|
327
+ # populate winter design day profile
328
+ oldWinterProfile = schedule.to_ScheduleRuleset.get.winterDesignDaySchedule
329
+ times_final = summer_dsn_day.times
330
+ i = 0
331
+ valueUpdatedArray = []
332
+ # loop through times already in profile and update values
333
+ until i > times_final.size - 1
334
+ value = oldWinterProfile.getValue(times_final[i]) * weight / denominator
335
+ starting_value = winter_dsn_day.getValue(times_final[i])
336
+ winter_dsn_day.addValue(times_final[i], value + starting_value)
337
+ valueUpdatedArray << times_final[i]
338
+ i += 1
339
+ end
340
+ # loop through any new times unique to the current old profile to be merged
341
+ j = 0
342
+ times = oldWinterProfile.times
343
+ values = oldWinterProfile.values
344
+ until j > times.size - 1
345
+ unless valueUpdatedArray.include? times[j]
346
+ value = values[j] * weight / denominator
347
+ starting_value = winter_dsn_day.getValue(times[j])
348
+ winter_dsn_day.addValue(times[j], value + starting_value)
349
+ end
350
+ j += 1
351
+ end
352
+
353
+ # populate summer design day profile
354
+ oldSummerProfile = schedule.to_ScheduleRuleset.get.summerDesignDaySchedule
355
+ times_final = summer_dsn_day.times
356
+ i = 0
357
+ valueUpdatedArray = []
358
+ # loop through times already in profile and update values
359
+ until i > times_final.size - 1
360
+ value = oldSummerProfile.getValue(times_final[i]) * weight / denominator
361
+ starting_value = summer_dsn_day.getValue(times_final[i])
362
+ summer_dsn_day.addValue(times_final[i], value + starting_value)
363
+ valueUpdatedArray << times_final[i]
364
+ i += 1
365
+ end
366
+ # loop through any new times unique to the current old profile to be merged
367
+ j = 0
368
+ times = oldSummerProfile.times
369
+ values = oldSummerProfile.values
370
+ until j > times.size - 1
371
+ unless valueUpdatedArray.include? times[j]
372
+ value = values[j] * weight / denominator
373
+ starting_value = summer_dsn_day.getValue(times[j])
374
+ summer_dsn_day.addValue(times[j], value + starting_value)
375
+ end
376
+ j += 1
377
+ end
378
+
379
+ # populate default profile
380
+ oldDefaultProfile = schedule.to_ScheduleRuleset.get.defaultDaySchedule
381
+ times_final = default_day.times
382
+ i = 0
383
+ valueUpdatedArray = []
384
+ # loop through times already in profile and update values
385
+ until i > times_final.size - 1
386
+ value = oldDefaultProfile.getValue(times_final[i]) * weight / denominator
387
+ starting_value = default_day.getValue(times_final[i])
388
+ default_day.addValue(times_final[i], value + starting_value)
389
+ valueUpdatedArray << times_final[i]
390
+ i += 1
391
+ end
392
+ # loop through any new times unique to the current old profile to be merged
393
+ j = 0
394
+ times = oldDefaultProfile.times
395
+ values = oldDefaultProfile.values
396
+ until j > times.size - 1
397
+ unless valueUpdatedArray.include? times[j]
398
+ value = values[j] * weight / denominator
399
+ starting_value = default_day.getValue(times[j])
400
+ default_day.addValue(times[j], value + starting_value)
401
+ end
402
+ j += 1
403
+ end
404
+
405
+ # create rules
406
+
407
+ # gather data for rule profiles
408
+
409
+ # populate rule profiles
410
+ end
411
+
412
+ result = { 'mergedSchedule' => sch_ruleset, 'denominator' => denominator }
413
+ return result
414
+ end
415
+
416
+ # create a new schedule using absolute velocity of existing schedule
417
+ def self.scheduleFromRateOfChange(model, schedule)
418
+ # clone source schedule
419
+ newSchedule = schedule.clone(model)
420
+ newSchedule.setName("#{schedule.name} - Rate of Change")
421
+ newSchedule = newSchedule.to_ScheduleRuleset.get
422
+
423
+ # create array of all profiles to change. This includes summer, winter, default, and rules
424
+ profiles = []
425
+ profiles << newSchedule.winterDesignDaySchedule
426
+ profiles << newSchedule.summerDesignDaySchedule
427
+ profiles << newSchedule.defaultDaySchedule
428
+
429
+ # time values may need
430
+ endProfileTime = OpenStudio::Time.new(0, 24, 0, 0)
431
+ hourBumpTime = OpenStudio::Time.new(0, 1, 0, 0)
432
+ oneHourLeftTime = OpenStudio::Time.new(0, 23, 0, 0)
433
+
434
+ rules = newSchedule.scheduleRules
435
+ rules.each do |rule|
436
+ profiles << rule.daySchedule
437
+ end
438
+
439
+ profiles.uniq.each do |profile|
440
+ times = profile.times
441
+ values = profile.values
442
+
443
+ i = 0
444
+ valuesIntermediate = []
445
+ timesIntermediate = []
446
+ until i == values.size
447
+ if i == 0
448
+ valuesIntermediate << 0.0
449
+ if times[i] > hourBumpTime
450
+ timesIntermediate << times[i] - hourBumpTime
451
+ if times[i + 1].nil?
452
+ timeStepValue = endProfileTime.hours + endProfileTime.minutes / 60 - times[i].hours - times[i].minutes / 60
453
+ else
454
+ timeStepValue = times[i + 1].hours + times[i + 1].minutes / 60 - times[i].hours - times[i].minutes / 60
455
+ end
456
+ valuesIntermediate << (values[i + 1].to_f - values[i].to_f).abs / (timeStepValue * 2)
457
+ end
458
+ timesIntermediate << times[i]
459
+ elsif i == (values.size - 1)
460
+ if times[times.size - 2] < oneHourLeftTime
461
+ timesIntermediate << times[times.size - 2] + hourBumpTime # this should be the second to last time
462
+ timeStepValue = times[i - 1].hours + times[i - 1].minutes / 60 - times[i - 2].hours - times[i - 2].minutes / 60
463
+ valuesIntermediate << (values[i - 1].to_f - values[i - 2].to_f).abs / (timeStepValue * 2)
464
+ end
465
+ valuesIntermediate << 0.0
466
+ timesIntermediate << times[i] # this should be the last time
467
+ else
468
+ # get value multiplier based on how many hours it is spread over
469
+ timeStepValue = times[i].hours + times[i].minutes / 60 - times[i - 1].hours - times[i - 1].minutes / 60
470
+ valuesIntermediate << (values[i].to_f - values[i - 1].to_f).abs / timeStepValue
471
+ timesIntermediate << times[i]
472
+ end
473
+ i += 1
474
+ end
475
+
476
+ # delete all profile values
477
+ profile.clearValues
478
+
479
+ i = 0
480
+ until i == timesIntermediate.size
481
+ if i == (timesIntermediate.size - 1)
482
+ profile.addValue(timesIntermediate[i], valuesIntermediate[i].to_f)
483
+ else
484
+ profile.addValue(timesIntermediate[i], valuesIntermediate[i].to_f)
485
+ end
486
+ i += 1
487
+ end
488
+ end
489
+
490
+ # fix velocity so it isn't fraction change per step, but per hour (I need to count hours between times and divide value by this)
491
+
492
+ result = newSchedule
493
+ return result
494
+ end
495
+
496
+ # create a complex ruleset schedule
497
+ def self.createComplexSchedule(model, options = {})
498
+ defaults = {
499
+ 'name' => nil,
500
+ 'default_day' => ['always_on', [24.0, 1.0]]
501
+ }
502
+
503
+ # merge user inputs with defaults
504
+ options = defaults.merge(options)
505
+
506
+ # ScheduleRuleset
507
+ sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
508
+ if name
509
+ sch_ruleset.setName(options['name'])
510
+ end
511
+
512
+ # Winter Design Day
513
+ unless options['winter_design_day'].nil?
514
+ winter_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
515
+ sch_ruleset.setWinterDesignDaySchedule(winter_dsn_day)
516
+ winter_dsn_day = sch_ruleset.winterDesignDaySchedule
517
+ winter_dsn_day.setName("#{sch_ruleset.name} Winter Design Day")
518
+ options['winter_design_day'].each do |data_pair|
519
+ hour = data_pair[0].truncate
520
+ min = ((data_pair[0] - hour) * 60).to_i
521
+ winter_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
522
+ end
523
+ end
524
+
525
+ # Summer Design Day
526
+ unless options['summer_design_day'].nil?
527
+ summer_dsn_day = OpenStudio::Model::ScheduleDay.new(model)
528
+ sch_ruleset.setSummerDesignDaySchedule(summer_dsn_day)
529
+ summer_dsn_day = sch_ruleset.summerDesignDaySchedule
530
+ summer_dsn_day.setName("#{sch_ruleset.name} Summer Design Day")
531
+ options['summer_design_day'].each do |data_pair|
532
+ hour = data_pair[0].truncate
533
+ min = ((data_pair[0] - hour) * 60).to_i
534
+ summer_dsn_day.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
535
+ end
536
+ end
537
+
538
+ # Default Day
539
+ default_day = sch_ruleset.defaultDaySchedule
540
+ default_day.setName("#{sch_ruleset.name} #{options['default_day'][0]}")
541
+ default_data_array = options['default_day']
542
+ default_data_array.delete_at(0)
543
+ default_data_array.each do |data_pair|
544
+ hour = data_pair[0].truncate
545
+ min = ((data_pair[0] - hour) * 60).to_i
546
+ default_day.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
547
+ end
548
+
549
+ # Rules
550
+ unless options['rules'].nil?
551
+ options['rules'].each do |data_array|
552
+ rule = OpenStudio::Model::ScheduleRule.new(sch_ruleset)
553
+ rule.setName("#{sch_ruleset.name} #{data_array[0]} Rule")
554
+ date_range = data_array[1].split('-')
555
+ start_date = date_range[0].split('/')
556
+ end_date = date_range[1].split('/')
557
+ rule.setStartDate(model.getYearDescription.makeDate(start_date[0].to_i, start_date[1].to_i))
558
+ rule.setEndDate(model.getYearDescription.makeDate(end_date[0].to_i, end_date[1].to_i))
559
+ days = data_array[2].split('/')
560
+ rule.setApplySunday(true) if days.include? 'Sun'
561
+ rule.setApplyMonday(true) if days.include? 'Mon'
562
+ rule.setApplyTuesday(true) if days.include? 'Tue'
563
+ rule.setApplyWednesday(true) if days.include? 'Wed'
564
+ rule.setApplyThursday(true) if days.include? 'Thu'
565
+ rule.setApplyFriday(true) if days.include? 'Fri'
566
+ rule.setApplySaturday(true) if days.include? 'Sat'
567
+ day_schedule = rule.daySchedule
568
+ day_schedule.setName("#{sch_ruleset.name} #{data_array[0]}")
569
+ data_array.delete_at(0)
570
+ data_array.delete_at(0)
571
+ data_array.delete_at(0)
572
+ data_array.each do |data_pair|
573
+ hour = data_pair[0].truncate
574
+ min = ((data_pair[0] - hour) * 60).to_i
575
+ day_schedule.addValue(OpenStudio::Time.new(0, hour, min, 0), data_pair[1])
576
+ end
577
+ end
578
+ end
579
+
580
+ result = sch_ruleset
581
+ return result
582
+ end
583
+
584
+ def self.addScheduleTypeLimits(model) # TODO: - make sure to add this new method to cofee when done
585
+ type_limits = {}
586
+
587
+ lightsScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
588
+ lightsScheduleTypeLimits.setName('Lights Schedule Type Limits')
589
+ lightsScheduleTypeLimits.setLowerLimitValue(0.0)
590
+ lightsScheduleTypeLimits.setUpperLimitValue(1.0)
591
+ lightsScheduleTypeLimits.setNumericType('Continuous')
592
+ lightsScheduleTypeLimits.setUnitType('Dimensionless')
593
+ type_limits['Lights'] = lightsScheduleTypeLimits
594
+
595
+ occupancyScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
596
+ occupancyScheduleTypeLimits.setName('Occupancy Schedule Type Limits')
597
+ occupancyScheduleTypeLimits.setLowerLimitValue(0.0)
598
+ occupancyScheduleTypeLimits.setUpperLimitValue(1.0)
599
+ occupancyScheduleTypeLimits.setNumericType('Continuous')
600
+ occupancyScheduleTypeLimits.setUnitType('Dimensionless')
601
+ type_limits['Occupancy'] = occupancyScheduleTypeLimits
602
+
603
+ peopleActivityScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
604
+ peopleActivityScheduleTypeLimits.setName('People Activity Type Limits')
605
+ peopleActivityScheduleTypeLimits.setLowerLimitValue(0.0)
606
+ # peopleActivityScheduleTypeLimits.setUpperLimitValue(1500.0)
607
+ peopleActivityScheduleTypeLimits.setNumericType('Continuous')
608
+ peopleActivityScheduleTypeLimits.setUnitType('ActivityLevel')
609
+ type_limits['People Activity'] = peopleActivityScheduleTypeLimits
610
+
611
+ equipmentScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
612
+ equipmentScheduleTypeLimits.setName('Equipment Schedule Type Limits')
613
+ equipmentScheduleTypeLimits.setLowerLimitValue(0.0)
614
+ equipmentScheduleTypeLimits.setUpperLimitValue(1.0)
615
+ equipmentScheduleTypeLimits.setNumericType('Continuous')
616
+ equipmentScheduleTypeLimits.setUnitType('Dimensionless')
617
+ type_limits['Equipment'] = equipmentScheduleTypeLimits
618
+
619
+ waterUseScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
620
+ waterUseScheduleTypeLimits.setName('Water Use Schedule Type Limits')
621
+ waterUseScheduleTypeLimits.setLowerLimitValue(0.0)
622
+ waterUseScheduleTypeLimits.setUpperLimitValue(1.0)
623
+ waterUseScheduleTypeLimits.setNumericType('Continuous')
624
+ waterUseScheduleTypeLimits.setUnitType('Dimensionless')
625
+ type_limits['Water Use'] = waterUseScheduleTypeLimits
626
+
627
+ elevatorsScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
628
+ elevatorsScheduleTypeLimits.setName('Elevators Schedule Type Limits')
629
+ elevatorsScheduleTypeLimits.setLowerLimitValue(0.0)
630
+ elevatorsScheduleTypeLimits.setUpperLimitValue(1.0)
631
+ elevatorsScheduleTypeLimits.setNumericType('Continuous')
632
+ elevatorsScheduleTypeLimits.setUnitType('Dimensionless')
633
+ type_limits['Elevators'] = elevatorsScheduleTypeLimits
634
+
635
+ processLoadsScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
636
+ processLoadsScheduleTypeLimits.setName('Process Loads Schedule Type Limits')
637
+ processLoadsScheduleTypeLimits.setLowerLimitValue(0.0)
638
+ processLoadsScheduleTypeLimits.setUpperLimitValue(1.0)
639
+ processLoadsScheduleTypeLimits.setNumericType('Continuous')
640
+ processLoadsScheduleTypeLimits.setUnitType('Dimensionless')
641
+ type_limits['Process Load'] = elevatorsScheduleTypeLimits
642
+
643
+ thermostatHeatingScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
644
+ thermostatHeatingScheduleTypeLimits.setName('Thermostat Heating Setpoint Schedule Type Limits')
645
+ thermostatHeatingScheduleTypeLimits.setLowerLimitValue(0.0)
646
+ thermostatHeatingScheduleTypeLimits.setUpperLimitValue(100.0)
647
+ thermostatHeatingScheduleTypeLimits.setNumericType('Continuous')
648
+ thermostatHeatingScheduleTypeLimits.setUnitType('Temperature')
649
+ type_limits['Thermostat Heating Setpoint'] = thermostatHeatingScheduleTypeLimits
650
+
651
+ temperatureScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
652
+ temperatureScheduleTypeLimits.setName('Thermostat Cooling Setpoint Schedule Type Limits')
653
+ temperatureScheduleTypeLimits.setLowerLimitValue(0.0)
654
+ temperatureScheduleTypeLimits.setUpperLimitValue(100.0)
655
+ temperatureScheduleTypeLimits.setNumericType('Continuous')
656
+ temperatureScheduleTypeLimits.setUnitType('Temperature')
657
+ type_limits['Thermostat Cooling Setpoint'] = temperatureScheduleTypeLimits
658
+
659
+ hvacOperationScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
660
+ hvacOperationScheduleTypeLimits.setName('HVAC Operation Schedule Type Limits')
661
+ hvacOperationScheduleTypeLimits.setLowerLimitValue(0)
662
+ hvacOperationScheduleTypeLimits.setUpperLimitValue(1)
663
+ hvacOperationScheduleTypeLimits.setNumericType('Discrete')
664
+ hvacOperationScheduleTypeLimits.setUnitType('Availability')
665
+ type_limits['HVAC Operation'] = hvacOperationScheduleTypeLimits
666
+
667
+ temperatureScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
668
+ temperatureScheduleTypeLimits.setName('Temperature Schedule Type Limits')
669
+ temperatureScheduleTypeLimits.setNumericType('Continuous')
670
+ temperatureScheduleTypeLimits.setUnitType('Temperature')
671
+ type_limits['Temperature'] = temperatureScheduleTypeLimits
672
+
673
+ fractionScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
674
+ fractionScheduleTypeLimits.setName('Fraction Schedule Type Limits')
675
+ fractionScheduleTypeLimits.setLowerLimitValue(0.0)
676
+ fractionScheduleTypeLimits.setUpperLimitValue(1.0)
677
+ fractionScheduleTypeLimits.setNumericType('Continuous')
678
+ fractionScheduleTypeLimits.setUnitType('Dimensionless')
679
+ type_limits['Fraction'] = fractionScheduleTypeLimits
680
+
681
+ dimensionlessScheduleTypeLimits = OpenStudio::Model::ScheduleTypeLimits.new(model)
682
+ dimensionlessScheduleTypeLimits.setName('Dimensionless Schedule Type Limits')
683
+ dimensionlessScheduleTypeLimits.setNumericType('Continuous')
684
+ dimensionlessScheduleTypeLimits.setUnitType('Dimensionless')
685
+ type_limits['Dimensionless'] = dimensionlessScheduleTypeLimits
686
+
687
+ return type_limits
688
+ end
689
+
690
+ # create TimeSeries from ScheduleRuleset
691
+ def self.create_timeseries_from_schedule_ruleset(model, schedule_ruleset)
692
+ yd = model.getYearDescription
693
+ start_date = yd.makeDate(1, 1)
694
+ end_date = yd.makeDate(12, 31)
695
+
696
+ values = OpenStudio::DoubleVector.new
697
+ day = OpenStudio::Time.new(1.0)
698
+ interval = OpenStudio::Time.new(1.0 / 48.0)
699
+ day_schedules = schedule_ruleset.to_ScheduleRuleset.get.getDaySchedules(start_date, end_date)
700
+ day_schedules.each do |day_schedule|
701
+ time = interval
702
+ while time < day
703
+ values << day_schedule.getValue(time)
704
+ time += interval
705
+ end
706
+ end
707
+ time_series = OpenStudio::TimeSeries.new(start_date, interval, OpenStudio.createVector(values), '')
708
+ end
709
+
710
+ # create ScheduleVariableInterval from TimeSeries
711
+ def self.create_schedule_variable_interval_from_time_series(model, time_series)
712
+ result = OpenStudio::Model::ScheduleInterval.fromTimeSeries(time_series, model).get
713
+ end
714
+
715
+ def self.adjust_hours_of_operation_for_schedule_ruleset(runner, model, schedule, options = {})
716
+ defaults = {
717
+ 'base_start_hoo' => 8.0, # may not be good idea to have default
718
+ 'base_finish_hoo' => 18.0, # may not be good idea to have default
719
+ 'delta_length_hoo' => 0.0,
720
+ 'shift_hoo' => 0.0,
721
+ 'default' => true,
722
+ 'mon' => true,
723
+ 'tue' => true,
724
+ 'wed' => true,
725
+ 'thur' => true,
726
+ 'fri' => true,
727
+ 'sat' => true,
728
+ 'sun' => true,
729
+ 'summer' => false,
730
+ 'winter' => false
731
+ }
732
+
733
+ # merge user inputs with defaults
734
+ options = defaults.merge(options)
735
+
736
+ # grab schedule out of argument
737
+ if schedule.to_ScheduleRuleset.is_initialized
738
+ schedule = schedule.to_ScheduleRuleset.get
739
+ else
740
+ runner.registerWarning("you should only pass ruleset schedules into this method. skipping #{schedule.name}")
741
+ return nil
742
+ end
743
+
744
+ # array of all profiles to change
745
+ profiles = []
746
+
747
+ # push default profiles to array
748
+ if options['default']
749
+ default_rule = schedule.defaultDaySchedule
750
+ profiles << default_rule
751
+ end
752
+
753
+ # push profiles to array
754
+ rules = schedule.scheduleRules
755
+ rules.each do |rule|
756
+ day_sch = rule.daySchedule
757
+
758
+ # if any day requested also exists in the rule, then it will be altered
759
+ alter_rule = false
760
+ if rule.applyMonday && rule.applyMonday == options['mon'] then alter_rule = true end
761
+ if rule.applyTuesday && rule.applyTuesday == options['tue'] then alter_rule = true end
762
+ if rule.applyWednesday && rule.applyWednesday == options['wed'] then alter_rule = true end
763
+ if rule.applyThursday && rule.applyThursday == options['thur'] then alter_rule = true end
764
+ if rule.applyFriday && rule.applyFriday == options['fri'] then alter_rule = true end
765
+ if rule.applySaturday && rule.applySaturday == options['sat'] then alter_rule = true end
766
+ if rule.applySunday && rule.applySunday == options['sun'] then alter_rule = true end
767
+
768
+ # TODO: - add in logic to warn user about conflicts where a single rule has conflicting tests
769
+
770
+ if alter_rule
771
+ profiles << day_sch
772
+ end
773
+ end
774
+
775
+ # add design days to array
776
+ if options['summer']
777
+ summer_design = schedule.summerDesignDaySchedule
778
+ profiles << summer_design
779
+ end
780
+ if options['winter']
781
+ winter_design = schedule.winterDesignDaySchedule
782
+ profiles << winter_design
783
+ end
784
+
785
+ # give info messages as I change specific profiles
786
+ runner.registerInfo("Adjusting #{schedule.name}")
787
+
788
+ # rename schedule
789
+ schedule.setName("#{schedule.name} - extend #{options['delta_length_hoo']} shift #{options['shift_hoo']}") # if I put inputs here name will get long
790
+
791
+ # break time args into hours and minutes
792
+ start_hoo_hours = (options['base_start_hoo']).to_i
793
+ start_hoo_minutes = (((options['base_start_hoo']) - (options['base_start_hoo']).to_i) * 60).to_i
794
+ finish_hoo_hours = (options['base_finish_hoo']).to_i
795
+ finish_hoo_minutes = (((options['base_finish_hoo']) - (options['base_finish_hoo']).to_i) * 60).to_i
796
+ delta_hours = (options['delta_length_hoo']).to_i
797
+ delta_minutes = (((options['delta_length_hoo']) - (options['delta_length_hoo']).to_i) * 60).to_i
798
+ shift_hours = (options['shift_hoo']).to_i
799
+ shift_minutes = (((options['shift_hoo']) - (options['shift_hoo']).to_i) * 60).to_i
800
+
801
+ # time objects to use in measure
802
+ time_0 = OpenStudio::Time.new(0, 0, 0, 0)
803
+ time_1_min = OpenStudio::Time.new(0, 0, 1, 0) # add this to avoid times in day profile less than this
804
+ time_12 = OpenStudio::Time.new(0, 12, 0, 0)
805
+ time_24 = OpenStudio::Time.new(0, 24, 0, 0)
806
+ start_hoo_time = OpenStudio::Time.new(0, start_hoo_hours, start_hoo_minutes, 0)
807
+ finish_hoo_time = OpenStudio::Time.new(0, finish_hoo_hours, finish_hoo_minutes, 0)
808
+ delta_time = OpenStudio::Time.new(0, delta_hours, delta_minutes, 0) # not used
809
+ shift_time = OpenStudio::Time.new(0, shift_hours, shift_minutes, 0)
810
+
811
+ # calculations
812
+ if options['base_start_hoo'] <= options['base_finish_hoo']
813
+ base_opp_day_length = options['base_finish_hoo'] - options['base_start_hoo']
814
+ mid_hoo = start_hoo_time + (finish_hoo_time - start_hoo_time) / 2
815
+ mid_non_hoo = mid_hoo + time_12
816
+ if mid_non_hoo > time_24 then mid_non_hoo -= time_24 end
817
+ else
818
+ base_opp_day_length = options['base_finish_hoo'] - options['base_start_hoo'] + 24
819
+ mid_non_hoo = finish_hoo_time + (start_hoo_time - finish_hoo_time) / 2
820
+ mid_hoo = mid_non_hoo + time_12
821
+ if mid_non_hoo > time_24 then mid_non_hoo -= time_24 end
822
+ end
823
+ adjusted_opp_day_length = base_opp_day_length + options['delta_length_hoo']
824
+ hoo_time_multiplier = adjusted_opp_day_length / base_opp_day_length
825
+ non_hoo_time_multiplier = (24 - adjusted_opp_day_length) / (24 - base_opp_day_length)
826
+
827
+ # check for invalid input
828
+ if adjusted_opp_day_length < 0
829
+ runner.registerError('Requested hours of operation adjustment results in an invalid negative hours of operation')
830
+ return false
831
+ end
832
+ # check for invalid input
833
+ if adjusted_opp_day_length > 24
834
+ runner.registerError('Requested hours of operation adjustment results in more than 24 hours of operation')
835
+ return false
836
+ end
837
+
838
+ # making some temp objects to avoid having to deal with wrap around for change of hoo times
839
+ mid_hoo < start_hoo_time ? (adj_mid_hoo = mid_hoo + time_24) : (adj_mid_hoo = mid_hoo)
840
+ finish_hoo_time < adj_mid_hoo ? (adj_finish_hoo_time = finish_hoo_time + time_24) : (adj_finish_hoo_time = finish_hoo_time)
841
+ mid_non_hoo < adj_finish_hoo_time ? (adj_mid_non_hoo = mid_non_hoo + time_24) : (adj_mid_non_hoo = mid_non_hoo)
842
+ adj_start = start_hoo_time + time_24 # not used
843
+
844
+ # edit profiles
845
+ profiles.each do |day_sch|
846
+ times = day_sch.times
847
+ values = day_sch.values
848
+
849
+ # in this case delete all values outside of
850
+ # todo - may need similar logic if exactly 0 hours
851
+ if adjusted_opp_day_length == 24
852
+ start_val = day_sch.getValue(start_hoo_time)
853
+ finish_val = day_sch.getValue(finish_hoo_time)
854
+
855
+ # remove times out of range that should not be reference or compressed
856
+ if start_hoo_time < finish_hoo_time
857
+ times.each do |time|
858
+ if time <= start_hoo_time || time > finish_hoo_time
859
+ day_sch.removeValue(time)
860
+ end
861
+ end
862
+ # add in values
863
+ day_sch.addValue(start_hoo_time,start_val)
864
+ day_sch.addValue(finish_hoo_time,finish_val)
865
+ day_sch.addValue(time_24,[start_val,finish_val].max)
866
+ else
867
+ times.each do |time|
868
+ if time > start_hoo_time && time <= finish_hoo_time
869
+ day_sch.removeValue(time)
870
+ end
871
+ end
872
+ # add in values
873
+ day_sch.addValue(finish_hoo_time,finish_val)
874
+ day_sch.addValue(start_hoo_time,start_val)
875
+ day_sch.addValue(time_24,[values.first,values.last].max)
876
+ end
877
+
878
+ end
879
+
880
+ times = day_sch.times
881
+ values = day_sch.values
882
+
883
+ # arrays for values to avoid overlap conflict of times
884
+ new_times = []
885
+ new_values = []
886
+
887
+ # this is to store what datapoint will be first after midnight, and what the value at that time should be
888
+ min_time_new = time_24
889
+ min_time_value = nil
890
+
891
+ # flag if found time at 24
892
+ found_24_or_0 = false
893
+
894
+ # push times to array
895
+ times.each do |time|
896
+ # create logic for four possible quadrants. Assume any quadrant can pass over 24/0 threshold
897
+ time < start_hoo_time ? (temp_time = time + time_24) : (temp_time = time)
898
+
899
+ # calculate change in time do to hoo delta
900
+ if temp_time <= adj_finish_hoo_time
901
+ expand_time = (temp_time - adj_mid_hoo) * hoo_time_multiplier - (temp_time - adj_mid_hoo)
902
+ else
903
+ expand_time = (temp_time - adj_mid_non_hoo) * non_hoo_time_multiplier - (temp_time - adj_mid_non_hoo)
904
+ end
905
+
906
+ new_time = time + shift_time + expand_time
907
+
908
+ # adjust wrap around times
909
+ if new_time < time_0
910
+ new_time += time_24
911
+ elsif new_time > time_24
912
+ new_time -= time_24
913
+ end
914
+ new_times << new_time
915
+
916
+ # see which new_time has the lowest value. Then add a value at 24 equal to that
917
+ if !found_24_or_0 && new_time <= min_time_new
918
+ min_time_new = new_time
919
+ min_time_value = day_sch.getValue(time)
920
+ elsif new_time == time_24 # this was added to address time exactly at 24
921
+ min_time_new = new_time
922
+ min_time_value = day_sch.getValue(time)
923
+ found_24_or_0 = true
924
+ elsif new_time == time_0
925
+ min_time_new = new_time
926
+ min_time_value = day_sch.getValue(time_0)
927
+ found_24_or_0 = true
928
+ end
929
+ end
930
+
931
+ # push values to array
932
+ values.each do |value|
933
+ new_values << value
934
+ end
935
+
936
+ # add value for what will be 24
937
+ new_times << time_24
938
+ new_values << min_time_value
939
+
940
+ new_time_val_hash = {}
941
+ new_times.each_with_index do |time,i|
942
+ new_time_val_hash[time.totalHours] = {:time => time, :value => new_values[i]}
943
+ end
944
+
945
+ # clear values
946
+ day_sch.clearValues
947
+
948
+ new_time_val_hash = Hash[new_time_val_hash.sort]
949
+ prev_time = nil
950
+ new_time_val_hash.sort.each do |hours,time_val|
951
+ if prev_time.nil? || time_val[:time] - prev_time > time_1_min
952
+ day_sch.addValue(time_val[:time], time_val[:value])
953
+ prev_time = time_val[:time]
954
+ else
955
+ puts "time step in #{day_sch.name} between #{prev_time.toString} and #{time_val[:time].toString} is too small to support, not adding value"
956
+ end
957
+ end
958
+
959
+ end
960
+
961
+ return schedule
962
+ end
963
+ end