openstudio-load-flexibility-measures 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +36 -0
  4. data/.rubocop.yml +9 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +3 -0
  7. data/README.md +42 -0
  8. data/Rakefile +15 -0
  9. data/doc_templates/LICENSE.md +27 -0
  10. data/doc_templates/README.md.erb +42 -0
  11. data/doc_templates/copyright_erb.txt +36 -0
  12. data/doc_templates/copyright_js.txt +4 -0
  13. data/doc_templates/copyright_ruby.txt +34 -0
  14. data/lib/measures/add_central_hpwh_for_load_flexibility/LICENSE.md +1 -0
  15. data/lib/measures/add_central_hpwh_for_load_flexibility/README.md +186 -0
  16. data/lib/measures/add_central_hpwh_for_load_flexibility/README.md.erb +42 -0
  17. data/lib/measures/add_central_hpwh_for_load_flexibility/docs/Flexible Domestic Hot Water Implementation Guide.pdf +0 -0
  18. data/lib/measures/add_central_hpwh_for_load_flexibility/measure.rb +648 -0
  19. data/lib/measures/add_central_hpwh_for_load_flexibility/measure.xml +398 -0
  20. data/lib/measures/add_central_hpwh_for_load_flexibility/tests/SmallHotel-2A.osm +42893 -0
  21. data/lib/measures/add_central_hpwh_for_load_flexibility/tests/add_central_hpwh_for_load_flexibility.rb +98 -0
  22. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/LICENSE.md +13 -0
  23. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/README.md +189 -0
  24. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/measure.rb +689 -0
  25. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/measure.xml +253 -0
  26. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/resources/TESCurves.idf +1059 -0
  27. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/tests/MeasureTest.osm +9507 -0
  28. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/tests/add_distributed_ice_storage_to_air_loop_for_load_flexibility_test.rb +96 -0
  29. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/LICENSE.md +13 -0
  30. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/README.md +264 -0
  31. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/README.md.erb +42 -0
  32. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/docs/Ice Measure Implementation Guide.pdf +0 -0
  33. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/measure.rb +1310 -0
  34. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/measure.xml +506 -0
  35. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/resources/OsLib_Schedules.rb +173 -0
  36. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/tests/add_ice_storage_to_plant_loop_for_load_flexibility_test.rb +202 -0
  37. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/tests/ice_test_model.osm +21523 -0
  38. data/lib/openstudio/load_flexibility_measures.rb +50 -0
  39. data/lib/openstudio/load_flexibility_measures/version.rb +40 -0
  40. data/openstudio-load-flexibility-measures.gemspec +32 -0
  41. metadata +172 -0
@@ -0,0 +1,96 @@
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2020, 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 'openstudio'
37
+ require 'openstudio/measure/ShowRunnerOutput'
38
+ require 'minitest/autorun'
39
+
40
+ require_relative '../measure.rb'
41
+
42
+ class AddDistributedIceStorageToAirLoopForLoadFlexibilityTest < MiniTest::Test
43
+ # def setup
44
+ # end
45
+
46
+ # def teardown
47
+ # end
48
+
49
+ def test_good_argument_values
50
+ # create an instance of the measure
51
+ measure = AddDistributedIceStorageToAirLoopForLoadFlexibility.new
52
+
53
+ # create runner with empty OSW
54
+ osw = OpenStudio::WorkflowJSON.new
55
+ runner = OpenStudio::Measure::OSRunner.new(osw)
56
+
57
+ # load the test model
58
+ translator = OpenStudio::OSVersion::VersionTranslator.new
59
+ path = OpenStudio::Path.new(File.dirname(__FILE__) + '/MeasureTest.osm')
60
+ model = translator.loadModel(path)
61
+ assert(!model.empty?)
62
+ model = model.get
63
+
64
+ # forward translate OSM file to IDF file
65
+ ft = OpenStudio::EnergyPlus::ForwardTranslator.new
66
+ workspace = ft.translateModel(model)
67
+
68
+ # get arguments
69
+ arguments = measure.arguments(workspace)
70
+ argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)
71
+
72
+ # create hash of argument values
73
+ args_hash = {}
74
+ args_hash['ice_cap'] = '40,50'
75
+
76
+ # populate argument with specified has value if set
77
+ arguments.each do |arg|
78
+ temp_arg_var = arg.clone
79
+ assert(temp_arg_var.setValue(args_hash[arg.name])) if args_hash[arg.name]
80
+ argument_map[arg.name] = temp_arg_var
81
+ end
82
+
83
+ # run the measure
84
+ measure.run(workspace, runner, argument_map)
85
+ result = runner.result
86
+ assert_equal('Success', result.value.valueName)
87
+
88
+ show_output(result)
89
+
90
+ # save the workspace to output directory
91
+ output_file_path = OpenStudio::Path.new(File.dirname(__FILE__) + '/output/test_output.idf')
92
+ workspace.save(output_file_path, true)
93
+
94
+ true
95
+ end
96
+ end
@@ -0,0 +1,13 @@
1
+ OpenStudio(R), Copyright (c) 2008-2020, Alliance for Sustainable Energy, LLC. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission from the respective party.
10
+
11
+ (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior written permission from Alliance for Sustainable Energy, LLC.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER, THE UNITED STATES GOVERNMENT, OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,264 @@
1
+
2
+
3
+ ###### (Automatically generated documentation)
4
+
5
+ # Add Ice Storage Tank
6
+
7
+ ## Description
8
+ This measure adds an ice storage tank to a chilled water loop for the purpose of thermal energy storage.
9
+
10
+ ## Modeler Description
11
+ This measure adds the necessary components and performs required model articulations to add an ice thermal storage tank (ITS) to an existing chilled water loop. Special consideration is given to implementing configuration and control options. Refer to the ASHRAE CTES Design Guide or manufacturer applications guides for detailed implementation info. A user guide document is included in the docs folder of this measure to help translate design objectives into measure argument input values.
12
+
13
+ ## Measure Type
14
+ ModelMeasure
15
+
16
+ ## Taxonomy
17
+
18
+
19
+ ## Arguments
20
+
21
+
22
+ ### Select Energy Storage Objective:
23
+
24
+ **Name:** objective,
25
+ **Type:** Choice,
26
+ **Units:** ,
27
+ **Required:** true,
28
+ **Model Dependent:** false
29
+
30
+ ### Select Upstream Device:
31
+ Partial Storage Only. See documentation for control implementation.
32
+ **Name:** upstream,
33
+ **Type:** Choice,
34
+ **Units:** ,
35
+ **Required:** true,
36
+ **Model Dependent:** false
37
+
38
+ ### Enter Thermal Energy Storage Capacity for Ice Tank [ton-hours]:
39
+
40
+ **Name:** storage_capacity,
41
+ **Type:** Double,
42
+ **Units:** ,
43
+ **Required:** true,
44
+ **Model Dependent:** false
45
+
46
+ ### Select Thaw Process Indicator for Ice Storage:
47
+
48
+ **Name:** melt_indicator,
49
+ **Type:** Choice,
50
+ **Units:** ,
51
+ **Required:** true,
52
+ **Model Dependent:** false
53
+
54
+ ### Select Loop:
55
+ Error: No Cooling Loop Found
56
+ **Name:** selected_loop,
57
+ **Type:** Choice,
58
+ **Units:** ,
59
+ **Required:** true,
60
+ **Model Dependent:** false
61
+
62
+ ### Select Chiller:
63
+ Error: No Chiller Found
64
+ **Name:** selected_chiller,
65
+ **Type:** Choice,
66
+ **Units:** ,
67
+ **Required:** true,
68
+ **Model Dependent:** false
69
+
70
+ ### Enter Chiller Sizing Factor:
71
+
72
+ **Name:** chiller_resize_factor,
73
+ **Type:** Double,
74
+ **Units:** ,
75
+ **Required:** false,
76
+ **Model Dependent:** false
77
+
78
+ ### Enter Chiller Max Capacity Limit During Ice Discharge:
79
+ Enter as a fraction of chiller capacity (0.0 - 1.0).
80
+ **Name:** chiller_limit,
81
+ **Type:** Double,
82
+ **Units:** ,
83
+ **Required:** false,
84
+ **Model Dependent:** false
85
+
86
+ ### Use Existing (Pre-Defined) Temperature Control Schedules?
87
+ Use drop-down selections below.
88
+ **Name:** old,
89
+ **Type:** Boolean,
90
+ **Units:** ,
91
+ **Required:** false,
92
+ **Model Dependent:** false
93
+
94
+ ### Select Pre-Defined Ice Availability Schedule
95
+
96
+ **Name:** ctes_av,
97
+ **Type:** Choice,
98
+ **Units:** ,
99
+ **Required:** false,
100
+ **Model Dependent:** false
101
+
102
+ ### Select Pre-Defined Ice Tank Component Setpoint Schedule
103
+
104
+ **Name:** ctes_sch,
105
+ **Type:** Choice,
106
+ **Units:** ,
107
+ **Required:** false,
108
+ **Model Dependent:** false
109
+
110
+ ### Select Pre-Defined Chiller Component Setpoint Schedule
111
+
112
+ **Name:** chill_sch,
113
+ **Type:** Choice,
114
+ **Units:** ,
115
+ **Required:** false,
116
+ **Model Dependent:** false
117
+
118
+ ### Create New (Simple) Temperature Control Schedules?
119
+ Use entry fields below. If Pre-Defined is also selected, these new schedules will be created but not applied.
120
+ **Name:** new,
121
+ **Type:** Boolean,
122
+ **Units:** ,
123
+ **Required:** false,
124
+ **Model Dependent:** false
125
+
126
+ ### Loop Setpoint Temperature F:
127
+ This value replaces the existing loop temperature setpoint manager; the old manager will be disconnected but not deleted from the model.
128
+ **Name:** loop_sp,
129
+ **Type:** Double,
130
+ **Units:** ,
131
+ **Required:** true,
132
+ **Model Dependent:** false
133
+
134
+ ### Enter Intermediate Setpoint for Upstream Cooling Device During Ice Discharge F:
135
+ Partial storage only
136
+ **Name:** inter_sp,
137
+ **Type:** Double,
138
+ **Units:** ,
139
+ **Required:** false,
140
+ **Model Dependent:** false
141
+
142
+ ### Ice Charging Setpoint Temperature F:
143
+
144
+ **Name:** chg_sp,
145
+ **Type:** Double,
146
+ **Units:** ,
147
+ **Required:** true,
148
+ **Model Dependent:** false
149
+
150
+ ### Loop Design Temperature Difference F:
151
+ Enter numeric value to adjust selected loop settings.
152
+ **Name:** delta_t,
153
+ **Type:** String,
154
+ **Units:** ,
155
+ **Required:** true,
156
+ **Model Dependent:** false
157
+
158
+ ### Enter Seasonal Availabity of Ice Storage:
159
+ Use MM/DD-MM/DD format
160
+ **Name:** ctes_season,
161
+ **Type:** String,
162
+ **Units:** ,
163
+ **Required:** true,
164
+ **Model Dependent:** false
165
+
166
+ ### Enter Starting Time for Ice Discharge:
167
+ Use 24 hour format (HR:MM)
168
+ **Name:** discharge_start,
169
+ **Type:** String,
170
+ **Units:** ,
171
+ **Required:** true,
172
+ **Model Dependent:** false
173
+
174
+ ### Enter End Time for Ice Discharge:
175
+ Use 24 hour format (HR:MM)
176
+ **Name:** discharge_end,
177
+ **Type:** String,
178
+ **Units:** ,
179
+ **Required:** true,
180
+ **Model Dependent:** false
181
+
182
+ ### Enter Starting Time for Ice charge:
183
+ Use 24 hour format (HR:MM)
184
+ **Name:** charge_start,
185
+ **Type:** String,
186
+ **Units:** ,
187
+ **Required:** true,
188
+ **Model Dependent:** false
189
+
190
+ ### Enter End Time for Ice charge:
191
+ Use 24 hour format (HR:MM)
192
+ **Name:** charge_end,
193
+ **Type:** String,
194
+ **Units:** ,
195
+ **Required:** true,
196
+ **Model Dependent:** false
197
+
198
+ ### Allow Ice Discharge on Weekends?
199
+
200
+ **Name:** wknds,
201
+ **Type:** Boolean,
202
+ **Units:** ,
203
+ **Required:** true,
204
+ **Model Dependent:** false
205
+
206
+ ### Select Reporting Frequency for New Output Variables
207
+ This will not change reporting frequency for existing output variables in the model.
208
+ **Name:** report_freq,
209
+ **Type:** Choice,
210
+ **Units:** ,
211
+ **Required:** false,
212
+ **Model Dependent:** false
213
+
214
+ ### Test Demand Reponse Event?
215
+
216
+ **Name:** dr,
217
+ **Type:** Boolean,
218
+ **Units:** ,
219
+ **Required:** false,
220
+ **Model Dependent:** false
221
+
222
+ ### Select if a Load Add or Load Shed Event
223
+
224
+ **Name:** dr_add_shed,
225
+ **Type:** Choice,
226
+ **Units:** ,
227
+ **Required:** false,
228
+ **Model Dependent:** false
229
+
230
+ ### Enter date of demand response event:
231
+ Use MM/DD format.
232
+ **Name:** dr_date,
233
+ **Type:** String,
234
+ **Units:** ,
235
+ **Required:** false,
236
+ **Model Dependent:** false
237
+
238
+ ### Enter start time of demand response event:
239
+ Use 24 hour format (HR:MM)
240
+ **Name:** dr_time,
241
+ **Type:** String,
242
+ **Units:** ,
243
+ **Required:** false,
244
+ **Model Dependent:** false
245
+
246
+ ### Enter duration of demand response event [hr]:
247
+
248
+ **Name:** dr_dur,
249
+ **Type:** Double,
250
+ **Units:** ,
251
+ **Required:** false,
252
+ **Model Dependent:** false
253
+
254
+ ### Allow chiller to back-up ice during DR event?
255
+ Unselection may result in unmet cooling hours
256
+ **Name:** dr_chill,
257
+ **Type:** Boolean,
258
+ **Units:** ,
259
+ **Required:** false,
260
+ **Model Dependent:** false
261
+
262
+
263
+
264
+
@@ -0,0 +1,42 @@
1
+ <%#= README.md.erb is used to auto-generate README.md. %>
2
+ <%#= To manually maintain README.md throw away README.md.erb and manually edit README.md %>
3
+ ###### (Automatically generated documentation)
4
+
5
+ # <%= name %>
6
+
7
+ ## Description
8
+ <%= description %>
9
+
10
+ ## Modeler Description
11
+ <%= modelerDescription %>
12
+
13
+ ## Measure Type
14
+ <%= measureType %>
15
+
16
+ ## Taxonomy
17
+ <%= taxonomy %>
18
+
19
+ ## Arguments
20
+
21
+ <% arguments.each do |argument| %>
22
+ ### <%= argument[:display_name] %>
23
+ <%= argument[:description] %>
24
+ **Name:** <%= argument[:name] %>,
25
+ **Type:** <%= argument[:type] %>,
26
+ **Units:** <%= argument[:units] %>,
27
+ **Required:** <%= argument[:required] %>,
28
+ **Model Dependent:** <%= argument[:model_dependent] %>
29
+ <% end %>
30
+
31
+ <% if arguments.size == 0 %>
32
+ <%= "This measure does not have any user arguments" %>
33
+ <% end %>
34
+
35
+ <% if outputs.size > 0 %>
36
+ ## Outputs
37
+ <% output_names = [] %>
38
+ <% outputs.each do |output| %>
39
+ <% output_names << output[:display_name] %>
40
+ <% end %>
41
+ <%= output_names.join(", ") %>
42
+ <% end %>
@@ -0,0 +1,1310 @@
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2020, 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
+ # Measure distributed under NREL Copyright terms, see LICENSE.md file.
37
+
38
+ # Author: Karl Heine
39
+ # Date: June 2019 - July 2020
40
+ # Additional Code Added to Test Ice Performance for Demand Response Events: September 2019
41
+ # Revised February 2020
42
+
43
+ # References:
44
+ # => ASHRAE Handbook, HVAC Systems and Equipment, Chapter 51: Thermal storage, 2016
45
+ # => ASHRAE Design Guide for Cool Thermal Storage, 2nd Edition, January 2019
46
+ # => Manufacturer marketing materials, available online
47
+
48
+ # load OpenStudio measure libraries
49
+ require "#{File.dirname(__FILE__)}/resources/OsLib_Schedules"
50
+
51
+ # start the measure
52
+ class AddIceStorageToPlantLoopForLoadFlexibility < OpenStudio::Measure::ModelMeasure
53
+ # human readable name
54
+ def name
55
+ # Measure name should be the title case of the class name.
56
+ 'Add Ice Storage Tank'
57
+ end
58
+
59
+ # human readable description
60
+ def description
61
+ 'This measure adds an ice storage tank to a chilled water loop for the purpose of thermal energy storage.'
62
+ end
63
+
64
+ # human readable description of modeling approach
65
+ def modeler_description
66
+ 'This measure adds the necessary components and performs required model articulations to add an ice ' \
67
+ 'thermal storage tank (ITS) to an existing chilled water loop. Special consideration is given to ' \
68
+ 'implementing configuration and control options. Refer to the ASHRAE CTES Design Guide or manufacturer ' \
69
+ 'applications guides for detailed implementation info. A user guide document is included in the docs ' \
70
+ 'folder of this measure to help translate design objectives into measure argument input values.'
71
+ end
72
+
73
+ # define the arguments that the user will input
74
+ def arguments(model)
75
+ args = OpenStudio::Measure::OSArgumentVector.new
76
+
77
+ # Make choice argument for energy storage objective
78
+ objective = OpenStudio::Measure::OSArgument.makeChoiceArgument('objective', ['Full Storage', 'Partial Storage'], true)
79
+ objective.setDisplayName('Select Energy Storage Objective:')
80
+ objective.setDefaultValue('Partial Storage')
81
+ args << objective
82
+
83
+ # Make choice argument for component layout
84
+ upstream = OpenStudio::Measure::OSArgument.makeChoiceArgument('upstream', ['Chiller', 'Storage'])
85
+ upstream.setDisplayName('Select Upstream Device:')
86
+ upstream.setDescription('Partial Storage Only. See documentation for control implementation.')
87
+ upstream.setDefaultValue('Chiller')
88
+ args << upstream
89
+
90
+ # Make double argument for thermal energy storage capacity
91
+ storage_capacity = OpenStudio::Measure::OSArgument.makeDoubleArgument('storage_capacity', true)
92
+ storage_capacity.setDisplayName('Enter Thermal Energy Storage Capacity for Ice Tank [ton-hours]:')
93
+ storage_capacity.setDefaultValue(2000)
94
+ args << storage_capacity
95
+
96
+ # Make choice argument for ice melt process indicator
97
+ melt_indicator = OpenStudio::Measure::OSArgument.makeChoiceArgument('melt_indicator', ['InsideMelt', 'OutsideMelt'], true)
98
+ melt_indicator.setDisplayName('Select Thaw Process Indicator for Ice Storage:')
99
+ melt_indicator.setDescription('')
100
+ melt_indicator.setDefaultValue('InsideMelt')
101
+ args << melt_indicator
102
+
103
+ # Make list of chilled water loop(s) from which user can select
104
+ plant_loops = model.getPlantLoops
105
+ loop_choices = OpenStudio::StringVector.new
106
+ plant_loops.each do |loop|
107
+ if loop.sizingPlant.loopType.to_s == 'Cooling'
108
+ loop_choices << loop.name.to_s
109
+ end
110
+ end
111
+
112
+ # Make choice argument for loop selection
113
+ selected_loop = OpenStudio::Measure::OSArgument.makeChoiceArgument('selected_loop', loop_choices, true)
114
+ selected_loop.setDisplayName('Select Loop:')
115
+ selected_loop.setDescription('This is the cooling loop on which the ice tank will be added.')
116
+ if loop_choices.include?('Chilled Water Loop')
117
+ selected_loop.setDefaultValue('Chilled Water Loop')
118
+ else
119
+ selected_loop.setDescription('Error: No Cooling Loop Found')
120
+ end
121
+ args << selected_loop
122
+
123
+ # Make list of available chillers from which the user can select
124
+ chillers = model.getChillerElectricEIRs
125
+ chillers += model.getChillerAbsorptions
126
+ chillers += model.getChillerAbsorptionIndirects
127
+ chiller_choices = OpenStudio::StringVector.new
128
+ chillers.each do |chill|
129
+ chiller_choices << chill.name.to_s
130
+ end
131
+
132
+ # Make choice argument for chiller selection
133
+ selected_chiller = OpenStudio::Measure::OSArgument.makeChoiceArgument('selected_chiller', chiller_choices, true)
134
+ selected_chiller.setDisplayName('Select Chiller:')
135
+ selected_chiller.setDescription('The ice tank will be placed in series with this chiller.')
136
+ if !chillers.empty?
137
+ selected_chiller.setDefaultValue(chiller_choices[0])
138
+ else
139
+ selected_chiller.setDescription('Error: No Chiller Found')
140
+ end
141
+ args << selected_chiller
142
+
143
+ # Make double argument for ice chiller resizing factor - relative to selected chiller capacity
144
+ chiller_resize_factor = OpenStudio::Measure::OSArgument.makeDoubleArgument('chiller_resize_factor', false)
145
+ chiller_resize_factor.setDisplayName('Enter Chiller Sizing Factor:')
146
+ chiller_resize_factor.setDefaultValue('0.7')
147
+ args << chiller_resize_factor
148
+
149
+ # Make double argument for chiller max capacity limit during ice discharge
150
+ chiller_limit = OpenStudio::Measure::OSArgument.makeDoubleArgument('chiller_limit', false)
151
+ chiller_limit.setDisplayName('Enter Chiller Max Capacity Limit During Ice Discharge:')
152
+ chiller_limit.setDescription('Enter as a fraction of chiller capacity (0.0 - 1.0).')
153
+ chiller_limit.setDefaultValue('0.75')
154
+ args << chiller_limit
155
+
156
+ # Make choice argument for schedule options
157
+ old = OpenStudio::Measure::OSArgument.makeBoolArgument('old', false)
158
+ old.setDisplayName('Use Existing (Pre-Defined) Temperature Control Schedules?')
159
+ old.setDescription('Use drop-down selections below.')
160
+ old.setDefaultValue(false)
161
+ args << old
162
+
163
+ # Find All Existing Schedules with Type Limits of "Temperature"
164
+ sched_options = []
165
+ sched_options2 = []
166
+ all_scheds = model.getSchedules
167
+ all_scheds.each do |sched|
168
+ sched.to_ScheduleBase.get
169
+ unless sched.scheduleTypeLimits.empty?
170
+ if sched.scheduleTypeLimits.get.unitType.to_s == 'Temperature'
171
+ sched_options << sched.name.to_s
172
+ elsif sched.scheduleTypeLimits.get.unitType.to_s == 'Availability' ||
173
+ sched.scheduleTypeLimits.get.unitType.to_s == 'OnOff'
174
+ sched_options2 << sched.name.to_s
175
+ end
176
+ end
177
+ end
178
+ sched_options = ['N/A'] + sched_options.sort
179
+ sched_options2 = ['N/A'] + sched_options2.sort
180
+
181
+ # Create choice argument for ice availability schedule (old = true)
182
+ ctes_av = OpenStudio::Measure::OSArgument.makeChoiceArgument('ctes_av', sched_options2, false)
183
+ ctes_av.setDisplayName('Select Pre-Defined Ice Availability Schedule')
184
+ if sched_options2.empty?
185
+ ctes_av.setDescription('Warning: No availability schedules found')
186
+ end
187
+ ctes_av.setDefaultValue('N/A')
188
+ args << ctes_av
189
+
190
+ # Create choice argument for ice tank component setpoint sched (old = true)
191
+ ctes_sch = OpenStudio::Measure::OSArgument.makeChoiceArgument('ctes_sch', sched_options, false)
192
+ ctes_sch.setDisplayName('Select Pre-Defined Ice Tank Component Setpoint Schedule')
193
+ if sched_options.empty?
194
+ ctes_sch.setDescription('Warning: No temperature setpoint schedules found')
195
+ end
196
+ ctes_sch.setDefaultValue('N/A')
197
+ args << ctes_sch
198
+
199
+ # Create choice argument for chiller component setpoint sched (old = true)
200
+ chill_sch = OpenStudio::Measure::OSArgument.makeChoiceArgument('chill_sch', sched_options, false)
201
+ chill_sch.setDisplayName('Select Pre-Defined Chiller Component Setpoint Schedule')
202
+ if sched_options.empty?
203
+ chill_sch.setDescription('Warning: No temperature setpoint schedules found')
204
+ end
205
+ chill_sch.setDefaultValue('N/A')
206
+ args << chill_sch
207
+
208
+ # Make bool argument for creating new schedules
209
+ new = OpenStudio::Measure::OSArgument.makeBoolArgument('new', false)
210
+ new.setDisplayName('Create New (Simple) Temperature Control Schedules?')
211
+ new.setDescription('Use entry fields below. If Pre-Defined is also selected, these new schedules will be created' \
212
+ ' but not applied.')
213
+ new.setDefaultValue(true)
214
+ args << new
215
+
216
+ # Make double argument for loop setpoint temperature
217
+ loop_sp = OpenStudio::Measure::OSArgument.makeDoubleArgument('loop_sp', true)
218
+ loop_sp.setDisplayName('Loop Setpoint Temperature F:')
219
+ loop_sp.setDescription('This value replaces the existing loop temperature setpoint manager; the old manager will ' \
220
+ 'be disconnected but not deleted from the model.')
221
+ loop_sp.setDefaultValue(44)
222
+ args << loop_sp
223
+
224
+ # Make double argument for ice chiller outlet temp during partial storage operation
225
+ inter_sp = OpenStudio::Measure::OSArgument.makeDoubleArgument('inter_sp', false)
226
+ inter_sp.setDisplayName('Enter Intermediate Setpoint for Upstream Cooling Device During Ice Discharge F:')
227
+ inter_sp.setDescription('Partial storage only')
228
+ inter_sp.setDefaultValue(47)
229
+ args << inter_sp
230
+
231
+ # Make double argument for loop temperature for ice charging
232
+ chg_sp = OpenStudio::Measure::OSArgument.makeDoubleArgument('chg_sp', true)
233
+ chg_sp.setDisplayName('Ice Charging Setpoint Temperature F:')
234
+ chg_sp.setDefaultValue('25')
235
+ args << chg_sp
236
+
237
+ # Make double argument for loop design delta T
238
+ delta_t = OpenStudio::Measure::OSArgument.makeStringArgument('delta_t', true)
239
+ delta_t.setDisplayName('Loop Design Temperature Difference F:')
240
+ delta_t.setDescription('Enter numeric value to adjust selected loop settings.')
241
+ delta_t.setDefaultValue('Use Existing Loop Value')
242
+ args << delta_t
243
+
244
+ # Make string argument for ctes seasonal availabilty
245
+ ctes_season = OpenStudio::Measure::OSArgument.makeStringArgument('ctes_season', true)
246
+ ctes_season.setDisplayName('Enter Seasonal Availabity of Ice Storage:')
247
+ ctes_season.setDescription('Use MM/DD-MM/DD format')
248
+ ctes_season.setDefaultValue('01/01-12/31')
249
+ args << ctes_season
250
+
251
+ # Make string arguments for ctes discharge times
252
+ discharge_start = OpenStudio::Measure::OSArgument.makeStringArgument('discharge_start', true)
253
+ discharge_start.setDisplayName('Enter Starting Time for Ice Discharge:')
254
+ discharge_start.setDescription('Use 24 hour format (HR:MM)')
255
+ discharge_start.setDefaultValue('08:00')
256
+ args << discharge_start
257
+
258
+ discharge_end = OpenStudio::Measure::OSArgument.makeStringArgument('discharge_end', true)
259
+ discharge_end.setDisplayName('Enter End Time for Ice Discharge:')
260
+ discharge_end.setDescription('Use 24 hour format (HR:MM)')
261
+ discharge_end.setDefaultValue('21:00')
262
+ args << discharge_end
263
+
264
+ # Make string arguments for ctes charge times
265
+ charge_start = OpenStudio::Measure::OSArgument.makeStringArgument('charge_start', true)
266
+ charge_start.setDisplayName('Enter Starting Time for Ice charge:')
267
+ charge_start.setDescription('Use 24 hour format (HR:MM)')
268
+ charge_start.setDefaultValue('23:00')
269
+ args << charge_start
270
+
271
+ charge_end = OpenStudio::Measure::OSArgument.makeStringArgument('charge_end', true)
272
+ charge_end.setDisplayName('Enter End Time for Ice charge:')
273
+ charge_end.setDescription('Use 24 hour format (HR:MM)')
274
+ charge_end.setDefaultValue('07:00')
275
+ args << charge_end
276
+
277
+ # Make boolean arguments for ctes dischage days
278
+ wknds = OpenStudio::Measure::OSArgument.makeBoolArgument('wknds', true)
279
+ wknds.setDisplayName('Allow Ice Discharge on Weekends?')
280
+ wknds.setDefaultValue(false)
281
+ args << wknds
282
+
283
+ # Make choice argument for output variable reporting frequency
284
+ report_choices = ['Detailed', 'Timestep', 'Hourly', 'Daily', 'Monthly', 'RunPeriod']
285
+ report_freq = OpenStudio::Measure::OSArgument.makeChoiceArgument('report_freq', report_choices, false)
286
+ report_freq.setDisplayName('Select Reporting Frequency for New Output Variables')
287
+ report_freq.setDescription('This will not change reporting frequency for existing output variables in the model.')
288
+ report_freq.setDefaultValue('Timestep')
289
+ args << report_freq
290
+
291
+ ## DR TESTER INPUTS -----------------------------------------
292
+ # Make boolean argument for use of demand response event test
293
+ dr = OpenStudio::Measure::OSArgument.makeBoolArgument('dr', false)
294
+ dr.setDisplayName('Test Demand Reponse Event?')
295
+ dr.setDescription('')
296
+ dr.setDefaultValue(false)
297
+ args << dr
298
+
299
+ # Make choice argument for type of demand response event (add or shed)
300
+ dr_add_shed = OpenStudio::Measure::OSArgument.makeChoiceArgument('dr_add_shed', ['Add', 'Shed'], false)
301
+ dr_add_shed.setDisplayName('Select if a Load Add or Load Shed Event')
302
+ dr_add_shed.setDescription('')
303
+ dr_add_shed.setDefaultValue('Shed')
304
+ args << dr_add_shed
305
+
306
+ # Make string argument for DR event date
307
+ dr_date = OpenStudio::Measure::OSArgument.makeStringArgument('dr_date', false)
308
+ dr_date.setDisplayName('Enter date of demand response event:')
309
+ dr_date.setDescription('Use MM/DD format.')
310
+ dr_date.setDefaultValue('9/19')
311
+ args << dr_date
312
+
313
+ # Make string argument for DR Event time
314
+ dr_time = OpenStudio::Measure::OSArgument.makeStringArgument('dr_time', false)
315
+ dr_time.setDisplayName('Enter start time of demand response event:')
316
+ dr_time.setDescription('Use 24 hour format (HR:MM)')
317
+ dr_time.setDefaultValue('11:30')
318
+ args << dr_time
319
+
320
+ # Make double argument for DR event duration
321
+ dr_dur = OpenStudio::Measure::OSArgument.makeDoubleArgument('dr_dur', false)
322
+ dr_dur.setDisplayName('Enter duration of demand response event [hr]:')
323
+ dr_dur.setDescription('')
324
+ dr_dur.setDefaultValue('3')
325
+ args << dr_dur
326
+
327
+ # Make boolean argument for allowing chiller to back-up ice
328
+ dr_chill = OpenStudio::Measure::OSArgument.makeBoolArgument('dr_chill', false)
329
+ dr_chill.setDisplayName('Allow chiller to back-up ice during DR event?')
330
+ dr_chill.setDescription('Unselection may result in unmet cooling hours')
331
+ dr_chill.setDefaultValue('false')
332
+ args << dr_chill
333
+ ## END DR TESTER INPUTS --------------------------------------
334
+
335
+ args
336
+ end
337
+
338
+ # define what happens when the measure is run
339
+ def run(model, runner, user_arguments)
340
+ super(model, runner, user_arguments)
341
+
342
+ # use the built-in error checking
343
+ unless runner.validateUserArguments(arguments(model), user_arguments)
344
+ return false
345
+ end
346
+
347
+ ## Arguments and Declarations---------------------------------------------------------------------------------------
348
+ # Assign user arguments to variables
349
+ objective = runner.getStringArgumentValue('objective', user_arguments)
350
+ upstream = runner.getStringArgumentValue('upstream', user_arguments)
351
+ storage_capacity = runner.getDoubleArgumentValue('storage_capacity', user_arguments)
352
+ melt_indicator = runner.getStringArgumentValue('melt_indicator', user_arguments)
353
+ selected_loop = runner.getStringArgumentValue('selected_loop', user_arguments)
354
+ selected_chiller = runner.getStringArgumentValue('selected_chiller', user_arguments)
355
+ chiller_resize_factor = runner.getDoubleArgumentValue('chiller_resize_factor', user_arguments)
356
+ chiller_limit = runner.getDoubleArgumentValue('chiller_limit', user_arguments)
357
+ old = runner.getBoolArgumentValue('old', user_arguments)
358
+ ctes_av = runner.getStringArgumentValue('ctes_av', user_arguments)
359
+ ctes_sch = runner.getStringArgumentValue('ctes_sch', user_arguments)
360
+ chill_sch = runner.getStringArgumentValue('chill_sch', user_arguments)
361
+ new = runner.getBoolArgumentValue('new', user_arguments)
362
+ loop_sp = runner.getDoubleArgumentValue('loop_sp', user_arguments)
363
+ inter_sp = runner.getDoubleArgumentValue('inter_sp', user_arguments)
364
+ chg_sp = runner.getDoubleArgumentValue('chg_sp', user_arguments)
365
+ delta_t = runner.getStringArgumentValue('delta_t', user_arguments)
366
+ ctes_season = runner.getStringArgumentValue('ctes_season', user_arguments)
367
+ discharge_start = runner.getStringArgumentValue('discharge_start', user_arguments)
368
+ discharge_end = runner.getStringArgumentValue('discharge_end', user_arguments)
369
+ charge_start = runner.getStringArgumentValue('charge_start', user_arguments)
370
+ charge_end = runner.getStringArgumentValue('charge_end', user_arguments)
371
+ wknds = runner.getBoolArgumentValue('wknds', user_arguments)
372
+ report_freq = runner.getStringArgumentValue('report_freq', user_arguments)
373
+
374
+ ## DR TESTER INPUTS ----------------------------------
375
+ dr = runner.getBoolArgumentValue('dr', user_arguments)
376
+ dr_add_shed = runner.getStringArgumentValue('dr_add_shed', user_arguments)
377
+ (dr_mon, dr_day) = runner.getStringArgumentValue('dr_date', user_arguments).split('/')
378
+ (dr_hr, dr_min) = runner.getStringArgumentValue('dr_time', user_arguments).split(':')
379
+ dr_dur = runner.getDoubleArgumentValue('dr_dur', user_arguments)
380
+ dr_chill = runner.getBoolArgumentValue('dr_chill', user_arguments)
381
+ dr_time = (dr_hr.to_f + (dr_min.to_f / 60)).round(2)
382
+ ## END DR TESTER INPUTS ------------------------------
383
+
384
+ # Declare useful variables with values set within do-loops
385
+ cond_loop = ''
386
+ ctes_sp_sched = ''
387
+ ctes_setpoint = 99.0 # This is a flag value and should be overwritten later
388
+ demand_sp_mgr = ''
389
+
390
+ ## Validate User Inputs---------------------------------------------------------------------------------------------
391
+
392
+ # Convert thermal storage capacity from ton-hours to GJ
393
+ storage_capacity = 12_660_670.23144e-9 * storage_capacity
394
+
395
+ # Check for existence of charging setpoint temperature, reset to default if left blank.
396
+ if chg_sp.nil? || chg_sp >= 32.0
397
+ runner.registerWarning('An invalid ice charging temperature was entered. Value reset to -3.88 C (25.0 F).')
398
+ chg_sp = 25.0
399
+ elsif chg_sp < 20.0
400
+ runner.registerWarning('The ice charging temperature is set below 20 F; this is atypically low. Verify input.')
401
+ end
402
+
403
+ # Convert setpoint temperature inputs from F to C
404
+ loop_sp = (loop_sp - 32.0) / 1.8
405
+ inter_sp = (inter_sp - 32.0) / 1.8
406
+ chg_sp = (chg_sp - 32.0) / 1.8
407
+
408
+ # Check if both old and new are false, set new = true and report error
409
+ if !old && !new
410
+ runner.registerError('No CTES schedule option was selected; either use old schedules or the create new option. ' \
411
+ 'Measure aborted.')
412
+ return false
413
+ end
414
+
415
+ # Locate selected chiller and verify loop connection
416
+ if !model.getChillerElectricEIRByName(selected_chiller).empty?
417
+ ctes_chiller = model.getChillerElectricEIRByName(selected_chiller).get
418
+ elsif !model.getChillerAbsorptionByName(selected_chiller).empty?
419
+ ctes_chiller = model.getChillerAbsorptionByName(selected_chiller).get
420
+ elsif !model.getChillerAbsorptionIndirectByName(selected_chiller).empty?
421
+ ctes_chiller = model.getChillerAbsorptionIndirectByName(selected_chiller).get
422
+ end
423
+
424
+ ctes_loop = model.getModelObjectByName(selected_loop).get.to_PlantLoop.get
425
+
426
+ unless ctes_loop.components.include?(ctes_chiller)
427
+ runner.registerError('The selected chiller is not located on the selected chilled water loop. Measure aborted.')
428
+ return false
429
+ end
430
+
431
+ # Convert Delta T if needed from F to C (Overwrites string variables as floats)
432
+ if delta_t != 'Use Existing Loop Value' && delta_t.to_f != 0.0
433
+ delta_t = delta_t.to_f / 1.8
434
+ else
435
+ # Could add additional checks here for invalid (non-numerical) entries
436
+ delta_t = ctes_loop.sizingPlant.loopDesignTemperatureDifference
437
+ end
438
+
439
+ # Check chiller limit input values
440
+ if chiller_limit > 1.0
441
+ runner.registerWarning('Chiller limit must be a ratio less than 1. Limit set to 1.0.')
442
+ chiller_limit = 1.0
443
+ elsif chiller_limit < 0
444
+ runner.registerWarning('Chiller limit must be a ratio greater than or equal to 0. Limit set to 0.0' \
445
+ ' (i.e. full storage).')
446
+ chiller_limit = 0.0
447
+ elsif chiller_limit < 0.15
448
+ runner.registerInfo('Chiller limit is below 15%; this may be outside the reasonable part load operating " \
449
+ "window for the device. Consider increasing or setting to 0.')
450
+ end
451
+
452
+ # Convert chiller limit to a temperature value based on delta_t variable if != 1. Otherwise, use as flag for EMS
453
+ if chiller_limit != 1.0
454
+ dt_max = chiller_limit * delta_t # degrees C
455
+ runner.registerInfo("Max chiller dT during ice discharge is set to: #{dt_max.round(2)} C " \
456
+ "(#{(dt_max * 1.8).round(2)} F).")
457
+ else
458
+ dt_max = delta_t
459
+ end
460
+
461
+ # Check limits of chiller performance curves and adjust if necessary - Notify user with WARNING
462
+ curve_output_check = false
463
+ cap_ft = ctes_chiller.coolingCapacityFunctionOfTemperature
464
+ min_x = cap_ft.minimumValueofx.to_f
465
+ if min_x > chg_sp
466
+ cap_ft.setMinimumValueofx(chg_sp)
467
+ runner.registerWarning("VERIFY CURVE VALIDITY: The input range for the '#{cap_ft.name}' curve is too " \
468
+ 'restrictive for use with ice charging. The provided curve has been ' \
469
+ "extrapolated to a lower limit of #{chg_sp.round(2)} C for the 'x' variable.")
470
+ curve_output_check = true
471
+ end
472
+
473
+ eir_ft = ctes_chiller.electricInputToCoolingOutputRatioFunctionOfTemperature
474
+ min_x = eir_ft.minimumValueofx.to_f
475
+ if min_x > chg_sp
476
+ runner.registerWarning("VERIFY CURVE VALIDITY: The input range for the '#{eir_ft.name}' curve is too " \
477
+ 'restrictive for use with ice charging. The provided curve has been ' \
478
+ "extrapolated to a lower limit of #{chg_sp.round(2)} C for the 'x' variable.")
479
+ end
480
+
481
+ # Report chiller performance derate at the ice-making conditions.
482
+ if curve_output_check == true
483
+ derate = cap_ft.evaluate(chg_sp, ctes_chiller.referenceEnteringCondenserFluidTemperature)
484
+ runner.registerInfo('A curve extrapolation warning was registered for the chiller capacity as a function ' \
485
+ 'of temperature curve. At normal ice making temperatures, a chiller derate to 60-70% ' \
486
+ 'of nominal capacity is expected. Using a condenser entering water temperature of ' \
487
+ "#{ctes_chiller.referenceEnteringCondenserFluidTemperature.round(1)} C and the ice " \
488
+ "charging temperature of #{chg_sp.round(1)} C, a derate to #{(derate * 100).round(1)}% is " \
489
+ 'returned. This value will increase with lower condenser fluid return temperatures.')
490
+ end
491
+
492
+ # Check to ensure schedules are selected if old = true
493
+ if old
494
+ if ctes_av == 'N/A'
495
+ runner.registerError('Pre-Defined schedule option chosen, but no availabity schedule was selected.')
496
+ runner.registerWarning('Measure terminated early; no storage was applied.')
497
+ return false
498
+ end
499
+ if ctes_sch == 'N/A'
500
+ runner.registerError('Pre-Defined schedule option chosen, but no ice tank setpoint schedule was selected.')
501
+ runner.registerWarning('Measure terminated early; no storage was applied.')
502
+ return false
503
+ end
504
+ if chill_sch == 'N/A'
505
+ runner.registerError('Pre-Defined schedule option chosen, but no chiller setpoint schedule was selected.')
506
+ runner.registerWarning('Measure terminated early; no storage was applied.')
507
+ return false
508
+ end
509
+ end
510
+
511
+ # Parse and verify schedule inputs
512
+ # Remove potential spaces from date inputs
513
+ ctes_season = ctes_season.delete(' ')
514
+
515
+ # Convert HR:MM format into HR.fraction format
516
+ (d_start_hr, d_start_min) = discharge_start.split(':')
517
+ (d_end_hr, d_end_min) = discharge_end.split(':')
518
+ (c_start_hr, c_start_min) = charge_start.split(':')
519
+ (c_end_hr, c_end_min) = charge_end.split(':')
520
+
521
+ # Store re-formatted time values in shorthand variables for use in schedule
522
+ # building
523
+ ds = (d_start_hr.to_f + d_start_min.to_f / 60).round(2)
524
+ de = (d_end_hr.to_f + d_end_min.to_f / 60).round(2)
525
+ cs = (c_start_hr.to_f + c_start_min.to_f / 60).round(2)
526
+ ce = (c_end_hr.to_f + c_end_min.to_f / 60).round(2)
527
+
528
+ # Verify that input times make sense
529
+ if ds > de
530
+ runner.registerWarning('Dischage start time is later than discharge ' \
531
+ 'end time (your ice will discharge overnight). ' \
532
+ 'Verify schedule inputs.')
533
+ end
534
+
535
+ if cs.between?(ds - 0.01, de + 0.01) || ce.between?(ds - 0.01, de + 0.01)
536
+ runner.registerWarning('The tank charge and discharge periods overlap. ' \
537
+ 'Examine results for unexpected operation; ' \
538
+ 'verify schedule inputs.')
539
+ end
540
+
541
+ if [ds, de, cs, ce].any? { |i| i > 24 }
542
+ runner.registerError('One of you time enteries exceeds 24:00, ' \
543
+ 'resulting in a schedule error. Measure aborted.')
544
+ return false
545
+ end
546
+
547
+ ## Report Initial Condition of the Model----------------------------------------------------------------------------
548
+ total_storage = model.getThermalStorageIceDetaileds.size
549
+ runner.registerInitialCondition("The model started with #{total_storage} ice storage device(s).")
550
+
551
+ runner.registerInfo("Chiller '#{selected_chiller}' on Loop '#{selected_loop}' was selected for the addition " \
552
+ "of a #{storage_capacity.round(2)} GJ (#{(storage_capacity / 12_660_670.23144e-9).round(0)} " \
553
+ 'ton-hours) ice thermal energy storage object.')
554
+
555
+ ## Modify Chiller Settings------------------------------------------------------------------------------------------
556
+
557
+ # Adjust ctes chiller minimum outlet temperature
558
+ ctes_chiller.setLeavingChilledWaterLowerTemperatureLimit(chg_sp)
559
+ runner.registerInfo("Selected chiller minimum setpoint temperature was adjusted to #{chg_sp.round(2)} C " \
560
+ "(#{(chg_sp * 1.8) + 32} F).")
561
+
562
+ # Adjust ctes chiller sizing factor based on user input
563
+ if ctes_chiller.isReferenceCapacityAutosized
564
+ ctes_chiller.setSizingFactor(chiller_resize_factor)
565
+ runner.registerInfo("Selected chiller has been resized to #{chiller_resize_factor * 100}% of autosized " \
566
+ 'capacity.')
567
+ else
568
+ ctes_chiller.setReferenceCapacity(
569
+ chiller_resize_factor * ctes_chiller.referenceCapacity.to_f
570
+ )
571
+ runner.registerInfo("Selected chiller has been resized to #{chiller_resize_factor * 100}% of original " \
572
+ '(hardsized) capacity.')
573
+ end
574
+
575
+ ## Modify Loop Settings---------------------------------------------------------------------------------------------
576
+
577
+ # Adjust minimum loop temperature
578
+ ctes_loop.setMinimumLoopTemperature(chg_sp)
579
+ runner.registerInfo("Selected loop minimum temperature was adjusted to #{chg_sp.round(2)} C " \
580
+ "(#{(chg_sp * 1.8) + 32} F).")
581
+
582
+ # Adjust plant load distribution scheme
583
+ if ctes_loop.loadDistributionScheme != 'SequentialLoad'
584
+ ctes_loop.setLoadDistributionScheme('SequentialLoad')
585
+ runner.registerInfo("Selected loop load distribution scheme was set to 'SequentialLoad'.")
586
+ end
587
+
588
+ # Adjust loop design temperature difference
589
+ if ctes_loop.sizingPlant.loopDesignTemperatureDifference.to_f != delta_t
590
+ ctes_loop.sizingPlant.setLoopDesignTemperatureDifference(delta_t)
591
+ runner.registerInfo("Selected loop design temperature difference was adjusted to #{delta_t.round(2)} C " \
592
+ "(#{(delta_t * 1.8)} F).")
593
+ end
594
+
595
+ # Adjust loop gylcol solution percentage and set glycol - if necessary
596
+ if ctes_loop.fluidType == 'Water'
597
+ ctes_loop.setFluidType('EthyleneGlycol')
598
+ ctes_loop.setGlycolConcentration(25)
599
+ runner.registerInfo('Selected loop working fluid changed to ethylene glycol at a 25% concentration.')
600
+ elsif ctes_loop.glycolConcentration < 25
601
+ runner.registerInfo('Selected loop gylycol concentration is less than 25%. Consider increasing to 25-30%.')
602
+ end
603
+
604
+ # Adjust loop to two-way common pipe simulation - if necessary
605
+ if ctes_loop.commonPipeSimulation != 'TwoWayCommonPipe'
606
+ ctes_loop.setCommonPipeSimulation('TwoWayCommonPipe')
607
+ runner.registerInfo("Selected loop common pipe simulation changed to 'TwoWayCommonPipe'.")
608
+
609
+ # Add setpoint manager at inlet of demand loop (req'd for two-way common pipe sim.)
610
+ if old # Only applies if old curves are used, regardless of whether new curves are created (old takes precedence)
611
+ loop_sp_node = ctes_loop.loopTemperatureSetpointNode
612
+ loop_sp_mgrs = loop_sp_node.setpointManagers
613
+ loop_sp_mgrs.each do |spm|
614
+ if spm.controlVariable == 'Temperature'
615
+ demand_sp_mgr = spm.clone.to_SetpointManagerScheduled.get
616
+ end
617
+ end
618
+ demand_sp_mgr.addToNode(ctes_loop.demandInletNode)
619
+ runner.registerInfo('Original loop temperature setpoint manager duplicated and added to demand loop inlet node.')
620
+ end
621
+
622
+ end
623
+
624
+ ## Create CTES Hardware---------------------------------------------------------------------------------------------
625
+
626
+ # Create ice tank (aka ctes)
627
+ ctes = OpenStudio::Model::ThermalStorageIceDetailed.new(model)
628
+ ctes.setCapacity(storage_capacity)
629
+ ctes.setThawProcessIndicator(melt_indicator)
630
+
631
+ # Add ice tank to loop based on user-selected objective option and upstream device
632
+ if objective == 'Full Storage'
633
+ # Full storage places the ice tank upstream of the chiller with no user option to change.
634
+ ctes.addToNode(ctes_chiller.supplyInletModelObject.get.to_Node.get)
635
+ elsif objective == 'Partial Storage' && upstream == 'Storage'
636
+ ctes.addToNode(ctes_chiller.supplyInletModelObject.get.to_Node.get)
637
+ elsif objective == 'Partial Storage' && upstream == 'Chiller'
638
+ ctes.addToNode(ctes_chiller.supplyOutletModelObject.get.to_Node.get)
639
+ end
640
+
641
+ ## Create New Schedules if Necessary--------------------------------------------------------------------------------
642
+ #-------------------------------------------------------------------------------------------------------------------
643
+ if new
644
+ ## Check for Schedule Type Limits and Create if Needed------------------------------------------------------------
645
+ if model.getModelObjectByName('OnOff').get.initialized
646
+ sched_limits_onoff = model.getModelObjectByName('OnOff').get.to_ScheduleTypeLimits.get
647
+ else
648
+ sched_limits_onoff = OpenStudio::Model::ScheduleTypeLimits.new(model)
649
+ sched_limits_onoff.setName('OnOff')
650
+ sched_limits_onoff.setNumericType('Discrete')
651
+ sched_limits_onoff.setUnitType('Availability')
652
+ sched_limits_onoff.setLowerLimitValue(0.0)
653
+ sched_limits_onoff.setUpperLimitValue(1.0)
654
+ end
655
+
656
+ if model.getModelObjectByName('Temperature').get.initialized
657
+ sched_limits_temp = model.getModelObjectByName('Temperature').get.to_ScheduleTypeLimits.get
658
+ if sched_limits_temp.lowerLimitValue.to_f > chg_sp
659
+ sched_limits_temp.setLowerLimitValue(chg_sp)
660
+ end
661
+ else
662
+ sched_limits_temp = OpenStudio::Model::ScheduleTypeLimits.new(model)
663
+ sched_limits_temp.setName('Temperature')
664
+ sched_limits_temp.setNumericType('Continuous')
665
+ sched_limits_temp.setUnitType('Temperature')
666
+ end
667
+
668
+ ## Create Schedules-----------------------------------------------------------------------------------------------
669
+
670
+ # Create key-value sets based on user inputs for charge/discharge times
671
+ # cs = charge start, ce = charge end, ds = discharge start, de = discharge end
672
+
673
+ # Set chiller and ice discharge setpoints for partial storage configs
674
+ if objective == 'Full Storage'
675
+ chiller_setpoint = loop_sp
676
+ ctes_setpoint = loop_sp
677
+ elsif objective == 'Partial Storage'
678
+ if upstream == 'Chiller'
679
+ chiller_setpoint = inter_sp
680
+ ctes_setpoint = loop_sp
681
+ elsif upstream == 'Storage'
682
+ chiller_setpoint = loop_sp
683
+ ctes_setpoint = inter_sp
684
+ end
685
+ end
686
+
687
+ # Handle overnight charging and discharging
688
+ if ce < cs
689
+ midnight_av = [24, 1]
690
+ midnight_chiller = [24, chg_sp]
691
+ midnight_ctes = [24, loop_sp]
692
+ elsif de < ds
693
+ midnight_av = [24, 1]
694
+ midnight_chiller = [24, chiller_setpoint]
695
+ midnight_ctes = [24, ctes_setpoint]
696
+ else
697
+ midnight_av = [24, 0]
698
+ midnight_chiller = [24, loop_sp]
699
+ midnight_ctes = [24, 99]
700
+ end
701
+
702
+ # Availablity k-v sets for CTES
703
+ wk_av = [[cs, 0], [ce, 1], [ds, 0], [de, 1], midnight_av].sort
704
+ wknd_av = [[cs, 0], [ce, 1], midnight_av].sort
705
+
706
+ # Temperature k-v sets for CTES
707
+ wk_ctes = [[cs, 99], [ce, loop_sp], [ds, 99], [de, ctes_setpoint], midnight_ctes].sort
708
+ wknd_ctes = [[cs, 99], [ce, loop_sp], midnight_ctes].sort
709
+
710
+ # Temperature k-v set for Chiller
711
+ wk_chiller = [[cs, loop_sp], [ce, chg_sp], [ds, loop_sp], [de, chiller_setpoint], midnight_chiller].sort
712
+ wknd_chiller = [[cs, loop_sp], [ce, chg_sp], midnight_chiller].sort
713
+
714
+ # Apply weekends modifer if necessary
715
+ if wknds
716
+ wknd_av = wk_av
717
+ wknd_ctes = wk_ctes
718
+ wknd_chiller = wk_chiller
719
+ end
720
+
721
+ # Create ice availability schedule
722
+ ruleset_name = 'Ice Availability Schedule (New)'
723
+ winter_design_day = [[24, 0]]
724
+ summer_design_day = wk_av
725
+ default_day = ['AllDays'] + [[24, 0]]
726
+ rules = []
727
+ rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_av
728
+ rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_av
729
+ options_ctes = { 'name' => ruleset_name,
730
+ 'winter_design_day' => winter_design_day,
731
+ 'summer_design_day' => summer_design_day,
732
+ 'default_day' => default_day,
733
+ 'rules' => rules }
734
+ ctes_av_new = OsLib_Schedules.createComplexSchedule(model, options_ctes)
735
+ ctes_av_new.setScheduleTypeLimits(sched_limits_onoff)
736
+
737
+ # Create ctes setpoint temperature schedule
738
+ ruleset_name = "#{ctes.name} Setpoint Schedule (New)"
739
+ winter_design_day = [[24, 99]]
740
+ summer_design_day = wk_ctes
741
+ default_day = ['AllDays'] + [[24, 99]]
742
+ rules = []
743
+ rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_ctes
744
+ rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_ctes
745
+ options_ctes_ctes = { 'name' => ruleset_name,
746
+ 'winter_design_day' => winter_design_day,
747
+ 'summer_design_day' => summer_design_day,
748
+ 'default_day' => default_day,
749
+ 'rules' => rules }
750
+ ctes_sch_new = OsLib_Schedules.createComplexSchedule(model, options_ctes_ctes)
751
+ ctes_sch_new.setScheduleTypeLimits(sched_limits_temp)
752
+
753
+ # Create chiller setpoint temperature schedule
754
+ ruleset_name = "#{ctes_chiller.name} Setpoint Schedule (New)"
755
+ winter_design_day = [[24, loop_sp]]
756
+ summer_design_day = wk_chiller
757
+ default_day = ['AllDays'] + [[24, loop_sp]]
758
+ rules = []
759
+ rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_chiller
760
+ rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_chiller
761
+ options_ctes_chiller = { 'name' => ruleset_name,
762
+ 'winter_design_day' => winter_design_day,
763
+ 'summer_design_day' => summer_design_day,
764
+ 'default_day' => default_day,
765
+ 'rules' => rules }
766
+ chill_sch_new = OsLib_Schedules.createComplexSchedule(model, options_ctes_chiller)
767
+ chill_sch_new.setScheduleTypeLimits(sched_limits_temp)
768
+
769
+ # Create loop setpoint temperature schedule - if new = true
770
+ if new
771
+ ruleset_name = "#{ctes_loop.name} Setpoint Schedule (New)"
772
+ options_ctes_loop = { 'name' => ruleset_name,
773
+ 'winterTimeValuePairs' => [[24, loop_sp]],
774
+ 'summerTimeValuePairs' => [[24, loop_sp]],
775
+ 'defaultTimeValuePairs' => [[24, loop_sp]] }
776
+ loop_sch_new = OsLib_Schedules.createSimpleSchedule(model, options_ctes_loop)
777
+ loop_sch_new.setScheduleTypeLimits(sched_limits_temp)
778
+ end
779
+
780
+ # Register info about new schedule objects
781
+ runner.registerInfo("The following schedules were added to the model:\n" \
782
+ " * #{ctes_av_new.name}\n" \
783
+ " * #{ctes_sch_new.name}\n" \
784
+ " * #{chill_sch_new.name}\n" \
785
+ " * #{loop_sch_new.name}")
786
+
787
+ if old
788
+ runner.registerInfo('However, these schedules are not used in favor of those pre-defined by the user.')
789
+ end
790
+
791
+ end
792
+ # end of new schedule build-----------------------------------------------------------------------------------------
793
+ #-------------------------------------------------------------------------------------------------------------------
794
+
795
+ ## Create Component Setpoint Objects--------------------------------------------------------------------------------
796
+
797
+ if old
798
+ ctes_avail_sched = model.getScheduleRulesetByName(ctes_av).get
799
+ ctes_temp_sched = model.getScheduleRulesetByName(ctes_sch).get
800
+ chill_temp_sched = model.getScheduleRulesetByName(chill_sch).get
801
+ elsif new
802
+ ctes_avail_sched = ctes_av_new
803
+ ctes_temp_sched = ctes_sch_new
804
+ chill_temp_sched = chill_sch_new
805
+ end
806
+
807
+ # Apply ice availability schedule
808
+ ctes.setAvailabilitySchedule(ctes_avail_sched)
809
+
810
+ # Add component setpoint manager for ice tank
811
+ ctes_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, ctes_temp_sched)
812
+ ctes_sp_mgr.addToNode(ctes.outletModelObject.get.to_Node.get)
813
+ ctes_sp_mgr.setName("#{ctes.name} Setpoint Manager")
814
+
815
+ # Add component setpoint manager for ctes chiller
816
+ chill_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, chill_temp_sched)
817
+ chill_sp_mgr.addToNode(ctes_chiller.supplyOutletModelObject.get.to_Node.get)
818
+ chill_sp_mgr.setName("#{ctes_chiller.name} Setpoint Manager")
819
+
820
+ # Replace existing loop setpoint manager - if new = true and old = false
821
+ if new && !old
822
+ loop_sp_node = ctes_loop.loopTemperatureSetpointNode
823
+ loop_sp_mgrs = loop_sp_node.setpointManagers
824
+ loop_sp_mgrs.each do |spm|
825
+ next unless ['Temperature', 'MinimumTemperature', 'MaximumTemperature'].include?(spm.controlVariable)
826
+ spm.disconnect
827
+ runner.registerInfo("Selected loop temperature setpoint manager '#{spm.name}' " \
828
+ "with control variable '#{spm.controlVariable}' was disconnected.")
829
+ end
830
+
831
+ loop_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, loop_sch_new)
832
+ loop_sp_mgr.addToNode(loop_sp_node)
833
+ loop_sp_mgr.setName("#{ctes_loop.name} Setpoint Manager (New)")
834
+
835
+ demand_sp_mgr = loop_sp_mgr.clone.to_SetpointManagerScheduled.get
836
+ demand_sp_mgr.addToNode(ctes_loop.demandInletNode)
837
+ demand_sp_mgr.setName("#{ctes_loop.name} Demand Side Setpoint Manager (New)")
838
+ end
839
+
840
+ # Register info about new schedule objects
841
+ runner.registerInfo('The following component temperature setpoint managers were added to the ' \
842
+ "model:\n" \
843
+ " * #{ctes_sp_mgr.name}\n" \
844
+ " * #{chill_sp_mgr.name}")
845
+
846
+ if old # Old Schedules always take precedence, even if new ones are also created
847
+ runner.registerInfo("The following schedules ared used in the model:\n" \
848
+ " * #{ctes_avail_sched.name}\n" \
849
+ " * #{ctes_temp_sched.name}\n" \
850
+ " * #{chill_temp_sched.name}")
851
+ runner.registerInfo('The following loop temperature setpoint manager was added to the ' \
852
+ "model:\n" \
853
+ " * #{demand_sp_mgr.name}")
854
+ elsif new && !old
855
+ runner.registerInfo('The following loop temperature setpoint managers were added to the ' \
856
+ "model:\n" \
857
+ " * #{loop_sp_mgr.name}\n" \
858
+ " * #{demand_sp_mgr.name}")
859
+ end
860
+
861
+ ## Create EMS Components to Control Load on Upstream (Priority) Device----------------------------------------------
862
+
863
+ # Flag value indicating that a chiller limiter is required or DR Test is Activated
864
+ if chiller_limit != 1.0 || dr == true
865
+
866
+ # Set up EMS output
867
+ output_ems = model.getOutputEnergyManagementSystem
868
+ output_ems.setActuatorAvailabilityDictionaryReporting('Verbose')
869
+ output_ems.setInternalVariableAvailabilityDictionaryReporting('Verbose')
870
+ output_ems.setEMSRuntimeLanguageDebugOutputLevel('None')
871
+
872
+ runner.registerInfo("A #{(chiller_limit * 100).round(2)}% capacity limit has been placed on the chiller " \
873
+ 'during ice discharge. EMS scripting is employed to actuate this control via chiller ' \
874
+ 'outlet setpoint. ')
875
+
876
+ # Internal and Global Variable(s)
877
+ # Chiller Nominal Capacity
878
+ evar_chiller_cap = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, 'Chiller Nominal Capacity')
879
+ evar_chiller_cap.setInternalDataIndexKeyName(ctes_chiller.name.to_s)
880
+ evar_chiller_cap.setName('CTES_Chiller_Capacity')
881
+
882
+ # Chiller Limited Capacity for Ice Discharge Period - Empty Global Variable
883
+ evar_chiller_limit = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'Chiller_Limited_Capacity')
884
+
885
+ # Instances of Chiller Limit Application - Empty Global Variable
886
+ evar_limit_counter = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'Limit_Counter')
887
+
888
+ # Ice Tank thermal storage capacity - Empty Global Variable
889
+ evar_tes_cap = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'TES_Cap')
890
+
891
+ # Max Delta-T for Chiller De-Rate - Empty Global Variable
892
+ dt_ems = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'DT_Max')
893
+
894
+ # DR In-Progress Flag - Empty Global Variable
895
+ dr_flag = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'DR_Flag')
896
+
897
+ # Sensor(s)
898
+ # Evaporator Entering Water Temperature
899
+ eewt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Chiller Evaporator Inlet Temperature')
900
+ eewt.setName('EEWT')
901
+ eewt.setKeyName(ctes_chiller.name.to_s)
902
+
903
+ # Evaporator Leaving Water Temperature
904
+ elwt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Chiller Evaporator Outlet Temperature')
905
+ elwt.setName('ELWT')
906
+ elwt.setKeyName(ctes_chiller.name.to_s)
907
+
908
+ # Evaporator Leave Water Temperature Setpoint
909
+ elwt_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
910
+ elwt_sp.setName('SP')
911
+ elwt_sp.setKeyName(chill_temp_sched.name.to_s)
912
+
913
+ # Supply Water Temperature Setpoint
914
+ swt_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Setpoint Temperature')
915
+ swt_sp.setName('SWT_SP')
916
+ swt_sp.setKeyName(ctes_loop.supplyOutletNode.name.to_s)
917
+
918
+ # Ice Tank Availability Schedule
919
+ av_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
920
+ av_sp.setName('ICE_AV')
921
+ av_sp.setKeyName(ctes.availabilitySchedule.get.name.to_s)
922
+
923
+ # Ice Tank Leaving Water Temperature
924
+ ilwt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Temperature')
925
+ ilwt.setName('ILWT')
926
+ ilwt.setKeyName(ctes.outletModelObject.get.name.to_s)
927
+
928
+ # Ice Tank State of Charge
929
+ soc = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Ice Thermal Storage End Fraction')
930
+ soc.setName('SOC')
931
+ soc.setKeyName(ctes.name.to_s)
932
+
933
+ # Actuator(s)
934
+ # Evaporator Leaving Water Temperature Septoint Node Actuator
935
+ elwt = OpenStudio::Model::EnergyManagementSystemActuator.new(ctes_chiller.supplyOutletModelObject.get,
936
+ 'System Node Setpoint', 'Temperature Setpoint')
937
+ elwt.setName('ELWT_SP')
938
+ end
939
+
940
+ if chiller_limit != 1
941
+ # Program(s)
942
+ # Apply Chiller Capacity Limit During Ice Discharge
943
+ chiller_limit_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
944
+ chiller_limit_program.setName('Chiller_Limiter')
945
+ body = <<-EMS
946
+ IF ( ICE_AV == 1 ) && ( SP >= SWT_SP ) && ( DR_Flag <> 1 )
947
+ IF ( EEWT - SP ) > DT_Max
948
+ SET ELWT_SP = ( EEWT - DT_Max )
949
+ SET Limit_Counter = ( Limit_Counter + ( SystemTimeStep / ZoneTimeStep ) )
950
+ ELSE
951
+ SET ELWT_SP = SP
952
+ ENDIF
953
+ ELSE
954
+ SET ELWT_SP = SP
955
+ ENDIF
956
+ EMS
957
+ chiller_limit_program.setBody(body)
958
+
959
+ # Determine Capacity Limit of the Chiller in Watts (Also initializes limit counter)
960
+ chiller_limit_calculation = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
961
+ chiller_limit_calculation.setName('Chiller_Limit_Calc')
962
+ body = <<-EMS
963
+ SET Chiller_Limited_Capacity = ( #{chiller_limit} * CTES_Chiller_Capacity )
964
+ SET Limit_Counter = 0
965
+ SET DR_Flag = 0
966
+ SET DT_Max = #{dt_max}
967
+ SET TES_Cap = #{storage_capacity}
968
+ EMS
969
+ chiller_limit_calculation.setBody(body)
970
+
971
+ # Program Calling Manager(s)
972
+ chiller_limit_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
973
+ chiller_limit_pcm.setName('Chiller_Limiter_CallMgr')
974
+ chiller_limit_pcm.setCallingPoint('InsideHVACSystemIterationLoop')
975
+ chiller_limit_pcm.addProgram(chiller_limit_program)
976
+
977
+ chiller_limit_calc_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
978
+ chiller_limit_calc_pcm.setName('Chiller_Limit_Calc_CallMgr')
979
+ chiller_limit_calc_pcm.setCallingPoint('BeginNewEnvironment')
980
+ chiller_limit_calc_pcm.addProgram(chiller_limit_calculation)
981
+
982
+ # EMS Output Variable(s)
983
+ eout_chiller_cap = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_chiller_cap)
984
+ eout_chiller_cap.setName('Chiller Nominal Capacity')
985
+
986
+ eout_chiller_limit = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_chiller_limit)
987
+ eout_chiller_limit.setName('Chiller Limited Capacity')
988
+
989
+ eout_limit_counter = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_limit_counter)
990
+ eout_limit_counter.setName('Chiller Limit Counter')
991
+
992
+ eout_tes_cap = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_tes_cap)
993
+ eout_tes_cap.setName('Ice Thermal Storage Capacity')
994
+ end
995
+
996
+ ## DR EVENT TESTER EMS --------------------------
997
+ ## Add Demand Response Event Tester if Applicable (EMS Controller Override)-----------------------------------------
998
+
999
+ if dr
1000
+
1001
+ # Create EMS Script that:
1002
+ # => 1. Determines if DR Event has been triggered (inspects date/time)
1003
+ # => 2. Actuates full storage if in a Load Shed DR event
1004
+ # => 3. Actuates ice charging/chiller @ max if in a Load Add DR event
1005
+ # => 4. Allows staged chiller ramp if ice runs out (if selected by user)
1006
+
1007
+ # Create DR EMS Program
1008
+ dr_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
1009
+ dr_program.setName('Demand_Response_Pgm')
1010
+
1011
+ # Define Program Script Based on Permission of Chiller to Operate to Meet Load
1012
+ if dr_chill && dr_add_shed == 'Shed' # Chiller is permitted to pick up unmet load
1013
+ body = <<-EMS
1014
+ IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1015
+ IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1016
+ SET DR_Flag = 1
1017
+ IF ( ILWT - SWT_SP < 0.05 )
1018
+ SET ELWT_SP = EEWT
1019
+ ELSEIF ( ILWT - SWT_SP <= 0.33 * DT_Max )
1020
+ SET ELWT_SP = EEWT - ( 0.33 * DT_Max )
1021
+ ELSEIF ( ILWT - SWT_SP <= 0.67 * DT_Max )
1022
+ SET ELWT_SP = EEWT - ( 0.67 * DT_Max )
1023
+ ELSE
1024
+ SET ELWT_SP = ( EEWT - DT_Max )
1025
+ SET Limit_Counter = ( Limit_Counter + ( SystemTimeStep / ZoneTimeStep ) )
1026
+ ENDIF
1027
+ ELSEIF ( DR_Flag == 1 )
1028
+ SET DR_Flag = 0
1029
+ SET ELWT_SP = SP
1030
+ ENDIF
1031
+ ENDIF
1032
+ EMS
1033
+ dr_program.setBody(body)
1034
+ elsif !dr_chill && dr_add_shed == 'Shed' # Chiller is not permitted to pick up unmet load when ice is deficient
1035
+ body = <<-EMS
1036
+ IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1037
+ IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1038
+ SET DR_Flag = 1
1039
+ SET ELWT_SP = EEWT + 10.0
1040
+ ELSEIF ( DR_Flag == 1 )
1041
+ SET DR_Flag = 0
1042
+ ENDIF
1043
+ ENDIF
1044
+ EMS
1045
+ dr_program.setBody(body)
1046
+ elsif dr_add_shed == 'Add'
1047
+ body = <<-EMS
1048
+ IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1049
+ IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1050
+ SET DR_Flag = 1
1051
+ IF ( SOC < 0.99 ) && ( ICE_AV == 1 )
1052
+ SET ELWT_SP = #{chg_sp}
1053
+ ELSEIF SOC > 0.95
1054
+ SET ELWT_SP = SWT_SP
1055
+ ENDIF
1056
+ ELSEIF ( DR_Flag == 1 )
1057
+ SET DR_Flag = 0
1058
+ ENDIF
1059
+ ENDIF
1060
+ EMS
1061
+ dr_program.setBody(body)
1062
+ end
1063
+
1064
+ # Create DR EMS Program Calling Manager
1065
+ dr_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
1066
+ dr_pcm.setName('Demand_Response_PCM')
1067
+ dr_pcm.setCallingPoint('InsideHVACSystemIterationLoop')
1068
+ dr_pcm.addProgram(dr_program)
1069
+
1070
+ # EMS Output Variable(s)
1071
+ eout_drflag = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, dr_flag)
1072
+ eout_drflag.setName('Demand Response Flag')
1073
+ end
1074
+ ## END DR EVENT TESTER EMS ---------------------
1075
+
1076
+ ## Add Output Variables and Meters----------------------------------------------------------------------------------
1077
+
1078
+ # Identify existing output variables
1079
+ vars = model.getOutputVariables
1080
+ var_names = []
1081
+ vars.each do |v|
1082
+ var_names << v.variableName
1083
+ end
1084
+
1085
+ # List names of desired output variables
1086
+ ovar_names = ['Ice Thermal Storage Cooling Rate',
1087
+ 'Ice Thermal Storage Cooling Charge Rate',
1088
+ 'Ice Thermal Storage Cooling Discharge Rate',
1089
+ 'Ice Thermal Storage Cooling Charge Energy',
1090
+ 'Ice Thermal Storage Cooling Discharge Energy',
1091
+ 'Ice Thermal Storage Cooling Discharge Energy',
1092
+ 'Ice Thermal Storage End Fraction',
1093
+ 'Ice Thermal Storage On Coil Fraction',
1094
+ 'Ice Thermal Storage Mass Flow Rate',
1095
+ 'Ice Thermal Storage Tank Mass Flow Rate',
1096
+ 'Ice Thermal Storage Bypass Mass Flow Rate',
1097
+ 'Ice Thermal Storage Fluid Inlet Temperature',
1098
+ 'Ice Thermal Storage Tank Outlet Temperature',
1099
+ 'Ice Thermal Storage Blended Outlet Temperature',
1100
+ 'Ice Thermal Storage Ancillary Electric Power',
1101
+ 'Ice Thermal Storage Ancillary Electric Energy',
1102
+ 'Chiller COP',
1103
+ 'Chiller Cycling Ratio',
1104
+ 'Chiller Part Load Ratio',
1105
+ 'Chiller Electric Power',
1106
+ 'Chiller Electric Energy',
1107
+ 'Chiller Evaporator Cooling Rate',
1108
+ 'Chiller Evaporator Cooling Energy',
1109
+ 'Chiller Condenser Heat Transfer Rate',
1110
+ 'Chiller Condenser Heat Transfer Energy',
1111
+ 'Chiller False Load Heat Transfer Rate',
1112
+ 'Chiller False Load Heat Transfer Energy',
1113
+ 'Chiller Evaporator Inlet Temperature',
1114
+ 'Chiller Evaporator Outlet Temperature',
1115
+ 'Chiller Evaporator Mass Flow Rate',
1116
+ 'Site Outdoor Air Drybulb Temperature',
1117
+ 'Site Outdoor Air Wetbulb Temperature']
1118
+
1119
+ # Create new output variables if they do not already exist
1120
+ ovars = []
1121
+ ovar_names.each do |nm|
1122
+ # if !var_names.include?(nm)
1123
+ ovars << OpenStudio::Model::OutputVariable.new(nm, model)
1124
+ # end
1125
+ end
1126
+
1127
+ # Create output variable for loop demand inlet temperature
1128
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1129
+ v.setKeyValue(ctes_loop.demandInletNode.name.to_s)
1130
+ ovars << v
1131
+
1132
+ # Create output variable for loop demand outlet temperature
1133
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1134
+ v.setKeyValue(ctes_loop.demandOutletNode.name.to_s)
1135
+ ovars << v
1136
+
1137
+ # Create output variable for loop supply inlet temperature
1138
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1139
+ v.setKeyValue(ctes_loop.supplyInletNode.name.to_s)
1140
+ ovars << v
1141
+
1142
+ # Create output variable for loop supply outlet temperature
1143
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1144
+ v.setKeyValue(ctes_loop.supplyOutletNode.name.to_s)
1145
+ ovars << v
1146
+
1147
+ # Create output variable for chiller inlet temperature
1148
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1149
+ v.setKeyValue(ctes_chiller.supplyInletModelObject.get.name.to_s)
1150
+ ovars << v
1151
+
1152
+ # Create output variable for chiller outlet temperature
1153
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1154
+ v.setKeyValue(ctes_chiller.supplyOutletModelObject.get.name.to_s)
1155
+ ovars << v
1156
+
1157
+ # Create output variable for ice tank inlet temperature
1158
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1159
+ v.setKeyValue(ctes.inletModelObject.get.name.to_s)
1160
+ ovars << v
1161
+
1162
+ # Create output variable for ice tank outlet temperature
1163
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1164
+ v.setKeyValue(ctes.outletModelObject.get.name.to_s)
1165
+ ovars << v
1166
+
1167
+ # Create output variables for the new operating schedules
1168
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1169
+ v.setKeyValue(ctes_avail_sched.name.to_s)
1170
+ ovars << v
1171
+
1172
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1173
+ v.setKeyValue(ctes_temp_sched.name.to_s)
1174
+ ovars << v
1175
+
1176
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1177
+ v.setKeyValue(chill_temp_sched.name.to_s)
1178
+ ovars << v
1179
+
1180
+ # Create output variable for plant loop setpoint temperature - if new = true
1181
+ if new
1182
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1183
+ v.setKeyValue(loop_sch_new.name.to_s)
1184
+ ovars << v
1185
+ end
1186
+
1187
+ # Create output variables for ice discharge performance curve
1188
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 1 Value', model)
1189
+ v.setKeyValue(ctes.dischargingCurve.name.to_s)
1190
+ v.setName('Discharge Curve Input Value 1')
1191
+ ovars << v
1192
+
1193
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 2 Value', model)
1194
+ v.setKeyValue(ctes.dischargingCurve.name.to_s)
1195
+ v.setName('Discharge Curve Input Value 2')
1196
+ ovars << v
1197
+
1198
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1199
+ v.setKeyValue(ctes.dischargingCurve.name.to_s)
1200
+ v.setName('Discharge Curve Output Value')
1201
+ ovars << v
1202
+
1203
+ # Create output variables for ice charge performance curve
1204
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 1 Value', model)
1205
+ v.setKeyValue(ctes.chargingCurve.name.to_s)
1206
+ v.setName('Charge Curve Input Value 1')
1207
+ ovars << v
1208
+
1209
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 2 Value', model)
1210
+ v.setKeyValue(ctes.chargingCurve.name.to_s)
1211
+ v.setName('Charge Curve Input Value 2')
1212
+ ovars << v
1213
+
1214
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1215
+ v.setKeyValue(ctes.chargingCurve.name.to_s)
1216
+ v.setName('Charge Curve Output Value')
1217
+ ovars << v
1218
+
1219
+ # Create output variables for chiller performance
1220
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1221
+ v.setKeyValue(ctes_chiller.coolingCapacityFunctionOfTemperature.name.to_s)
1222
+ v.setName('Charge Curve Output Value')
1223
+ ovars << v
1224
+
1225
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1226
+ v.setKeyValue(ctes_chiller.electricInputToCoolingOutputRatioFunctionOfTemperature.name.to_s)
1227
+ v.setName('Charge Curve Output Value')
1228
+ ovars << v
1229
+
1230
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1231
+ v.setKeyValue(ctes_chiller.electricInputToCoolingOutputRatioFunctionOfPLR.name.to_s)
1232
+ v.setName('Charge Curve Output Value')
1233
+ ovars << v
1234
+
1235
+ if chiller_limit != 1.0 # flag for EMS use, following EMS vars only exist if previous script ran
1236
+
1237
+ # Create output variable for chiller nominal capacity (from EMS Output Variable)
1238
+ v = OpenStudio::Model::OutputVariable.new(eout_chiller_cap.name.to_s, model)
1239
+ v.setName("#{ctes_chiller.name} Nominal Capacity [W]")
1240
+ v.setVariableName('Chiller Nominal Capacity')
1241
+ ovars << v
1242
+
1243
+ # Create output variable for chiller limited capacity (from EMS Output Variable)
1244
+ v = OpenStudio::Model::OutputVariable.new(eout_chiller_limit.name.to_s, model)
1245
+ v.setName("#{ctes_chiller.name} Limited Capacity")
1246
+ v.setVariableName('Chiller Limited Capacity')
1247
+ ovars << v
1248
+
1249
+ # Create output variable for chiller limit counter (from EMS Output Variable)
1250
+ v = OpenStudio::Model::OutputVariable.new(eout_limit_counter.name.to_s, model)
1251
+ v.setName("#{ctes_chiller.name} Limit Counter [Zone Timesteps]")
1252
+ v.setVariableName('Chiller Limit Counter')
1253
+ ovars << v
1254
+
1255
+ end
1256
+
1257
+ # Create output variable for Demand Response Flag (from EMS Output Variable)
1258
+ if dr
1259
+
1260
+ v = OpenStudio::Model::OutputVariable.new(eout_drflag.name.to_s, model)
1261
+ v.setName('Demand Response Event Flag')
1262
+ v.setVariableName('Demand Response Flag')
1263
+ ovars << v
1264
+
1265
+ end
1266
+
1267
+ # Create output variable for TES Capacity (from EMS Global Variable)
1268
+ v = OpenStudio::Model::OutputVariable.new(eout_tes_cap.name.to_s, model)
1269
+ v.setName("#{ctes.name} Ice Thermal Storage Capacity [GJ]")
1270
+ v.setVariableName('Ice Thermal Storage Capacity')
1271
+ ovars << v
1272
+
1273
+ # Set variable reporting frequency for newly created output variables
1274
+ ovars.each do |var|
1275
+ var.setReportingFrequency(report_freq)
1276
+ end
1277
+
1278
+ # Register info about new output variables
1279
+ runner.registerInfo("#{ovars.size} chiller and ice storage output variables were added to the model.")
1280
+
1281
+ # Create new energy/specific end use meters
1282
+ omet_names = ['Pumps:Electricity',
1283
+ 'Fans:Electricity',
1284
+ 'Cooling:Electricity',
1285
+ 'Electricity:HVAC',
1286
+ 'Electricity:Plant',
1287
+ 'Electricity:Building',
1288
+ 'Electricity:Facility']
1289
+
1290
+ omet_names.each do |nm|
1291
+ omet = OpenStudio::Model::OutputMeter.new(model)
1292
+ omet.setName(nm)
1293
+ omet.setReportingFrequency(report_freq)
1294
+ omet.setMeterFileOnly(false)
1295
+ omet.setCumulative(false)
1296
+ end
1297
+
1298
+ # Register info about new output meters
1299
+ runner.registerInfo("#{omet_names.size} output meters were added to the model.")
1300
+
1301
+ ## Report Final Condition of Model----------------------------------------------------------------------------------
1302
+ total_storage = model.getThermalStorageIceDetaileds.size
1303
+ runner.registerFinalCondition("The model finished with #{total_storage} ice energy storage device(s).")
1304
+
1305
+ true
1306
+ end
1307
+ end
1308
+
1309
+ # register the measure to be used by the application
1310
+ AddIceStorageToPlantLoopForLoadFlexibility.new.registerWithApplication