openstudio-extension 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +29 -26
  3. data/.rspec +3 -3
  4. data/.rubocop.yml +10 -9
  5. data/CHANGELOG.md +16 -0
  6. data/Gemfile +6 -6
  7. data/Jenkinsfile +10 -10
  8. data/README.md +251 -250
  9. data/Rakefile +119 -119
  10. data/bin/console +14 -14
  11. data/bin/setup +8 -8
  12. data/doc_templates/LICENSE.md +26 -26
  13. data/doc_templates/README.md.erb +41 -41
  14. data/doc_templates/copyright_erb.txt +35 -35
  15. data/doc_templates/copyright_js.txt +3 -3
  16. data/doc_templates/copyright_ruby.txt +33 -33
  17. data/init_templates/README.md +37 -37
  18. data/init_templates/gemspec.txt +32 -32
  19. data/init_templates/openstudio_module.rb +50 -50
  20. data/init_templates/spec.rb +47 -47
  21. data/init_templates/spec_helper.rb +49 -49
  22. data/init_templates/template_gemfile.txt +17 -17
  23. data/init_templates/template_rakefile.txt +15 -15
  24. data/init_templates/version.rb +40 -40
  25. data/lib/files/openstudio-extension-gem-test.ddy +536 -536
  26. data/lib/files/openstudio-extension-gem-test.epw +8768 -8768
  27. data/lib/files/openstudio-extension-gem-test.stat +554 -554
  28. data/lib/measures/openstudio_extension_test_measure/LICENSE.md +26 -26
  29. data/lib/measures/openstudio_extension_test_measure/README.md +26 -26
  30. data/lib/measures/openstudio_extension_test_measure/README.md.erb +41 -41
  31. data/lib/measures/openstudio_extension_test_measure/measure.rb +72 -72
  32. data/lib/measures/openstudio_extension_test_measure/measure.xml +83 -83
  33. data/lib/measures/openstudio_extension_test_measure/resources/os_lib_helper_methods.rb +399 -399
  34. data/lib/measures/openstudio_extension_test_measure/tests/{OpenStudioExtensionTestMeasure_Test.rb → openstudio_extension_test_measure_test.rb} +74 -75
  35. data/lib/openstudio-extension.rb +1 -1
  36. data/lib/openstudio/extension.rb +234 -229
  37. data/lib/openstudio/extension/core/CreateResults.rb +886 -886
  38. data/lib/openstudio/extension/core/check_air_sys_temps.rb +190 -190
  39. data/lib/openstudio/extension/core/check_calibration.rb +155 -155
  40. data/lib/openstudio/extension/core/check_cond_zns.rb +84 -84
  41. data/lib/openstudio/extension/core/check_domestic_hot_water.rb +334 -334
  42. data/lib/openstudio/extension/core/check_envelope_conductance.rb +453 -453
  43. data/lib/openstudio/extension/core/check_eui_by_end_use.rb +162 -162
  44. data/lib/openstudio/extension/core/check_eui_reasonableness.rb +135 -135
  45. data/lib/openstudio/extension/core/check_fan_pwr.rb +98 -98
  46. data/lib/openstudio/extension/core/check_internal_loads.rb +393 -393
  47. data/lib/openstudio/extension/core/check_mech_sys_capacity.rb +226 -226
  48. data/lib/openstudio/extension/core/check_mech_sys_efficiency.rb +326 -326
  49. data/lib/openstudio/extension/core/check_mech_sys_part_load_eff.rb +464 -464
  50. data/lib/openstudio/extension/core/check_mech_sys_type.rb +139 -139
  51. data/lib/openstudio/extension/core/check_part_loads.rb +451 -451
  52. data/lib/openstudio/extension/core/check_placeholder.rb +75 -75
  53. data/lib/openstudio/extension/core/check_plant_cap.rb +123 -123
  54. data/lib/openstudio/extension/core/check_plant_temps.rb +159 -159
  55. data/lib/openstudio/extension/core/check_plenum_loads.rb +87 -87
  56. data/lib/openstudio/extension/core/check_pump_pwr.rb +108 -108
  57. data/lib/openstudio/extension/core/check_sch_coord.rb +241 -241
  58. data/lib/openstudio/extension/core/check_schedules.rb +311 -311
  59. data/lib/openstudio/extension/core/check_simultaneous_heating_and_cooling.rb +158 -158
  60. data/lib/openstudio/extension/core/check_supply_air_and_thermostat_temp_difference.rb +148 -148
  61. data/lib/openstudio/extension/core/check_weather_files.rb +132 -132
  62. data/lib/openstudio/extension/core/deer_vintages.rb +311 -311
  63. data/lib/openstudio/extension/core/os_lib_aedg_measures.rb +491 -491
  64. data/lib/openstudio/extension/core/os_lib_cofee.rb +258 -258
  65. data/lib/openstudio/extension/core/os_lib_constructions.rb +378 -378
  66. data/lib/openstudio/extension/core/os_lib_geometry.rb +1022 -1022
  67. data/lib/openstudio/extension/core/os_lib_helper_methods.rb +399 -399
  68. data/lib/openstudio/extension/core/os_lib_hvac.rb +2171 -2171
  69. data/lib/openstudio/extension/core/os_lib_lighting_and_equipment.rb +214 -214
  70. data/lib/openstudio/extension/core/os_lib_model_generation.rb +817 -817
  71. data/lib/openstudio/extension/core/os_lib_model_simplification.rb +1049 -1049
  72. data/lib/openstudio/extension/core/os_lib_outdoorair_and_infiltration.rb +165 -165
  73. data/lib/openstudio/extension/core/os_lib_reporting.rb +4651 -4651
  74. data/lib/openstudio/extension/core/os_lib_reporting_qaqc.rb +200 -200
  75. data/lib/openstudio/extension/core/os_lib_schedules.rb +963 -963
  76. data/lib/openstudio/extension/rake_task.rb +159 -149
  77. data/lib/openstudio/extension/runner.rb +667 -644
  78. data/lib/openstudio/extension/runner_config.rb +114 -0
  79. data/lib/openstudio/extension/version.rb +40 -40
  80. data/openstudio-extension.gemspec +34 -33
  81. metadata +39 -37
@@ -1,200 +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
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
@@ -1,963 +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
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