openstudio-extension 0.1.0 → 0.1.1

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 (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