openstudio-load-flexibility-measures 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +36 -36
  3. data/.rubocop.yml +6 -9
  4. data/CHANGELOG.html +75 -75
  5. data/CHANGELOG.md +59 -37
  6. data/Gemfile +31 -7
  7. data/Jenkinsfile +11 -0
  8. data/README.md +53 -42
  9. data/Rakefile +15 -15
  10. data/doc_templates/LICENSE.md +26 -26
  11. data/doc_templates/README.md.erb +41 -41
  12. data/doc_templates/copyright_erb.txt +35 -35
  13. data/doc_templates/copyright_js.txt +3 -3
  14. data/doc_templates/copyright_ruby.txt +33 -33
  15. data/lib/measures/add_central_ice_storage/LICENSE.md +26 -26
  16. data/lib/measures/add_central_ice_storage/README.md +264 -264
  17. data/lib/measures/add_central_ice_storage/README.md.erb +41 -41
  18. data/lib/measures/add_central_ice_storage/measure.rb +1325 -1324
  19. data/lib/measures/add_central_ice_storage/measure.xml +503 -503
  20. data/lib/measures/add_central_ice_storage/resources/OsLib_Schedules.rb +171 -173
  21. data/lib/measures/add_central_ice_storage/tests/add_central_ice_storage_test.rb +203 -203
  22. data/lib/measures/add_central_ice_storage/tests/ice_test_model.osm +21523 -21523
  23. data/lib/measures/add_hpwh/LICENSE.md +26 -26
  24. data/lib/measures/add_hpwh/README.md +186 -186
  25. data/lib/measures/add_hpwh/README.md.erb +41 -41
  26. data/lib/measures/add_hpwh/docs/Flexible Domestic Hot Water Implementation Guide.pdf +0 -0
  27. data/lib/measures/add_hpwh/measure.rb +663 -647
  28. data/lib/measures/add_hpwh/measure.xml +402 -397
  29. data/lib/measures/add_hpwh/tests/SmallHotel-2A.osm +42893 -42893
  30. data/lib/measures/add_hpwh/tests/{add_hphw_test.rb → add_hpwh_test.rb} +137 -98
  31. data/lib/measures/add_packaged_ice_storage/LICENSE.md +26 -26
  32. data/lib/measures/add_packaged_ice_storage/README.html +185 -185
  33. data/lib/measures/add_packaged_ice_storage/README.md +189 -189
  34. data/lib/measures/add_packaged_ice_storage/measure.rb +694 -691
  35. data/lib/measures/add_packaged_ice_storage/measure.xml +245 -245
  36. data/lib/measures/add_packaged_ice_storage/resources/TESCurves.idf +1059 -1059
  37. data/lib/measures/add_packaged_ice_storage/tests/MeasureTest.osm +9507 -9507
  38. data/lib/measures/add_packaged_ice_storage/tests/add_packaged_ice_storage_test.rb +96 -96
  39. data/lib/openstudio/load_flexibility_measures/version.rb +40 -40
  40. data/lib/openstudio/load_flexibility_measures.rb +50 -50
  41. data/openstudio-load-flexibility-measures.gemspec +33 -32
  42. metadata +27 -27
@@ -1,1324 +1,1325 @@
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 AddCentralIceStorage < OpenStudio::Measure::ModelMeasure
53
- # human readable name
54
- def name
55
- # Measure name should be the title case of the class name.
56
- 'Add Central Ice Storage'
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.75)
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(1.0)
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.setDefaultValue(false)
296
- args << dr
297
-
298
- # Make choice argument for type of demand response event (add or shed)
299
- dr_add_shed = OpenStudio::Measure::OSArgument.makeChoiceArgument('dr_add_shed', ['Add', 'Shed'], false)
300
- dr_add_shed.setDisplayName('Select if a Load Add or Load Shed Event')
301
- dr_add_shed.setDefaultValue('Shed')
302
- args << dr_add_shed
303
-
304
- # Make string argument for DR event date
305
- dr_date = OpenStudio::Measure::OSArgument.makeStringArgument('dr_date', false)
306
- dr_date.setDisplayName('Enter date of demand response event:')
307
- dr_date.setDescription('Use MM/DD format.')
308
- dr_date.setDefaultValue('9/19')
309
- args << dr_date
310
-
311
- # Make string argument for DR Event time
312
- dr_time = OpenStudio::Measure::OSArgument.makeStringArgument('dr_time', false)
313
- dr_time.setDisplayName('Enter start time of demand response event:')
314
- dr_time.setDescription('Use 24 hour format (HR:MM)')
315
- dr_time.setDefaultValue('11:30')
316
- args << dr_time
317
-
318
- # Make double argument for DR event duration
319
- dr_dur = OpenStudio::Measure::OSArgument.makeDoubleArgument('dr_dur', false)
320
- dr_dur.setDisplayName('Enter duration of demand response event [hr]:')
321
- dr_dur.setDefaultValue(3)
322
- args << dr_dur
323
-
324
- # Make boolean argument for allowing chiller to back-up ice
325
- dr_chill = OpenStudio::Measure::OSArgument.makeBoolArgument('dr_chill', false)
326
- dr_chill.setDisplayName('Allow chiller to back-up ice during DR event')
327
- dr_chill.setDescription('Unselection may result in unmet cooling hours')
328
- dr_chill.setDefaultValue('false')
329
- args << dr_chill
330
- ## END DR TESTER INPUTS --------------------------------------
331
-
332
- args
333
- end
334
-
335
- # define what happens when the measure is run
336
- def run(model, runner, user_arguments)
337
- super(model, runner, user_arguments)
338
-
339
- # use the built-in error checking
340
- unless runner.validateUserArguments(arguments(model), user_arguments)
341
- return false
342
- end
343
-
344
- ## Arguments and Declarations---------------------------------------------------------------------------------------
345
- # Assign user arguments to variables
346
- objective = runner.getStringArgumentValue('objective', user_arguments)
347
- upstream = runner.getStringArgumentValue('upstream', user_arguments)
348
- storage_capacity = runner.getDoubleArgumentValue('storage_capacity', user_arguments)
349
- melt_indicator = runner.getStringArgumentValue('melt_indicator', user_arguments)
350
- selected_loop = runner.getStringArgumentValue('selected_loop', user_arguments)
351
- selected_chiller = runner.getStringArgumentValue('selected_chiller', user_arguments)
352
- chiller_resize_factor = runner.getDoubleArgumentValue('chiller_resize_factor', user_arguments)
353
- chiller_limit = runner.getDoubleArgumentValue('chiller_limit', user_arguments)
354
- old = runner.getBoolArgumentValue('old', user_arguments)
355
- ctes_av = runner.getStringArgumentValue('ctes_av', user_arguments)
356
- ctes_sch = runner.getStringArgumentValue('ctes_sch', user_arguments)
357
- chill_sch = runner.getStringArgumentValue('chill_sch', user_arguments)
358
- new = runner.getBoolArgumentValue('new', user_arguments)
359
- loop_sp = runner.getDoubleArgumentValue('loop_sp', user_arguments)
360
- inter_sp = runner.getDoubleArgumentValue('inter_sp', user_arguments)
361
- chg_sp = runner.getDoubleArgumentValue('chg_sp', user_arguments)
362
- delta_t = runner.getStringArgumentValue('delta_t', user_arguments)
363
- ctes_season = runner.getStringArgumentValue('ctes_season', user_arguments)
364
- discharge_start = runner.getStringArgumentValue('discharge_start', user_arguments)
365
- discharge_end = runner.getStringArgumentValue('discharge_end', user_arguments)
366
- charge_start = runner.getStringArgumentValue('charge_start', user_arguments)
367
- charge_end = runner.getStringArgumentValue('charge_end', user_arguments)
368
- wknds = runner.getBoolArgumentValue('wknds', user_arguments)
369
- report_freq = runner.getStringArgumentValue('report_freq', user_arguments)
370
-
371
- ## DR TESTER INPUTS ----------------------------------
372
- dr = runner.getBoolArgumentValue('dr', user_arguments)
373
- dr_add_shed = runner.getStringArgumentValue('dr_add_shed', user_arguments)
374
- (dr_mon, dr_day) = runner.getStringArgumentValue('dr_date', user_arguments).split('/')
375
- (dr_hr, dr_min) = runner.getStringArgumentValue('dr_time', user_arguments).split(':')
376
- dr_dur = runner.getDoubleArgumentValue('dr_dur', user_arguments)
377
- dr_chill = runner.getBoolArgumentValue('dr_chill', user_arguments)
378
- dr_time = (dr_hr.to_f + (dr_min.to_f / 60)).round(2)
379
- ## END DR TESTER INPUTS ------------------------------
380
-
381
- # Declare useful variables with values set within do-loops
382
- cond_loop = ''
383
- ctes_sp_sched = ''
384
- ctes_setpoint = 99.0 # This is a flag value and should be overwritten later
385
- demand_sp_mgr = ''
386
-
387
- ## Validate User Inputs---------------------------------------------------------------------------------------------
388
-
389
- # Convert thermal storage capacity from ton-hours to GJ
390
- storage_capacity = 12_660_670.23144e-9 * storage_capacity
391
-
392
- # Check for existence of charging setpoint temperature, reset to default if left blank.
393
- if chg_sp.nil? || chg_sp >= 32.0
394
- runner.registerWarning('An invalid ice charging temperature was entered. Value reset to -3.88 C (25.0 F).')
395
- chg_sp = 25.0
396
- elsif chg_sp < 20.0
397
- runner.registerWarning('The ice charging temperature is set below 20 F; this is atypically low. Verify input.')
398
- end
399
-
400
- # Convert setpoint temperature inputs from F to C
401
- loop_sp = (loop_sp - 32.0) / 1.8
402
- inter_sp = (inter_sp - 32.0) / 1.8
403
- chg_sp = (chg_sp - 32.0) / 1.8
404
-
405
- # Check if both old and new are false, set new = true and report error
406
- if !old && !new
407
- runner.registerError('No CTES schedule option was selected; either use old schedules or the create new option. ' \
408
- 'Measure aborted.')
409
- return false
410
- end
411
-
412
- # Locate selected chiller and verify loop connection
413
- if !model.getChillerElectricEIRByName(selected_chiller).empty?
414
- ctes_chiller = model.getChillerElectricEIRByName(selected_chiller).get
415
- elsif !model.getChillerAbsorptionByName(selected_chiller).empty?
416
- ctes_chiller = model.getChillerAbsorptionByName(selected_chiller).get
417
- elsif !model.getChillerAbsorptionIndirectByName(selected_chiller).empty?
418
- ctes_chiller = model.getChillerAbsorptionIndirectByName(selected_chiller).get
419
- end
420
-
421
- ctes_loop = model.getModelObjectByName(selected_loop).get.to_PlantLoop.get
422
-
423
- unless ctes_loop.components.include?(ctes_chiller)
424
- runner.registerError('The selected chiller is not located on the selected chilled water loop. Measure aborted.')
425
- return false
426
- end
427
-
428
- # Convert Delta T if needed from F to C (Overwrites string variables as floats)
429
- if delta_t != 'Use Existing Loop Value' && delta_t.to_f != 0.0
430
- delta_t = delta_t.to_f / 1.8
431
- else
432
- # Could add additional checks here for invalid (non-numerical) entries
433
- delta_t = ctes_loop.sizingPlant.loopDesignTemperatureDifference
434
- end
435
-
436
- # Check chiller limit input values
437
- if chiller_limit > 1.0
438
- runner.registerWarning('Chiller limit must be a ratio less than 1. Limit set to 1.0.')
439
- chiller_limit = 1.0
440
- elsif chiller_limit < 0
441
- runner.registerWarning('Chiller limit must be a ratio greater than or equal to 0. Limit set to 0.0' \
442
- ' (i.e. full storage).')
443
- chiller_limit = 0.0
444
- elsif chiller_limit < 0.15
445
- runner.registerInfo('Chiller limit is below 15%; this may be outside the reasonable part load operating " \
446
- "window for the device. Consider increasing or setting to 0.')
447
- end
448
-
449
- # Convert chiller limit to a temperature value based on delta_t variable if != 1. Otherwise, use as flag for EMS
450
- if chiller_limit != 1.0
451
- dt_max = chiller_limit * delta_t # degrees C
452
- runner.registerInfo("Max chiller dT during ice discharge is set to: #{dt_max.round(2)} C " \
453
- "(#{(dt_max * 1.8).round(2)} F).")
454
- else
455
- dt_max = delta_t
456
- end
457
-
458
- # Check limits of chiller performance curves and adjust if necessary - Notify user with WARNING
459
- curve_output_check = false
460
- cap_ft = ctes_chiller.coolingCapacityFunctionOfTemperature
461
- min_x = cap_ft.minimumValueofx.to_f
462
- if min_x > chg_sp
463
- cap_ft.setMinimumValueofx(chg_sp)
464
- runner.registerWarning("VERIFY CURVE VALIDITY: The input range for the '#{cap_ft.name}' curve is too " \
465
- 'restrictive for use with ice charging. The provided curve has been ' \
466
- "extrapolated to a lower limit of #{chg_sp.round(2)} C for the 'x' variable.")
467
- curve_output_check = true
468
- end
469
-
470
- eir_ft = ctes_chiller.electricInputToCoolingOutputRatioFunctionOfTemperature
471
- min_x = eir_ft.minimumValueofx.to_f
472
- if min_x > chg_sp
473
- runner.registerWarning("VERIFY CURVE VALIDITY: The input range for the '#{eir_ft.name}' curve is too " \
474
- 'restrictive for use with ice charging. The provided curve has been ' \
475
- "extrapolated to a lower limit of #{chg_sp.round(2)} C for the 'x' variable.")
476
- end
477
-
478
- # Report chiller performance derate at the ice-making conditions.
479
- if curve_output_check == true
480
- derate = cap_ft.evaluate(chg_sp, ctes_chiller.referenceEnteringCondenserFluidTemperature)
481
- runner.registerInfo('A curve extrapolation warning was registered for the chiller capacity as a function ' \
482
- 'of temperature curve. At normal ice making temperatures, a chiller derate to 60-70% ' \
483
- 'of nominal capacity is expected. Using a condenser entering water temperature of ' \
484
- "#{ctes_chiller.referenceEnteringCondenserFluidTemperature.round(1)} C and the ice " \
485
- "charging temperature of #{chg_sp.round(1)} C, a derate to #{(derate * 100).round(1)}% is " \
486
- 'returned. This value will increase with lower condenser fluid return temperatures.')
487
- end
488
-
489
- # Check to ensure schedules are selected if old = true
490
- if old
491
- if ctes_av == 'N/A'
492
- runner.registerError('Pre-Defined schedule option chosen, but no availabity schedule was selected.')
493
- runner.registerWarning('Measure terminated early; no storage was applied.')
494
- return false
495
- end
496
- if ctes_sch == 'N/A'
497
- runner.registerError('Pre-Defined schedule option chosen, but no ice tank setpoint schedule was selected.')
498
- runner.registerWarning('Measure terminated early; no storage was applied.')
499
- return false
500
- end
501
- if chill_sch == 'N/A'
502
- runner.registerError('Pre-Defined schedule option chosen, but no chiller setpoint schedule was selected.')
503
- runner.registerWarning('Measure terminated early; no storage was applied.')
504
- return false
505
- end
506
- end
507
-
508
- # Parse and verify schedule inputs
509
- # Remove potential spaces from date inputs
510
- ctes_season = ctes_season.delete(' ')
511
-
512
- # Convert HR:MM format into HR.fraction format
513
- (d_start_hr, d_start_min) = discharge_start.split(':')
514
- (d_end_hr, d_end_min) = discharge_end.split(':')
515
- (c_start_hr, c_start_min) = charge_start.split(':')
516
- (c_end_hr, c_end_min) = charge_end.split(':')
517
-
518
- # Store re-formatted time values in shorthand variables for use in schedule
519
- # building
520
- ds = (d_start_hr.to_f + d_start_min.to_f / 60).round(2)
521
- de = (d_end_hr.to_f + d_end_min.to_f / 60).round(2)
522
- cs = (c_start_hr.to_f + c_start_min.to_f / 60).round(2)
523
- ce = (c_end_hr.to_f + c_end_min.to_f / 60).round(2)
524
-
525
- # Verify that input times make sense
526
- if ds > de
527
- runner.registerWarning('Dischage start time is later than discharge ' \
528
- 'end time (your ice will discharge overnight). ' \
529
- 'Verify schedule inputs.')
530
- end
531
-
532
- if cs.between?(ds - 0.01, de + 0.01) || ce.between?(ds - 0.01, de + 0.01)
533
- runner.registerWarning('The tank charge and discharge periods overlap. ' \
534
- 'Examine results for unexpected operation; ' \
535
- 'verify schedule inputs.')
536
- end
537
-
538
- if [ds, de, cs, ce].any? { |i| i > 24 }
539
- runner.registerError('One of you time enteries exceeds 24:00, ' \
540
- 'resulting in a schedule error. Measure aborted.')
541
- return false
542
- end
543
-
544
- ## Report Initial Condition of the Model----------------------------------------------------------------------------
545
- total_storage = model.getThermalStorageIceDetaileds.size
546
- runner.registerInitialCondition("The model started with #{total_storage} ice storage device(s).")
547
-
548
- runner.registerInfo("Chiller '#{selected_chiller}' on Loop '#{selected_loop}' was selected for the addition " \
549
- "of a #{storage_capacity.round(2)} GJ (#{(storage_capacity / 12_660_670.23144e-9).round(0)} " \
550
- 'ton-hours) ice thermal energy storage object.')
551
-
552
- ## Modify Chiller Settings------------------------------------------------------------------------------------------
553
-
554
- # Adjust ctes chiller minimum outlet temperature
555
- ctes_chiller.setLeavingChilledWaterLowerTemperatureLimit(chg_sp)
556
- runner.registerInfo("Selected chiller minimum setpoint temperature was adjusted to #{chg_sp.round(2)} C " \
557
- "(#{(chg_sp * 1.8) + 32} F).")
558
-
559
- # Adjust ctes chiller sizing factor based on user input
560
- if ctes_chiller.isReferenceCapacityAutosized
561
- ctes_chiller.setSizingFactor(chiller_resize_factor)
562
- runner.registerInfo("Selected chiller has been resized to #{chiller_resize_factor * 100}% of autosized " \
563
- 'capacity.')
564
- else
565
- ctes_chiller.setReferenceCapacity(
566
- chiller_resize_factor * ctes_chiller.referenceCapacity.to_f
567
- )
568
- runner.registerInfo("Selected chiller has been resized to #{chiller_resize_factor * 100}% of original " \
569
- '(hardsized) capacity.')
570
- end
571
-
572
- ## Modify Loop Settings---------------------------------------------------------------------------------------------
573
-
574
- # Adjust minimum loop temperature
575
- ctes_loop.setMinimumLoopTemperature(chg_sp)
576
- runner.registerInfo("Selected loop minimum temperature was adjusted to #{chg_sp.round(2)} C " \
577
- "(#{(chg_sp * 1.8) + 32} F).")
578
-
579
- # Adjust plant load distribution scheme
580
- if ctes_loop.loadDistributionScheme != 'SequentialLoad'
581
- ctes_loop.setLoadDistributionScheme('SequentialLoad')
582
- runner.registerInfo("Selected loop load distribution scheme was set to 'SequentialLoad'.")
583
- end
584
-
585
- # Adjust loop design temperature difference
586
- if ctes_loop.sizingPlant.loopDesignTemperatureDifference.to_f != delta_t
587
- ctes_loop.sizingPlant.setLoopDesignTemperatureDifference(delta_t)
588
- runner.registerInfo("Selected loop design temperature difference was adjusted to #{delta_t.round(2)} C " \
589
- "(#{(delta_t * 1.8)} F).")
590
- end
591
-
592
- # Adjust loop gylcol solution percentage and set glycol - if necessary
593
- if ctes_loop.fluidType == 'Water'
594
- ctes_loop.setFluidType('EthyleneGlycol')
595
- ctes_loop.setGlycolConcentration(25)
596
- runner.registerInfo('Selected loop working fluid changed to ethylene glycol at a 25% concentration.')
597
- elsif ctes_loop.glycolConcentration < 25
598
- runner.registerInfo('Selected loop gylycol concentration is less than 25%. Consider increasing to 25-30%.')
599
- end
600
-
601
- # Adjust loop to two-way common pipe simulation - if necessary
602
- if ctes_loop.commonPipeSimulation != 'TwoWayCommonPipe'
603
- ctes_loop.setCommonPipeSimulation('TwoWayCommonPipe')
604
- runner.registerInfo("Selected loop common pipe simulation changed to 'TwoWayCommonPipe'.")
605
-
606
- # Add setpoint manager at inlet of demand loop (req'd for two-way common pipe sim.)
607
- if old # Only applies if old curves are used, regardless of whether new curves are created (old takes precedence)
608
- loop_sp_node = ctes_loop.loopTemperatureSetpointNode
609
- loop_sp_mgrs = loop_sp_node.setpointManagers
610
- loop_sp_mgrs.each do |spm|
611
- if spm.controlVariable == 'Temperature'
612
- demand_sp_mgr = spm.clone.to_SetpointManagerScheduled.get
613
- end
614
- end
615
- demand_sp_mgr.addToNode(ctes_loop.demandInletNode)
616
- runner.registerInfo('Original loop temperature setpoint manager duplicated and added to demand loop inlet node.')
617
- end
618
-
619
- end
620
-
621
- ## Create CTES Hardware---------------------------------------------------------------------------------------------
622
-
623
- # Create ice tank (aka ctes)
624
- ctes = OpenStudio::Model::ThermalStorageIceDetailed.new(model)
625
- ctes.setCapacity(storage_capacity)
626
- ctes.setThawProcessIndicator(melt_indicator)
627
-
628
- # Add ice tank to loop based on user-selected objective option and upstream device
629
- if objective == 'Full Storage'
630
- # Full storage places the ice tank upstream of the chiller with no user option to change.
631
- ctes.addToNode(ctes_chiller.supplyInletModelObject.get.to_Node.get)
632
- elsif objective == 'Partial Storage' && upstream == 'Storage'
633
- ctes.addToNode(ctes_chiller.supplyInletModelObject.get.to_Node.get)
634
- elsif objective == 'Partial Storage' && upstream == 'Chiller'
635
- ctes.addToNode(ctes_chiller.supplyOutletModelObject.get.to_Node.get)
636
- end
637
-
638
- ## Create New Schedules if Necessary--------------------------------------------------------------------------------
639
- #-------------------------------------------------------------------------------------------------------------------
640
- if new
641
- ## Check for Schedule Type Limits and Create if Needed------------------------------------------------------------
642
- if model.getModelObjectByName('OnOff').get.initialized
643
- sched_limits_onoff = model.getModelObjectByName('OnOff').get.to_ScheduleTypeLimits.get
644
- else
645
- sched_limits_onoff = OpenStudio::Model::ScheduleTypeLimits.new(model)
646
- sched_limits_onoff.setName('OnOff')
647
- sched_limits_onoff.setNumericType('Discrete')
648
- sched_limits_onoff.setUnitType('Availability')
649
- sched_limits_onoff.setLowerLimitValue(0.0)
650
- sched_limits_onoff.setUpperLimitValue(1.0)
651
- end
652
-
653
- if model.getModelObjectByName('Temperature').get.initialized
654
- sched_limits_temp = model.getModelObjectByName('Temperature').get.to_ScheduleTypeLimits.get
655
- if sched_limits_temp.lowerLimitValue.to_f > chg_sp
656
- sched_limits_temp.setLowerLimitValue(chg_sp)
657
- end
658
- else
659
- sched_limits_temp = OpenStudio::Model::ScheduleTypeLimits.new(model)
660
- sched_limits_temp.setName('Temperature')
661
- sched_limits_temp.setNumericType('Continuous')
662
- sched_limits_temp.setUnitType('Temperature')
663
- end
664
-
665
- ## Create Schedules-----------------------------------------------------------------------------------------------
666
-
667
- # Create key-value sets based on user inputs for charge/discharge times
668
- # cs = charge start, ce = charge end, ds = discharge start, de = discharge end
669
-
670
- # Set chiller and ice discharge setpoints for partial storage configs
671
- if objective == 'Full Storage'
672
- chiller_setpoint = loop_sp
673
- ctes_setpoint = loop_sp
674
- elsif objective == 'Partial Storage'
675
- if upstream == 'Chiller'
676
- chiller_setpoint = inter_sp
677
- ctes_setpoint = loop_sp
678
- elsif upstream == 'Storage'
679
- chiller_setpoint = loop_sp
680
- ctes_setpoint = inter_sp
681
- end
682
- end
683
-
684
- # Handle overnight charging and discharging
685
- if ce < cs
686
- midnight_av = [24, 1]
687
- midnight_chiller = [24, chg_sp]
688
- midnight_ctes = [24, loop_sp]
689
- elsif de < ds
690
- midnight_av = [24, 1]
691
- midnight_chiller = [24, chiller_setpoint]
692
- midnight_ctes = [24, ctes_setpoint]
693
- else
694
- midnight_av = [24, 0]
695
- midnight_chiller = [24, loop_sp]
696
- midnight_ctes = [24, 99]
697
- end
698
-
699
- # Availablity k-v sets for CTES
700
- wk_av = [[cs, 0], [ce, 1], [ds, 0], [de, 1], midnight_av].sort
701
- wknd_av = [[cs, 0], [ce, 1], midnight_av].sort
702
-
703
- # Temperature k-v sets for CTES
704
- wk_ctes = [[cs, 99], [ce, loop_sp], [ds, 99], [de, ctes_setpoint], midnight_ctes].sort
705
- wknd_ctes = [[cs, 99], [ce, loop_sp], midnight_ctes].sort
706
-
707
- # Temperature k-v set for Chiller
708
- wk_chiller = [[cs, loop_sp], [ce, chg_sp], [ds, loop_sp], [de, chiller_setpoint], midnight_chiller].sort
709
- wknd_chiller = [[cs, loop_sp], [ce, chg_sp], midnight_chiller].sort
710
-
711
- # Apply weekends modifer if necessary
712
- if wknds
713
- wknd_av = wk_av
714
- wknd_ctes = wk_ctes
715
- wknd_chiller = wk_chiller
716
- end
717
-
718
- # Create ice availability schedule
719
- ruleset_name = 'Ice Availability Schedule (New)'
720
- winter_design_day = [[24, 0]]
721
- summer_design_day = wk_av
722
- default_day = ['AllDays'] + [[24, 0]]
723
- rules = []
724
- rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_av
725
- rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_av
726
- options_ctes = { 'name' => ruleset_name,
727
- 'winter_design_day' => winter_design_day,
728
- 'summer_design_day' => summer_design_day,
729
- 'default_day' => default_day,
730
- 'rules' => rules }
731
- ctes_av_new = OsLib_Schedules.createComplexSchedule(model, options_ctes)
732
- ctes_av_new.setScheduleTypeLimits(sched_limits_onoff)
733
-
734
- # Create ctes setpoint temperature schedule
735
- ruleset_name = "#{ctes.name} Setpoint Schedule (New)"
736
- winter_design_day = [[24, 99]]
737
- summer_design_day = wk_ctes
738
- default_day = ['AllDays'] + [[24, 99]]
739
- rules = []
740
- rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_ctes
741
- rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_ctes
742
- options_ctes_ctes = { 'name' => ruleset_name,
743
- 'winter_design_day' => winter_design_day,
744
- 'summer_design_day' => summer_design_day,
745
- 'default_day' => default_day,
746
- 'rules' => rules }
747
- ctes_sch_new = OsLib_Schedules.createComplexSchedule(model, options_ctes_ctes)
748
- ctes_sch_new.setScheduleTypeLimits(sched_limits_temp)
749
-
750
- # Create chiller setpoint temperature schedule
751
- ruleset_name = "#{ctes_chiller.name} Setpoint Schedule (New)"
752
- winter_design_day = [[24, loop_sp]]
753
- summer_design_day = wk_chiller
754
- default_day = ['AllDays'] + [[24, loop_sp]]
755
- rules = []
756
- rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_chiller
757
- rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_chiller
758
- options_ctes_chiller = { 'name' => ruleset_name,
759
- 'winter_design_day' => winter_design_day,
760
- 'summer_design_day' => summer_design_day,
761
- 'default_day' => default_day,
762
- 'rules' => rules }
763
- chill_sch_new = OsLib_Schedules.createComplexSchedule(model, options_ctes_chiller)
764
- chill_sch_new.setScheduleTypeLimits(sched_limits_temp)
765
-
766
- # Create loop setpoint temperature schedule - if new = true
767
- if new
768
- ruleset_name = "#{ctes_loop.name} Setpoint Schedule (New)"
769
- options_ctes_loop = { 'name' => ruleset_name,
770
- 'winterTimeValuePairs' => [[24, loop_sp]],
771
- 'summerTimeValuePairs' => [[24, loop_sp]],
772
- 'defaultTimeValuePairs' => [[24, loop_sp]] }
773
- loop_sch_new = OsLib_Schedules.createSimpleSchedule(model, options_ctes_loop)
774
- loop_sch_new.setScheduleTypeLimits(sched_limits_temp)
775
- end
776
-
777
- # Register info about new schedule objects
778
- runner.registerInfo("The following schedules were added to the model:\n" \
779
- " * #{ctes_av_new.name}\n" \
780
- " * #{ctes_sch_new.name}\n" \
781
- " * #{chill_sch_new.name}\n" \
782
- " * #{loop_sch_new.name}")
783
-
784
- if old
785
- runner.registerInfo('However, these schedules are not used in favor of those pre-defined by the user.')
786
- end
787
-
788
- end
789
- # end of new schedule build-----------------------------------------------------------------------------------------
790
- #-------------------------------------------------------------------------------------------------------------------
791
-
792
- ## Create Component Setpoint Objects--------------------------------------------------------------------------------
793
-
794
- if old
795
- ctes_avail_sched = model.getScheduleRulesetByName(ctes_av).get
796
- ctes_temp_sched = model.getScheduleRulesetByName(ctes_sch).get
797
- chill_temp_sched = model.getScheduleRulesetByName(chill_sch).get
798
- elsif new
799
- ctes_avail_sched = ctes_av_new
800
- ctes_temp_sched = ctes_sch_new
801
- chill_temp_sched = chill_sch_new
802
- end
803
-
804
- # Apply ice availability schedule
805
- ctes.setAvailabilitySchedule(ctes_avail_sched)
806
-
807
- # Add component setpoint manager for ice tank
808
- ctes_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, ctes_temp_sched)
809
- ctes_sp_mgr.addToNode(ctes.outletModelObject.get.to_Node.get)
810
- ctes_sp_mgr.setName("#{ctes.name} Setpoint Manager")
811
-
812
- # Add component setpoint manager for ctes chiller
813
- chill_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, chill_temp_sched)
814
- chill_sp_mgr.addToNode(ctes_chiller.supplyOutletModelObject.get.to_Node.get)
815
- chill_sp_mgr.setName("#{ctes_chiller.name} Setpoint Manager")
816
-
817
- # Replace existing loop setpoint manager - if new = true and old = false
818
- if new && !old
819
- loop_sp_node = ctes_loop.loopTemperatureSetpointNode
820
- loop_sp_mgrs = loop_sp_node.setpointManagers
821
- loop_sp_mgrs.each do |spm|
822
- next unless ['Temperature', 'MinimumTemperature', 'MaximumTemperature'].include?(spm.controlVariable)
823
- spm.disconnect
824
- runner.registerInfo("Selected loop temperature setpoint manager '#{spm.name}' " \
825
- "with control variable '#{spm.controlVariable}' was disconnected.")
826
- end
827
-
828
- loop_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, loop_sch_new)
829
- loop_sp_mgr.addToNode(loop_sp_node)
830
- loop_sp_mgr.setName("#{ctes_loop.name} Setpoint Manager (New)")
831
-
832
- demand_sp_mgr = loop_sp_mgr.clone.to_SetpointManagerScheduled.get
833
- demand_sp_mgr.addToNode(ctes_loop.demandInletNode)
834
- demand_sp_mgr.setName("#{ctes_loop.name} Demand Side Setpoint Manager (New)")
835
- end
836
-
837
- # Register info about new schedule objects
838
- runner.registerInfo('The following component temperature setpoint managers were added to the ' \
839
- "model:\n" \
840
- " * #{ctes_sp_mgr.name}\n" \
841
- " * #{chill_sp_mgr.name}")
842
-
843
- if old # Old Schedules always take precedence, even if new ones are also created
844
- runner.registerInfo("The following schedules ared used in the model:\n" \
845
- " * #{ctes_avail_sched.name}\n" \
846
- " * #{ctes_temp_sched.name}\n" \
847
- " * #{chill_temp_sched.name}")
848
- runner.registerInfo('The following loop temperature setpoint manager was added to the ' \
849
- "model:\n" \
850
- " * #{demand_sp_mgr.name}")
851
- elsif new && !old
852
- runner.registerInfo('The following loop temperature setpoint managers were added to the ' \
853
- "model:\n" \
854
- " * #{loop_sp_mgr.name}\n" \
855
- " * #{demand_sp_mgr.name}")
856
- end
857
-
858
- ## Create General EMS Variables for Chiller and TES Capacities------------------------------------------------------
859
-
860
- # Chiller Nominal Capacity Internal Variable
861
- evar_chiller_cap = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, 'Chiller Nominal Capacity')
862
- evar_chiller_cap.setInternalDataIndexKeyName(ctes_chiller.name.to_s)
863
- evar_chiller_cap.setName('CTES_Chiller_Capacity')
864
-
865
- # Ice Tank thermal storage capacity - Empty Global Variable
866
- evar_tes_cap = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'TES_Cap')
867
-
868
- # Set TES Capacity from User Inputs
869
- set_tes_cap = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
870
- set_tes_cap.setName('Set_TES_Cap')
871
- body = <<-EMS
872
- SET TES_Cap = #{storage_capacity}
873
- EMS
874
- set_tes_cap.setBody(body)
875
-
876
- set_tes_cap_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
877
- set_tes_cap_pcm.setName('Set_TES_Cap_CallMgr')
878
- set_tes_cap_pcm.setCallingPoint('BeginNewEnvironment')
879
- set_tes_cap_pcm.addProgram(set_tes_cap)
880
-
881
- ## Create EMS Components to Control Load on Upstream (Priority) Device----------------------------------------------
882
-
883
- # Flag value indicating that a chiller limiter is required or DR Test is Activated
884
- if chiller_limit != 1.0 || dr == true
885
-
886
- # Set up EMS output
887
- output_ems = model.getOutputEnergyManagementSystem
888
- output_ems.setActuatorAvailabilityDictionaryReporting('Verbose')
889
- output_ems.setInternalVariableAvailabilityDictionaryReporting('Verbose')
890
- output_ems.setEMSRuntimeLanguageDebugOutputLevel('None')
891
-
892
- runner.registerInfo("A #{(chiller_limit * 100).round(2)}% capacity limit has been placed on the chiller " \
893
- 'during ice discharge. EMS scripting is employed to actuate this control via chiller ' \
894
- 'outlet setpoint. ')
895
-
896
- # Internal and Global Variable(s)
897
-
898
- # Chiller Limited Capacity for Ice Discharge Period - Empty Global Variable
899
- evar_chiller_limit = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'Chiller_Limited_Capacity')
900
-
901
- # Instances of Chiller Limit Application - Empty Global Variable
902
- evar_limit_counter = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'Limit_Counter')
903
-
904
- # Max Delta-T for Chiller De-Rate - Empty Global Variable
905
- dt_ems = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'DT_Max')
906
-
907
- # DR In-Progress Flag - Empty Global Variable
908
- dr_flag = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'DR_Flag')
909
-
910
- # Sensor(s)
911
- # Evaporator Entering Water Temperature
912
- eewt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Chiller Evaporator Inlet Temperature')
913
- eewt.setName('EEWT')
914
- eewt.setKeyName(ctes_chiller.name.to_s)
915
-
916
- # Evaporator Leaving Water Temperature
917
- elwt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Chiller Evaporator Outlet Temperature')
918
- elwt.setName('ELWT')
919
- elwt.setKeyName(ctes_chiller.name.to_s)
920
-
921
- # Evaporator Leave Water Temperature Setpoint
922
- elwt_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
923
- elwt_sp.setName('SP')
924
- elwt_sp.setKeyName(chill_temp_sched.name.to_s)
925
-
926
- # Supply Water Temperature Setpoint
927
- swt_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Setpoint Temperature')
928
- swt_sp.setName('SWT_SP')
929
- swt_sp.setKeyName(ctes_loop.supplyOutletNode.name.to_s)
930
-
931
- # Ice Tank Availability Schedule
932
- av_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
933
- av_sp.setName('ICE_AV')
934
- av_sp.setKeyName(ctes.availabilitySchedule.get.name.to_s)
935
-
936
- # Ice Tank Leaving Water Temperature
937
- ilwt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Temperature')
938
- ilwt.setName('ILWT')
939
- ilwt.setKeyName(ctes.outletModelObject.get.name.to_s)
940
-
941
- # Ice Tank State of Charge
942
- soc = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Ice Thermal Storage End Fraction')
943
- soc.setName('SOC')
944
- soc.setKeyName(ctes.name.to_s)
945
-
946
- # Actuator(s)
947
- # Evaporator Leaving Water Temperature Septoint Node Actuator
948
- elwt = OpenStudio::Model::EnergyManagementSystemActuator.new(ctes_chiller.supplyOutletModelObject.get,
949
- 'System Node Setpoint', 'Temperature Setpoint')
950
- elwt.setName('ELWT_SP')
951
- end
952
-
953
- if chiller_limit != 1
954
- # Program(s)
955
- # Apply Chiller Capacity Limit During Ice Discharge
956
- chiller_limit_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
957
- chiller_limit_program.setName('Chiller_Limiter')
958
- body = <<-EMS
959
- IF ( ICE_AV == 1 ) && ( SP >= SWT_SP ) && ( DR_Flag <> 1 )
960
- IF ( EEWT - SP ) > DT_Max
961
- SET ELWT_SP = ( EEWT - DT_Max )
962
- SET Limit_Counter = ( Limit_Counter + ( SystemTimeStep / ZoneTimeStep ) )
963
- ELSE
964
- SET ELWT_SP = SP
965
- ENDIF
966
- ELSE
967
- SET ELWT_SP = SP
968
- ENDIF
969
- EMS
970
- chiller_limit_program.setBody(body)
971
-
972
- # Determine Capacity Limit of the Chiller in Watts (Also initializes limit counter)
973
- chiller_limit_calculation = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
974
- chiller_limit_calculation.setName('Chiller_Limit_Calc')
975
- body = <<-EMS
976
- SET Chiller_Limited_Capacity = ( #{chiller_limit} * CTES_Chiller_Capacity )
977
- SET Limit_Counter = 0
978
- SET DR_Flag = 0
979
- SET DT_Max = #{dt_max}
980
- EMS
981
- chiller_limit_calculation.setBody(body)
982
-
983
- # Program Calling Manager(s)
984
- chiller_limit_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
985
- chiller_limit_pcm.setName('Chiller_Limiter_CallMgr')
986
- chiller_limit_pcm.setCallingPoint('InsideHVACSystemIterationLoop')
987
- chiller_limit_pcm.addProgram(chiller_limit_program)
988
-
989
- chiller_limit_calc_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
990
- chiller_limit_calc_pcm.setName('Chiller_Limit_Calc_CallMgr')
991
- chiller_limit_calc_pcm.setCallingPoint('BeginNewEnvironment')
992
- chiller_limit_calc_pcm.addProgram(chiller_limit_calculation)
993
-
994
- # EMS Output Variable(s) - Chiller Limiter Dependent
995
- eout_chiller_limit = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_chiller_limit)
996
- eout_chiller_limit.setName('Chiller Limited Capacity')
997
-
998
- eout_limit_counter = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_limit_counter)
999
- eout_limit_counter.setName('Chiller Limit Counter')
1000
-
1001
- end
1002
-
1003
- ## DR EVENT TESTER EMS --------------------------
1004
- ## Add Demand Response Event Tester if Applicable (EMS Controller Override)-----------------------------------------
1005
-
1006
- if dr
1007
-
1008
- # Create EMS Script that:
1009
- # => 1. Determines if DR Event has been triggered (inspects date/time)
1010
- # => 2. Actuates full storage if in a Load Shed DR event
1011
- # => 3. Actuates ice charging/chiller @ max if in a Load Add DR event
1012
- # => 4. Allows staged chiller ramp if ice runs out (if selected by user)
1013
-
1014
- # Create DR EMS Program
1015
- dr_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
1016
- dr_program.setName('Demand_Response_Pgm')
1017
-
1018
- # Define Program Script Based on Permission of Chiller to Operate to Meet Load
1019
- if dr_chill && dr_add_shed == 'Shed' # Chiller is permitted to pick up unmet load
1020
- body = <<-EMS
1021
- IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1022
- IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1023
- SET DR_Flag = 1
1024
- IF ( ILWT - SWT_SP < 0.05 )
1025
- SET ELWT_SP = EEWT
1026
- ELSEIF ( ILWT - SWT_SP <= 0.33 * DT_Max )
1027
- SET ELWT_SP = EEWT - ( 0.33 * DT_Max )
1028
- ELSEIF ( ILWT - SWT_SP <= 0.67 * DT_Max )
1029
- SET ELWT_SP = EEWT - ( 0.67 * DT_Max )
1030
- ELSE
1031
- SET ELWT_SP = ( EEWT - DT_Max )
1032
- SET Limit_Counter = ( Limit_Counter + ( SystemTimeStep / ZoneTimeStep ) )
1033
- ENDIF
1034
- ELSEIF ( DR_Flag == 1 )
1035
- SET DR_Flag = 0
1036
- SET ELWT_SP = SP
1037
- ENDIF
1038
- ENDIF
1039
- EMS
1040
- dr_program.setBody(body)
1041
- elsif !dr_chill && dr_add_shed == 'Shed' # Chiller is not permitted to pick up unmet load when ice is deficient
1042
- body = <<-EMS
1043
- IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1044
- IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1045
- SET DR_Flag = 1
1046
- SET ELWT_SP = EEWT + 10.0
1047
- ELSEIF ( DR_Flag == 1 )
1048
- SET DR_Flag = 0
1049
- ENDIF
1050
- ENDIF
1051
- EMS
1052
- dr_program.setBody(body)
1053
- elsif dr_add_shed == 'Add'
1054
- body = <<-EMS
1055
- IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1056
- IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1057
- SET DR_Flag = 1
1058
- IF ( SOC < 0.99 ) && ( ICE_AV == 1 )
1059
- SET ELWT_SP = #{chg_sp}
1060
- ELSEIF SOC > 0.95
1061
- SET ELWT_SP = SWT_SP
1062
- ENDIF
1063
- ELSEIF ( DR_Flag == 1 )
1064
- SET DR_Flag = 0
1065
- ENDIF
1066
- ENDIF
1067
- EMS
1068
- dr_program.setBody(body)
1069
- end
1070
-
1071
- # Create DR EMS Program Calling Manager
1072
- dr_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
1073
- dr_pcm.setName('Demand_Response_PCM')
1074
- dr_pcm.setCallingPoint('InsideHVACSystemIterationLoop')
1075
- dr_pcm.addProgram(dr_program)
1076
-
1077
- # EMS Output Variable(s)
1078
- eout_drflag = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, dr_flag)
1079
- eout_drflag.setName('Demand Response Flag')
1080
- end
1081
- ## END DR EVENT TESTER EMS ---------------------
1082
-
1083
- ## Add Output Variables and Meters----------------------------------------------------------------------------------
1084
-
1085
- # EMS Output Variable(s) - Chiller Limit Independent
1086
- eout_chiller_cap = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_chiller_cap)
1087
- eout_chiller_cap.setName('Chiller Nominal Capacity')
1088
-
1089
- eout_tes_cap = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_tes_cap)
1090
- eout_tes_cap.setName('Ice Thermal Storage Capacity')
1091
-
1092
- # Identify existing output variables
1093
- vars = model.getOutputVariables
1094
- var_names = []
1095
- vars.each do |v|
1096
- var_names << v.variableName
1097
- end
1098
-
1099
- # List names of desired output variables
1100
- ovar_names = ['Ice Thermal Storage Cooling Rate',
1101
- 'Ice Thermal Storage Cooling Charge Rate',
1102
- 'Ice Thermal Storage Cooling Discharge Rate',
1103
- 'Ice Thermal Storage Cooling Charge Energy',
1104
- 'Ice Thermal Storage Cooling Discharge Energy',
1105
- 'Ice Thermal Storage Cooling Discharge Energy',
1106
- 'Ice Thermal Storage End Fraction',
1107
- 'Ice Thermal Storage On Coil Fraction',
1108
- 'Ice Thermal Storage Mass Flow Rate',
1109
- 'Ice Thermal Storage Tank Mass Flow Rate',
1110
- 'Ice Thermal Storage Bypass Mass Flow Rate',
1111
- 'Ice Thermal Storage Fluid Inlet Temperature',
1112
- 'Ice Thermal Storage Tank Outlet Temperature',
1113
- 'Ice Thermal Storage Blended Outlet Temperature',
1114
- 'Ice Thermal Storage Ancillary Electric Power',
1115
- 'Ice Thermal Storage Ancillary Electric Energy',
1116
- 'Chiller COP',
1117
- 'Chiller Cycling Ratio',
1118
- 'Chiller Part Load Ratio',
1119
- 'Chiller Electric Power',
1120
- 'Chiller Electric Energy',
1121
- 'Chiller Evaporator Cooling Rate',
1122
- 'Chiller Evaporator Cooling Energy',
1123
- 'Chiller Condenser Heat Transfer Rate',
1124
- 'Chiller Condenser Heat Transfer Energy',
1125
- 'Chiller False Load Heat Transfer Rate',
1126
- 'Chiller False Load Heat Transfer Energy',
1127
- 'Chiller Evaporator Inlet Temperature',
1128
- 'Chiller Evaporator Outlet Temperature',
1129
- 'Chiller Evaporator Mass Flow Rate',
1130
- 'Site Outdoor Air Drybulb Temperature',
1131
- 'Site Outdoor Air Wetbulb Temperature']
1132
-
1133
- # Create new output variables if they do not already exist
1134
- ovars = []
1135
- ovar_names.each do |nm|
1136
- # if !var_names.include?(nm)
1137
- ovars << OpenStudio::Model::OutputVariable.new(nm, model)
1138
- # end
1139
- end
1140
-
1141
- # Create output variable for loop demand inlet temperature
1142
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1143
- v.setKeyValue(ctes_loop.demandInletNode.name.to_s)
1144
- ovars << v
1145
-
1146
- # Create output variable for loop demand outlet temperature
1147
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1148
- v.setKeyValue(ctes_loop.demandOutletNode.name.to_s)
1149
- ovars << v
1150
-
1151
- # Create output variable for loop supply inlet temperature
1152
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1153
- v.setKeyValue(ctes_loop.supplyInletNode.name.to_s)
1154
- ovars << v
1155
-
1156
- # Create output variable for loop supply outlet temperature
1157
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1158
- v.setKeyValue(ctes_loop.supplyOutletNode.name.to_s)
1159
- ovars << v
1160
-
1161
- # Create output variable for chiller inlet temperature
1162
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1163
- v.setKeyValue(ctes_chiller.supplyInletModelObject.get.name.to_s)
1164
- ovars << v
1165
-
1166
- # Create output variable for chiller outlet temperature
1167
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1168
- v.setKeyValue(ctes_chiller.supplyOutletModelObject.get.name.to_s)
1169
- ovars << v
1170
-
1171
- # Create output variable for ice tank inlet temperature
1172
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1173
- v.setKeyValue(ctes.inletModelObject.get.name.to_s)
1174
- ovars << v
1175
-
1176
- # Create output variable for ice tank outlet temperature
1177
- v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1178
- v.setKeyValue(ctes.outletModelObject.get.name.to_s)
1179
- ovars << v
1180
-
1181
- # Create output variables for the new operating schedules
1182
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1183
- v.setKeyValue(ctes_avail_sched.name.to_s)
1184
- ovars << v
1185
-
1186
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1187
- v.setKeyValue(ctes_temp_sched.name.to_s)
1188
- ovars << v
1189
-
1190
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1191
- v.setKeyValue(chill_temp_sched.name.to_s)
1192
- ovars << v
1193
-
1194
- # Create output variable for plant loop setpoint temperature - if new = true
1195
- if new
1196
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1197
- v.setKeyValue(loop_sch_new.name.to_s)
1198
- ovars << v
1199
- end
1200
-
1201
- # Create output variables for ice discharge performance curve
1202
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 1 Value', model)
1203
- v.setKeyValue(ctes.dischargingCurve.name.to_s)
1204
- v.setName('Discharge Curve Input Value 1')
1205
- ovars << v
1206
-
1207
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 2 Value', model)
1208
- v.setKeyValue(ctes.dischargingCurve.name.to_s)
1209
- v.setName('Discharge Curve Input Value 2')
1210
- ovars << v
1211
-
1212
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1213
- v.setKeyValue(ctes.dischargingCurve.name.to_s)
1214
- v.setName('Discharge Curve Output Value')
1215
- ovars << v
1216
-
1217
- # Create output variables for ice charge performance curve
1218
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 1 Value', model)
1219
- v.setKeyValue(ctes.chargingCurve.name.to_s)
1220
- v.setName('Charge Curve Input Value 1')
1221
- ovars << v
1222
-
1223
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 2 Value', model)
1224
- v.setKeyValue(ctes.chargingCurve.name.to_s)
1225
- v.setName('Charge Curve Input Value 2')
1226
- ovars << v
1227
-
1228
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1229
- v.setKeyValue(ctes.chargingCurve.name.to_s)
1230
- v.setName('Charge Curve Output Value')
1231
- ovars << v
1232
-
1233
- # Create output variables for chiller performance
1234
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1235
- v.setKeyValue(ctes_chiller.coolingCapacityFunctionOfTemperature.name.to_s)
1236
- v.setName('Charge Curve Output Value')
1237
- ovars << v
1238
-
1239
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1240
- v.setKeyValue(ctes_chiller.electricInputToCoolingOutputRatioFunctionOfTemperature.name.to_s)
1241
- v.setName('Charge Curve Output Value')
1242
- ovars << v
1243
-
1244
- v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1245
- v.setKeyValue(ctes_chiller.electricInputToCoolingOutputRatioFunctionOfPLR.name.to_s)
1246
- v.setName('Charge Curve Output Value')
1247
- ovars << v
1248
-
1249
- if chiller_limit != 1.0 # flag for EMS use, following EMS vars only exist if previous script ran
1250
-
1251
- # Create output variable for chiller limited capacity (from EMS Output Variable)
1252
- v = OpenStudio::Model::OutputVariable.new(eout_chiller_limit.name.to_s, model)
1253
- v.setName("#{ctes_chiller.name} Limited Capacity")
1254
- v.setVariableName('Chiller Limited Capacity')
1255
- ovars << v
1256
-
1257
- # Create output variable for chiller limit counter (from EMS Output Variable)
1258
- v = OpenStudio::Model::OutputVariable.new(eout_limit_counter.name.to_s, model)
1259
- v.setName("#{ctes_chiller.name} Limit Counter [Zone Timesteps]")
1260
- v.setVariableName('Chiller Limit Counter')
1261
- ovars << v
1262
-
1263
- end
1264
-
1265
- # Create output variable for Demand Response Flag (from EMS Output Variable)
1266
- if dr
1267
-
1268
- v = OpenStudio::Model::OutputVariable.new(eout_drflag.name.to_s, model)
1269
- v.setName('Demand Response Event Flag')
1270
- v.setVariableName('Demand Response Flag')
1271
- ovars << v
1272
-
1273
- end
1274
-
1275
- # Create output variable for TES Capacity (from EMS Global Variable)
1276
- v = OpenStudio::Model::OutputVariable.new(eout_tes_cap.name.to_s, model)
1277
- v.setName("#{ctes.name} Ice Thermal Storage Capacity [GJ]")
1278
- v.setVariableName('Ice Thermal Storage Capacity')
1279
- ovars << v
1280
-
1281
- # Create output variable for chiller nominal capacity (from EMS Output Variable)
1282
- v = OpenStudio::Model::OutputVariable.new(eout_chiller_cap.name.to_s, model)
1283
- v.setName("#{ctes_chiller.name} Nominal Capacity [W]")
1284
- v.setVariableName('Chiller Nominal Capacity')
1285
- ovars << v
1286
-
1287
- # Set variable reporting frequency for newly created output variables
1288
- ovars.each do |var|
1289
- var.setReportingFrequency(report_freq)
1290
- end
1291
-
1292
- # Register info about new output variables
1293
- runner.registerInfo("#{ovars.size} chiller and ice storage output variables were added to the model.")
1294
-
1295
- # Create new energy/specific end use meters
1296
- omet_names = ['Pumps:Electricity',
1297
- 'Fans:Electricity',
1298
- 'Cooling:Electricity',
1299
- 'Electricity:HVAC',
1300
- 'Electricity:Plant',
1301
- 'Electricity:Building',
1302
- 'Electricity:Facility']
1303
-
1304
- omet_names.each do |nm|
1305
- omet = OpenStudio::Model::OutputMeter.new(model)
1306
- omet.setName(nm)
1307
- omet.setReportingFrequency(report_freq)
1308
- omet.setMeterFileOnly(false)
1309
- omet.setCumulative(false)
1310
- end
1311
-
1312
- # Register info about new output meters
1313
- runner.registerInfo("#{omet_names.size} output meters were added to the model.")
1314
-
1315
- ## Report Final Condition of Model----------------------------------------------------------------------------------
1316
- total_storage = model.getThermalStorageIceDetaileds.size
1317
- runner.registerFinalCondition("The model finished with #{total_storage} ice energy storage device(s).")
1318
-
1319
- true
1320
- end
1321
- end
1322
-
1323
- # register the measure to be used by the application
1324
- AddCentralIceStorage.new.registerWithApplication
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2021, 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 AddCentralIceStorage < OpenStudio::Measure::ModelMeasure
53
+ # human readable name
54
+ def name
55
+ # Measure name should be the title case of the class name.
56
+ 'Add Central Ice Storage'
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.75)
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(1.0)
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
+ case sched.scheduleTypeLimits.get.unitType.to_s
171
+ when 'Temperature'
172
+ sched_options << sched.name.to_s
173
+ when 'Availability', '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.setDefaultValue(false)
296
+ args << dr
297
+
298
+ # Make choice argument for type of demand response event (add or shed)
299
+ dr_add_shed = OpenStudio::Measure::OSArgument.makeChoiceArgument('dr_add_shed', ['Add', 'Shed'], false)
300
+ dr_add_shed.setDisplayName('Select if a Load Add or Load Shed Event')
301
+ dr_add_shed.setDefaultValue('Shed')
302
+ args << dr_add_shed
303
+
304
+ # Make string argument for DR event date
305
+ dr_date = OpenStudio::Measure::OSArgument.makeStringArgument('dr_date', false)
306
+ dr_date.setDisplayName('Enter date of demand response event:')
307
+ dr_date.setDescription('Use MM/DD format.')
308
+ dr_date.setDefaultValue('9/19')
309
+ args << dr_date
310
+
311
+ # Make string argument for DR Event time
312
+ dr_time = OpenStudio::Measure::OSArgument.makeStringArgument('dr_time', false)
313
+ dr_time.setDisplayName('Enter start time of demand response event:')
314
+ dr_time.setDescription('Use 24 hour format (HR:MM)')
315
+ dr_time.setDefaultValue('11:30')
316
+ args << dr_time
317
+
318
+ # Make double argument for DR event duration
319
+ dr_dur = OpenStudio::Measure::OSArgument.makeDoubleArgument('dr_dur', false)
320
+ dr_dur.setDisplayName('Enter duration of demand response event [hr]:')
321
+ dr_dur.setDefaultValue(3)
322
+ args << dr_dur
323
+
324
+ # Make boolean argument for allowing chiller to back-up ice
325
+ dr_chill = OpenStudio::Measure::OSArgument.makeBoolArgument('dr_chill', false)
326
+ dr_chill.setDisplayName('Allow chiller to back-up ice during DR event')
327
+ dr_chill.setDescription('Unselection may result in unmet cooling hours')
328
+ dr_chill.setDefaultValue('false')
329
+ args << dr_chill
330
+ ## END DR TESTER INPUTS --------------------------------------
331
+
332
+ args
333
+ end
334
+
335
+ # define what happens when the measure is run
336
+ def run(model, runner, user_arguments)
337
+ super(model, runner, user_arguments)
338
+
339
+ # use the built-in error checking
340
+ unless runner.validateUserArguments(arguments(model), user_arguments)
341
+ return false
342
+ end
343
+
344
+ ## Arguments and Declarations---------------------------------------------------------------------------------------
345
+ # Assign user arguments to variables
346
+ objective = runner.getStringArgumentValue('objective', user_arguments)
347
+ upstream = runner.getStringArgumentValue('upstream', user_arguments)
348
+ storage_capacity = runner.getDoubleArgumentValue('storage_capacity', user_arguments)
349
+ melt_indicator = runner.getStringArgumentValue('melt_indicator', user_arguments)
350
+ selected_loop = runner.getStringArgumentValue('selected_loop', user_arguments)
351
+ selected_chiller = runner.getStringArgumentValue('selected_chiller', user_arguments)
352
+ chiller_resize_factor = runner.getDoubleArgumentValue('chiller_resize_factor', user_arguments)
353
+ chiller_limit = runner.getDoubleArgumentValue('chiller_limit', user_arguments)
354
+ old = runner.getBoolArgumentValue('old', user_arguments)
355
+ ctes_av = runner.getStringArgumentValue('ctes_av', user_arguments)
356
+ ctes_sch = runner.getStringArgumentValue('ctes_sch', user_arguments)
357
+ chill_sch = runner.getStringArgumentValue('chill_sch', user_arguments)
358
+ new = runner.getBoolArgumentValue('new', user_arguments)
359
+ loop_sp = runner.getDoubleArgumentValue('loop_sp', user_arguments)
360
+ inter_sp = runner.getDoubleArgumentValue('inter_sp', user_arguments)
361
+ chg_sp = runner.getDoubleArgumentValue('chg_sp', user_arguments)
362
+ delta_t = runner.getStringArgumentValue('delta_t', user_arguments)
363
+ ctes_season = runner.getStringArgumentValue('ctes_season', user_arguments)
364
+ discharge_start = runner.getStringArgumentValue('discharge_start', user_arguments)
365
+ discharge_end = runner.getStringArgumentValue('discharge_end', user_arguments)
366
+ charge_start = runner.getStringArgumentValue('charge_start', user_arguments)
367
+ charge_end = runner.getStringArgumentValue('charge_end', user_arguments)
368
+ wknds = runner.getBoolArgumentValue('wknds', user_arguments)
369
+ report_freq = runner.getStringArgumentValue('report_freq', user_arguments)
370
+
371
+ ## DR TESTER INPUTS ----------------------------------
372
+ dr = runner.getBoolArgumentValue('dr', user_arguments)
373
+ dr_add_shed = runner.getStringArgumentValue('dr_add_shed', user_arguments)
374
+ (dr_mon, dr_day) = runner.getStringArgumentValue('dr_date', user_arguments).split('/')
375
+ (dr_hr, dr_min) = runner.getStringArgumentValue('dr_time', user_arguments).split(':')
376
+ dr_dur = runner.getDoubleArgumentValue('dr_dur', user_arguments)
377
+ dr_chill = runner.getBoolArgumentValue('dr_chill', user_arguments)
378
+ dr_time = (dr_hr.to_f + (dr_min.to_f / 60)).round(2)
379
+ ## END DR TESTER INPUTS ------------------------------
380
+
381
+ # Declare useful variables with values set within do-loops
382
+ cond_loop = ''
383
+ ctes_sp_sched = ''
384
+ ctes_setpoint = 99.0 # This is a flag value and should be overwritten later
385
+ demand_sp_mgr = ''
386
+
387
+ ## Validate User Inputs---------------------------------------------------------------------------------------------
388
+
389
+ # Convert thermal storage capacity from ton-hours to GJ
390
+ storage_capacity = 0.0126606708 * storage_capacity
391
+
392
+ # Check for existence of charging setpoint temperature, reset to default if left blank.
393
+ if chg_sp.nil? || chg_sp >= 32.0
394
+ runner.registerWarning('An invalid ice charging temperature was entered. Value reset to -3.88 C (25.0 F).')
395
+ chg_sp = 25.0
396
+ elsif chg_sp < 20.0
397
+ runner.registerWarning('The ice charging temperature is set below 20 F; this is atypically low. Verify input.')
398
+ end
399
+
400
+ # Convert setpoint temperature inputs from F to C
401
+ loop_sp = (loop_sp - 32.0) / 1.8
402
+ inter_sp = (inter_sp - 32.0) / 1.8
403
+ chg_sp = (chg_sp - 32.0) / 1.8
404
+
405
+ # Check if both old and new are false, set new = true and report error
406
+ if !old && !new
407
+ runner.registerError('No CTES schedule option was selected; either use old schedules or the create new option. ' \
408
+ 'Measure aborted.')
409
+ return false
410
+ end
411
+
412
+ # Locate selected chiller and verify loop connection
413
+ if !model.getChillerElectricEIRByName(selected_chiller).empty?
414
+ ctes_chiller = model.getChillerElectricEIRByName(selected_chiller).get
415
+ elsif !model.getChillerAbsorptionByName(selected_chiller).empty?
416
+ ctes_chiller = model.getChillerAbsorptionByName(selected_chiller).get
417
+ elsif !model.getChillerAbsorptionIndirectByName(selected_chiller).empty?
418
+ ctes_chiller = model.getChillerAbsorptionIndirectByName(selected_chiller).get
419
+ end
420
+
421
+ ctes_loop = model.getModelObjectByName(selected_loop).get.to_PlantLoop.get
422
+
423
+ unless ctes_loop.components.include?(ctes_chiller)
424
+ runner.registerError('The selected chiller is not located on the selected chilled water loop. Measure aborted.')
425
+ return false
426
+ end
427
+
428
+ # Convert Delta T if needed from F to C (Overwrites string variables as floats)
429
+ if delta_t != 'Use Existing Loop Value' && delta_t.to_f > 0.0
430
+ delta_t = delta_t.to_f / 1.8
431
+ else
432
+ # Could add additional checks here for invalid (non-numerical) entries
433
+ delta_t = ctes_loop.sizingPlant.loopDesignTemperatureDifference
434
+ end
435
+
436
+ # Check chiller limit input values
437
+ if chiller_limit > 1.0
438
+ runner.registerWarning('Chiller limit must be a ratio less than 1. Limit set to 1.0.')
439
+ chiller_limit = 1.0
440
+ elsif chiller_limit < 0
441
+ runner.registerWarning('Chiller limit must be a ratio greater than or equal to 0. Limit set to 0.0' \
442
+ ' (i.e. full storage).')
443
+ chiller_limit = 0.0
444
+ elsif chiller_limit < 0.15
445
+ runner.registerInfo('Chiller limit is below 15%; this may be outside the reasonable part load operating " \
446
+ "window for the device. Consider increasing or setting to 0.')
447
+ end
448
+
449
+ # Convert chiller limit to a temperature value based on delta_t variable if != 1. Otherwise, use as flag for EMS
450
+ if chiller_limit < 1.0
451
+ dt_max = chiller_limit * delta_t # degrees C
452
+ runner.registerInfo("Max chiller dT during ice discharge is set to: #{dt_max.round(2)} C " \
453
+ "(#{(dt_max * 1.8).round(2)} F).")
454
+ else
455
+ dt_max = delta_t
456
+ end
457
+
458
+ # Check limits of chiller performance curves and adjust if necessary - Notify user with WARNING
459
+ curve_output_check = false
460
+ cap_ft = ctes_chiller.coolingCapacityFunctionOfTemperature
461
+ min_x = cap_ft.minimumValueofx.to_f
462
+ if min_x > chg_sp
463
+ cap_ft.setMinimumValueofx(chg_sp)
464
+ runner.registerWarning("VERIFY CURVE VALIDITY: The input range for the '#{cap_ft.name}' curve is too " \
465
+ 'restrictive for use with ice charging. The provided curve has been ' \
466
+ "extrapolated to a lower limit of #{chg_sp.round(2)} C for the 'x' variable.")
467
+ curve_output_check = true
468
+ end
469
+
470
+ eir_ft = ctes_chiller.electricInputToCoolingOutputRatioFunctionOfTemperature
471
+ min_x = eir_ft.minimumValueofx.to_f
472
+ if min_x > chg_sp
473
+ runner.registerWarning("VERIFY CURVE VALIDITY: The input range for the '#{eir_ft.name}' curve is too " \
474
+ 'restrictive for use with ice charging. The provided curve has been ' \
475
+ "extrapolated to a lower limit of #{chg_sp.round(2)} C for the 'x' variable.")
476
+ end
477
+
478
+ # Report chiller performance derate at the ice-making conditions.
479
+ if curve_output_check == true
480
+ derate = cap_ft.evaluate(chg_sp, ctes_chiller.referenceEnteringCondenserFluidTemperature)
481
+ runner.registerInfo('A curve extrapolation warning was registered for the chiller capacity as a function ' \
482
+ 'of temperature curve. At normal ice making temperatures, a chiller derate to 60-70% ' \
483
+ 'of nominal capacity is expected. Using a condenser entering water temperature of ' \
484
+ "#{ctes_chiller.referenceEnteringCondenserFluidTemperature.round(1)} C and the ice " \
485
+ "charging temperature of #{chg_sp.round(1)} C, a derate to #{(derate * 100).round(1)}% is " \
486
+ 'returned. This value will increase with lower condenser fluid return temperatures.')
487
+ end
488
+
489
+ # Check to ensure schedules are selected if old = true
490
+ if old
491
+ if ctes_av == 'N/A'
492
+ runner.registerError('Pre-Defined schedule option chosen, but no availabity schedule was selected.')
493
+ runner.registerWarning('Measure terminated early; no storage was applied.')
494
+ return false
495
+ end
496
+ if ctes_sch == 'N/A'
497
+ runner.registerError('Pre-Defined schedule option chosen, but no ice tank setpoint schedule was selected.')
498
+ runner.registerWarning('Measure terminated early; no storage was applied.')
499
+ return false
500
+ end
501
+ if chill_sch == 'N/A'
502
+ runner.registerError('Pre-Defined schedule option chosen, but no chiller setpoint schedule was selected.')
503
+ runner.registerWarning('Measure terminated early; no storage was applied.')
504
+ return false
505
+ end
506
+ end
507
+
508
+ # Parse and verify schedule inputs
509
+ # Remove potential spaces from date inputs
510
+ ctes_season = ctes_season.delete(' ')
511
+
512
+ # Convert HR:MM format into HR.fraction format
513
+ (d_start_hr, d_start_min) = discharge_start.split(':')
514
+ (d_end_hr, d_end_min) = discharge_end.split(':')
515
+ (c_start_hr, c_start_min) = charge_start.split(':')
516
+ (c_end_hr, c_end_min) = charge_end.split(':')
517
+
518
+ # Store re-formatted time values in shorthand variables for use in schedule
519
+ # building
520
+ ds = (d_start_hr.to_f + d_start_min.to_f / 60).round(2)
521
+ de = (d_end_hr.to_f + d_end_min.to_f / 60).round(2)
522
+ cs = (c_start_hr.to_f + c_start_min.to_f / 60).round(2)
523
+ ce = (c_end_hr.to_f + c_end_min.to_f / 60).round(2)
524
+
525
+ # Verify that input times make sense
526
+ if ds > de
527
+ runner.registerWarning('Dischage start time is later than discharge ' \
528
+ 'end time (your ice will discharge overnight). ' \
529
+ 'Verify schedule inputs.')
530
+ end
531
+
532
+ if cs.between?(ds - 0.01, de + 0.01) || ce.between?(ds - 0.01, de + 0.01)
533
+ runner.registerWarning('The tank charge and discharge periods overlap. ' \
534
+ 'Examine results for unexpected operation; ' \
535
+ 'verify schedule inputs.')
536
+ end
537
+
538
+ if [ds, de, cs, ce].any? { |i| i > 24 }
539
+ runner.registerError('One of you time enteries exceeds 24:00, ' \
540
+ 'resulting in a schedule error. Measure aborted.')
541
+ return false
542
+ end
543
+
544
+ ## Report Initial Condition of the Model----------------------------------------------------------------------------
545
+ total_storage = model.getThermalStorageIceDetaileds.size
546
+ runner.registerInitialCondition("The model started with #{total_storage} ice storage device(s).")
547
+
548
+ runner.registerInfo("Chiller '#{selected_chiller}' on Loop '#{selected_loop}' was selected for the addition " \
549
+ "of a #{storage_capacity.round(2)} GJ (#{(storage_capacity / 0.0126606708).round(0)} " \
550
+ 'ton-hours) ice thermal energy storage object.')
551
+
552
+ ## Modify Chiller Settings------------------------------------------------------------------------------------------
553
+
554
+ # Adjust ctes chiller minimum outlet temperature
555
+ ctes_chiller.setLeavingChilledWaterLowerTemperatureLimit(chg_sp)
556
+ runner.registerInfo("Selected chiller minimum setpoint temperature was adjusted to #{chg_sp.round(2)} C " \
557
+ "(#{(chg_sp * 1.8) + 32} F).")
558
+
559
+ # Adjust ctes chiller sizing factor based on user input
560
+ if ctes_chiller.isReferenceCapacityAutosized
561
+ ctes_chiller.setSizingFactor(chiller_resize_factor)
562
+ runner.registerInfo("Selected chiller has been resized to #{chiller_resize_factor * 100}% of autosized " \
563
+ 'capacity.')
564
+ else
565
+ ctes_chiller.setReferenceCapacity(
566
+ chiller_resize_factor * ctes_chiller.referenceCapacity.to_f
567
+ )
568
+ runner.registerInfo("Selected chiller has been resized to #{chiller_resize_factor * 100}% of original " \
569
+ '(hardsized) capacity.')
570
+ end
571
+
572
+ ## Modify Loop Settings---------------------------------------------------------------------------------------------
573
+
574
+ # Adjust minimum loop temperature
575
+ ctes_loop.setMinimumLoopTemperature(chg_sp)
576
+ runner.registerInfo("Selected loop minimum temperature was adjusted to #{chg_sp.round(2)} C " \
577
+ "(#{(chg_sp * 1.8) + 32} F).")
578
+
579
+ # Adjust plant load distribution scheme
580
+ if ctes_loop.loadDistributionScheme != 'SequentialLoad'
581
+ ctes_loop.setLoadDistributionScheme('SequentialLoad')
582
+ runner.registerInfo("Selected loop load distribution scheme was set to 'SequentialLoad'.")
583
+ end
584
+
585
+ # Adjust loop design temperature difference
586
+ ctes_loop.sizingPlant.setLoopDesignTemperatureDifference(delta_t)
587
+ runner.registerInfo("Selected loop design temperature difference was set to #{delta_t.round(2)} C " \
588
+ "(#{delta_t * 1.8} F).")
589
+
590
+ # Adjust loop gylcol solution percentage and set glycol - if necessary
591
+ if ctes_loop.fluidType == 'Water'
592
+ ctes_loop.setFluidType('EthyleneGlycol')
593
+ ctes_loop.setGlycolConcentration(25)
594
+ runner.registerInfo('Selected loop working fluid changed to ethylene glycol at a 25% concentration.')
595
+ elsif ctes_loop.glycolConcentration < 25
596
+ runner.registerInfo('Selected loop gylycol concentration is less than 25%. Consider increasing to 25-30%.')
597
+ end
598
+
599
+ # Adjust loop to two-way common pipe simulation - if necessary
600
+ if ctes_loop.commonPipeSimulation != 'TwoWayCommonPipe'
601
+ ctes_loop.setCommonPipeSimulation('TwoWayCommonPipe')
602
+ runner.registerInfo("Selected loop common pipe simulation changed to 'TwoWayCommonPipe'.")
603
+
604
+ # Add setpoint manager at inlet of demand loop (req'd for two-way common pipe sim.)
605
+ if old # Only applies if old curves are used, regardless of whether new curves are created (old takes precedence)
606
+ loop_sp_node = ctes_loop.loopTemperatureSetpointNode
607
+ loop_sp_mgrs = loop_sp_node.setpointManagers
608
+ loop_sp_mgrs.each do |spm|
609
+ if spm.controlVariable == 'Temperature'
610
+ demand_sp_mgr = spm.clone.to_SetpointManagerScheduled.get
611
+ end
612
+ end
613
+ demand_sp_mgr.addToNode(ctes_loop.demandInletNode)
614
+ runner.registerInfo('Original loop temperature setpoint manager duplicated and added to demand loop inlet node.')
615
+ end
616
+
617
+ end
618
+
619
+ ## Create CTES Hardware---------------------------------------------------------------------------------------------
620
+
621
+ # Create ice tank (aka ctes)
622
+ ctes = OpenStudio::Model::ThermalStorageIceDetailed.new(model)
623
+ ctes.setCapacity(storage_capacity)
624
+ ctes.setThawProcessIndicator(melt_indicator)
625
+
626
+ # Add ice tank to loop based on user-selected objective option and upstream device
627
+ if objective == 'Full Storage'
628
+ # Full storage places the ice tank upstream of the chiller with no user option to change.
629
+ ctes.addToNode(ctes_chiller.supplyInletModelObject.get.to_Node.get)
630
+ elsif objective == 'Partial Storage' && upstream == 'Storage'
631
+ ctes.addToNode(ctes_chiller.supplyInletModelObject.get.to_Node.get)
632
+ elsif objective == 'Partial Storage' && upstream == 'Chiller'
633
+ ctes.addToNode(ctes_chiller.supplyOutletModelObject.get.to_Node.get)
634
+ end
635
+
636
+ ## Create New Schedules if Necessary--------------------------------------------------------------------------------
637
+ #-------------------------------------------------------------------------------------------------------------------
638
+ if new
639
+ ## Check for Schedule Type Limits and Create if Needed------------------------------------------------------------
640
+ if model.getModelObjectByName('OnOff').get.initialized
641
+ sched_limits_onoff = model.getModelObjectByName('OnOff').get.to_ScheduleTypeLimits.get
642
+ else
643
+ sched_limits_onoff = OpenStudio::Model::ScheduleTypeLimits.new(model)
644
+ sched_limits_onoff.setName('OnOff')
645
+ sched_limits_onoff.setNumericType('Discrete')
646
+ sched_limits_onoff.setUnitType('Availability')
647
+ sched_limits_onoff.setLowerLimitValue(0.0)
648
+ sched_limits_onoff.setUpperLimitValue(1.0)
649
+ end
650
+
651
+ if model.getModelObjectByName('Temperature').get.initialized
652
+ sched_limits_temp = model.getModelObjectByName('Temperature').get.to_ScheduleTypeLimits.get
653
+ if sched_limits_temp.lowerLimitValue.to_f > chg_sp
654
+ sched_limits_temp.setLowerLimitValue(chg_sp)
655
+ end
656
+ else
657
+ sched_limits_temp = OpenStudio::Model::ScheduleTypeLimits.new(model)
658
+ sched_limits_temp.setName('Temperature')
659
+ sched_limits_temp.setNumericType('Continuous')
660
+ sched_limits_temp.setUnitType('Temperature')
661
+ end
662
+
663
+ ## Create Schedules-----------------------------------------------------------------------------------------------
664
+
665
+ # Create key-value sets based on user inputs for charge/discharge times
666
+ # cs = charge start, ce = charge end, ds = discharge start, de = discharge end
667
+
668
+ # Set chiller and ice discharge setpoints for partial storage configs
669
+ case objective
670
+ when 'Full Storage'
671
+ chiller_setpoint = loop_sp
672
+ ctes_setpoint = loop_sp
673
+ when 'Partial Storage'
674
+ case upstream
675
+ when 'Chiller'
676
+ chiller_setpoint = inter_sp
677
+ ctes_setpoint = loop_sp
678
+ when 'Storage'
679
+ chiller_setpoint = loop_sp
680
+ ctes_setpoint = inter_sp
681
+ end
682
+ end
683
+
684
+ # Handle overnight charging and discharging
685
+ if ce < cs
686
+ midnight_av = [24, 1]
687
+ midnight_chiller = [24, chg_sp]
688
+ midnight_ctes = [24, loop_sp]
689
+ elsif de < ds
690
+ midnight_av = [24, 1]
691
+ midnight_chiller = [24, chiller_setpoint]
692
+ midnight_ctes = [24, ctes_setpoint]
693
+ else
694
+ midnight_av = [24, 0]
695
+ midnight_chiller = [24, loop_sp]
696
+ midnight_ctes = [24, 99]
697
+ end
698
+
699
+ # Availablity k-v sets for CTES
700
+ wk_av = [[cs, 0], [ce, 1], [ds, 0], [de, 1], midnight_av].sort
701
+ wknd_av = [[cs, 0], [ce, 1], midnight_av].sort
702
+
703
+ # Temperature k-v sets for CTES
704
+ wk_ctes = [[cs, 99], [ce, loop_sp], [ds, 99], [de, ctes_setpoint], midnight_ctes].sort
705
+ wknd_ctes = [[cs, 99], [ce, loop_sp], midnight_ctes].sort
706
+
707
+ # Temperature k-v set for Chiller
708
+ wk_chiller = [[cs, loop_sp], [ce, chg_sp], [ds, loop_sp], [de, chiller_setpoint], midnight_chiller].sort
709
+ wknd_chiller = [[cs, loop_sp], [ce, chg_sp], midnight_chiller].sort
710
+
711
+ # Apply weekends modifer if necessary
712
+ if wknds
713
+ wknd_av = wk_av
714
+ wknd_ctes = wk_ctes
715
+ wknd_chiller = wk_chiller
716
+ end
717
+
718
+ # Create ice availability schedule
719
+ ruleset_name = 'Ice Availability Schedule (New)'
720
+ winter_design_day = [[24, 0]]
721
+ summer_design_day = wk_av
722
+ default_day = ['AllDays'] + [[24, 0]]
723
+ rules = []
724
+ rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_av
725
+ rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_av
726
+ options_ctes = { 'name' => ruleset_name,
727
+ 'winter_design_day' => winter_design_day,
728
+ 'summer_design_day' => summer_design_day,
729
+ 'default_day' => default_day,
730
+ 'rules' => rules }
731
+ ctes_av_new = OsLib_Schedules.createComplexSchedule(model, options_ctes)
732
+ ctes_av_new.setScheduleTypeLimits(sched_limits_onoff)
733
+
734
+ # Create ctes setpoint temperature schedule
735
+ ruleset_name = "#{ctes.name} Setpoint Schedule (New)"
736
+ winter_design_day = [[24, 99]]
737
+ summer_design_day = wk_ctes
738
+ default_day = ['AllDays'] + [[24, 99]]
739
+ rules = []
740
+ rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_ctes
741
+ rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_ctes
742
+ options_ctes_ctes = { 'name' => ruleset_name,
743
+ 'winter_design_day' => winter_design_day,
744
+ 'summer_design_day' => summer_design_day,
745
+ 'default_day' => default_day,
746
+ 'rules' => rules }
747
+ ctes_sch_new = OsLib_Schedules.createComplexSchedule(model, options_ctes_ctes)
748
+ ctes_sch_new.setScheduleTypeLimits(sched_limits_temp)
749
+
750
+ # Create chiller setpoint temperature schedule
751
+ ruleset_name = "#{ctes_chiller.name} Setpoint Schedule (New)"
752
+ winter_design_day = [[24, loop_sp]]
753
+ summer_design_day = wk_chiller
754
+ default_day = ['AllDays'] + [[24, loop_sp]]
755
+ rules = []
756
+ rules << ['Weekend', ctes_season, 'Sat/Sun'] + wknd_chiller
757
+ rules << ['Summer Weekday', ctes_season, 'Mon/Tue/Wed/Thu/Fri'] + wk_chiller
758
+ options_ctes_chiller = { 'name' => ruleset_name,
759
+ 'winter_design_day' => winter_design_day,
760
+ 'summer_design_day' => summer_design_day,
761
+ 'default_day' => default_day,
762
+ 'rules' => rules }
763
+ chill_sch_new = OsLib_Schedules.createComplexSchedule(model, options_ctes_chiller)
764
+ chill_sch_new.setScheduleTypeLimits(sched_limits_temp)
765
+
766
+ # Create loop setpoint temperature schedule - if new = true
767
+ if new
768
+ ruleset_name = "#{ctes_loop.name} Setpoint Schedule (New)"
769
+ options_ctes_loop = { 'name' => ruleset_name,
770
+ 'winterTimeValuePairs' => [[24, loop_sp]],
771
+ 'summerTimeValuePairs' => [[24, loop_sp]],
772
+ 'defaultTimeValuePairs' => [[24, loop_sp]] }
773
+ loop_sch_new = OsLib_Schedules.createSimpleSchedule(model, options_ctes_loop)
774
+ loop_sch_new.setScheduleTypeLimits(sched_limits_temp)
775
+ end
776
+
777
+ # Register info about new schedule objects
778
+ runner.registerInfo("The following schedules were added to the model:\n" \
779
+ " * #{ctes_av_new.name}\n" \
780
+ " * #{ctes_sch_new.name}\n" \
781
+ " * #{chill_sch_new.name}\n" \
782
+ " * #{loop_sch_new.name}")
783
+
784
+ if old
785
+ runner.registerInfo('However, these schedules are not used in favor of those pre-defined by the user.')
786
+ end
787
+
788
+ end
789
+ # end of new schedule build-----------------------------------------------------------------------------------------
790
+ #-------------------------------------------------------------------------------------------------------------------
791
+
792
+ ## Create Component Setpoint Objects--------------------------------------------------------------------------------
793
+
794
+ if old
795
+ ctes_avail_sched = model.getScheduleRulesetByName(ctes_av).get
796
+ ctes_temp_sched = model.getScheduleRulesetByName(ctes_sch).get
797
+ chill_temp_sched = model.getScheduleRulesetByName(chill_sch).get
798
+ elsif new
799
+ ctes_avail_sched = ctes_av_new
800
+ ctes_temp_sched = ctes_sch_new
801
+ chill_temp_sched = chill_sch_new
802
+ end
803
+
804
+ # Apply ice availability schedule
805
+ ctes.setAvailabilitySchedule(ctes_avail_sched)
806
+
807
+ # Add component setpoint manager for ice tank
808
+ ctes_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, ctes_temp_sched)
809
+ ctes_sp_mgr.addToNode(ctes.outletModelObject.get.to_Node.get)
810
+ ctes_sp_mgr.setName("#{ctes.name} Setpoint Manager")
811
+
812
+ # Add component setpoint manager for ctes chiller
813
+ chill_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, chill_temp_sched)
814
+ chill_sp_mgr.addToNode(ctes_chiller.supplyOutletModelObject.get.to_Node.get)
815
+ chill_sp_mgr.setName("#{ctes_chiller.name} Setpoint Manager")
816
+
817
+ # Replace existing loop setpoint manager - if new = true and old = false
818
+ if new && !old
819
+ loop_sp_node = ctes_loop.loopTemperatureSetpointNode
820
+ loop_sp_mgrs = loop_sp_node.setpointManagers
821
+ loop_sp_mgrs.each do |spm|
822
+ next unless ['Temperature', 'MinimumTemperature', 'MaximumTemperature'].include?(spm.controlVariable)
823
+
824
+ spm.disconnect
825
+ runner.registerInfo("Selected loop temperature setpoint manager '#{spm.name}' " \
826
+ "with control variable '#{spm.controlVariable}' was disconnected.")
827
+ end
828
+
829
+ loop_sp_mgr = OpenStudio::Model::SetpointManagerScheduled.new(model, loop_sch_new)
830
+ loop_sp_mgr.addToNode(loop_sp_node)
831
+ loop_sp_mgr.setName("#{ctes_loop.name} Setpoint Manager (New)")
832
+
833
+ demand_sp_mgr = loop_sp_mgr.clone.to_SetpointManagerScheduled.get
834
+ demand_sp_mgr.addToNode(ctes_loop.demandInletNode)
835
+ demand_sp_mgr.setName("#{ctes_loop.name} Demand Side Setpoint Manager (New)")
836
+ end
837
+
838
+ # Register info about new schedule objects
839
+ runner.registerInfo('The following component temperature setpoint managers were added to the ' \
840
+ "model:\n" \
841
+ " * #{ctes_sp_mgr.name}\n" \
842
+ " * #{chill_sp_mgr.name}")
843
+
844
+ if old # Old Schedules always take precedence, even if new ones are also created
845
+ runner.registerInfo("The following schedules ared used in the model:\n" \
846
+ " * #{ctes_avail_sched.name}\n" \
847
+ " * #{ctes_temp_sched.name}\n" \
848
+ " * #{chill_temp_sched.name}")
849
+ runner.registerInfo('The following loop temperature setpoint manager was added to the ' \
850
+ "model:\n" \
851
+ " * #{demand_sp_mgr.name}")
852
+ elsif new && !old
853
+ runner.registerInfo('The following loop temperature setpoint managers were added to the ' \
854
+ "model:\n" \
855
+ " * #{loop_sp_mgr.name}\n" \
856
+ " * #{demand_sp_mgr.name}")
857
+ end
858
+
859
+ ## Create General EMS Variables for Chiller and TES Capacities------------------------------------------------------
860
+
861
+ # Chiller Nominal Capacity Internal Variable
862
+ evar_chiller_cap = OpenStudio::Model::EnergyManagementSystemInternalVariable.new(model, 'Chiller Nominal Capacity')
863
+ evar_chiller_cap.setInternalDataIndexKeyName(ctes_chiller.name.to_s)
864
+ evar_chiller_cap.setName('CTES_Chiller_Capacity')
865
+
866
+ # Ice Tank thermal storage capacity - Empty Global Variable
867
+ evar_tes_cap = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'TES_Cap')
868
+
869
+ # Set TES Capacity from User Inputs
870
+ set_tes_cap = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
871
+ set_tes_cap.setName('Set_TES_Cap')
872
+ body = <<-EMS
873
+ SET TES_Cap = #{storage_capacity}
874
+ EMS
875
+ set_tes_cap.setBody(body)
876
+
877
+ set_tes_cap_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
878
+ set_tes_cap_pcm.setName('Set_TES_Cap_CallMgr')
879
+ set_tes_cap_pcm.setCallingPoint('BeginNewEnvironment')
880
+ set_tes_cap_pcm.addProgram(set_tes_cap)
881
+
882
+ ## Create EMS Components to Control Load on Upstream (Priority) Device----------------------------------------------
883
+
884
+ # Flag value indicating that a chiller limiter is required or DR Test is Activated
885
+ if chiller_limit < 1.0 || dr == true
886
+
887
+ # Set up EMS output
888
+ output_ems = model.getOutputEnergyManagementSystem
889
+ output_ems.setActuatorAvailabilityDictionaryReporting('Verbose')
890
+ output_ems.setInternalVariableAvailabilityDictionaryReporting('Verbose')
891
+ output_ems.setEMSRuntimeLanguageDebugOutputLevel('None')
892
+
893
+ runner.registerInfo("A #{(chiller_limit * 100).round(2)}% capacity limit has been placed on the chiller " \
894
+ 'during ice discharge. EMS scripting is employed to actuate this control via chiller ' \
895
+ 'outlet setpoint. ')
896
+
897
+ # Internal and Global Variable(s)
898
+
899
+ # Chiller Limited Capacity for Ice Discharge Period - Empty Global Variable
900
+ evar_chiller_limit = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'Chiller_Limited_Capacity')
901
+
902
+ # Instances of Chiller Limit Application - Empty Global Variable
903
+ evar_limit_counter = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'Limit_Counter')
904
+
905
+ # Max Delta-T for Chiller De-Rate - Empty Global Variable
906
+ dt_ems = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'DT_Max')
907
+
908
+ # DR In-Progress Flag - Empty Global Variable
909
+ dr_flag = OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, 'DR_Flag')
910
+
911
+ # Sensor(s)
912
+ # Evaporator Entering Water Temperature
913
+ eewt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Chiller Evaporator Inlet Temperature')
914
+ eewt.setName('EEWT')
915
+ eewt.setKeyName(ctes_chiller.name.to_s)
916
+
917
+ # Evaporator Leaving Water Temperature
918
+ elwt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Chiller Evaporator Outlet Temperature')
919
+ elwt.setName('ELWT')
920
+ elwt.setKeyName(ctes_chiller.name.to_s)
921
+
922
+ # Evaporator Leave Water Temperature Setpoint
923
+ elwt_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
924
+ elwt_sp.setName('SP')
925
+ elwt_sp.setKeyName(chill_temp_sched.name.to_s)
926
+
927
+ # Supply Water Temperature Setpoint
928
+ swt_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Setpoint Temperature')
929
+ swt_sp.setName('SWT_SP')
930
+ swt_sp.setKeyName(ctes_loop.supplyOutletNode.name.to_s)
931
+
932
+ # Ice Tank Availability Schedule
933
+ av_sp = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
934
+ av_sp.setName('ICE_AV')
935
+ av_sp.setKeyName(ctes.availabilitySchedule.get.name.to_s)
936
+
937
+ # Ice Tank Leaving Water Temperature
938
+ ilwt = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Temperature')
939
+ ilwt.setName('ILWT')
940
+ ilwt.setKeyName(ctes.outletModelObject.get.name.to_s)
941
+
942
+ # Ice Tank State of Charge
943
+ soc = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Ice Thermal Storage End Fraction')
944
+ soc.setName('SOC')
945
+ soc.setKeyName(ctes.name.to_s)
946
+
947
+ # Actuator(s)
948
+ # Evaporator Leaving Water Temperature Septoint Node Actuator
949
+ elwt = OpenStudio::Model::EnergyManagementSystemActuator.new(ctes_chiller.supplyOutletModelObject.get,
950
+ 'System Node Setpoint', 'Temperature Setpoint')
951
+ elwt.setName('ELWT_SP')
952
+ end
953
+
954
+ if chiller_limit < 1.0
955
+ # Program(s)
956
+ # Apply Chiller Capacity Limit During Ice Discharge
957
+ chiller_limit_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
958
+ chiller_limit_program.setName('Chiller_Limiter')
959
+ body = <<-EMS
960
+ IF ( ICE_AV == 1 ) && ( SP >= SWT_SP ) && ( DR_Flag <> 1 )
961
+ IF ( EEWT - SP ) > DT_Max
962
+ SET ELWT_SP = ( EEWT - DT_Max )
963
+ SET Limit_Counter = ( Limit_Counter + ( SystemTimeStep / ZoneTimeStep ) )
964
+ ELSE
965
+ SET ELWT_SP = SP
966
+ ENDIF
967
+ ELSE
968
+ SET ELWT_SP = SP
969
+ ENDIF
970
+ EMS
971
+ chiller_limit_program.setBody(body)
972
+
973
+ # Determine Capacity Limit of the Chiller in Watts (Also initializes limit counter)
974
+ chiller_limit_calculation = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
975
+ chiller_limit_calculation.setName('Chiller_Limit_Calc')
976
+ body = <<-EMS
977
+ SET Chiller_Limited_Capacity = ( #{chiller_limit} * CTES_Chiller_Capacity )
978
+ SET Limit_Counter = 0
979
+ SET DR_Flag = 0
980
+ SET DT_Max = #{dt_max}
981
+ EMS
982
+ chiller_limit_calculation.setBody(body)
983
+
984
+ # Program Calling Manager(s)
985
+ chiller_limit_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
986
+ chiller_limit_pcm.setName('Chiller_Limiter_CallMgr')
987
+ chiller_limit_pcm.setCallingPoint('InsideHVACSystemIterationLoop')
988
+ chiller_limit_pcm.addProgram(chiller_limit_program)
989
+
990
+ chiller_limit_calc_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
991
+ chiller_limit_calc_pcm.setName('Chiller_Limit_Calc_CallMgr')
992
+ chiller_limit_calc_pcm.setCallingPoint('BeginNewEnvironment')
993
+ chiller_limit_calc_pcm.addProgram(chiller_limit_calculation)
994
+
995
+ # EMS Output Variable(s) - Chiller Limiter Dependent
996
+ eout_chiller_limit = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_chiller_limit)
997
+ eout_chiller_limit.setName('Chiller Limited Capacity')
998
+
999
+ eout_limit_counter = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_limit_counter)
1000
+ eout_limit_counter.setName('Chiller Limit Counter')
1001
+
1002
+ end
1003
+
1004
+ ## DR EVENT TESTER EMS --------------------------
1005
+ ## Add Demand Response Event Tester if Applicable (EMS Controller Override)-----------------------------------------
1006
+
1007
+ if dr
1008
+
1009
+ # Create EMS Script that:
1010
+ # => 1. Determines if DR Event has been triggered (inspects date/time)
1011
+ # => 2. Actuates full storage if in a Load Shed DR event
1012
+ # => 3. Actuates ice charging/chiller @ max if in a Load Add DR event
1013
+ # => 4. Allows staged chiller ramp if ice runs out (if selected by user)
1014
+
1015
+ # Create DR EMS Program
1016
+ dr_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
1017
+ dr_program.setName('Demand_Response_Pgm')
1018
+
1019
+ # Define Program Script Based on Permission of Chiller to Operate to Meet Load
1020
+ if dr_chill && dr_add_shed == 'Shed' # Chiller is permitted to pick up unmet load
1021
+ body = <<-EMS
1022
+ IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1023
+ IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1024
+ SET DR_Flag = 1
1025
+ IF ( ILWT - SWT_SP < 0.05 )
1026
+ SET ELWT_SP = EEWT
1027
+ ELSEIF ( ILWT - SWT_SP <= 0.33 * DT_Max )
1028
+ SET ELWT_SP = EEWT - ( 0.33 * DT_Max )
1029
+ ELSEIF ( ILWT - SWT_SP <= 0.67 * DT_Max )
1030
+ SET ELWT_SP = EEWT - ( 0.67 * DT_Max )
1031
+ ELSE
1032
+ SET ELWT_SP = ( EEWT - DT_Max )
1033
+ SET Limit_Counter = ( Limit_Counter + ( SystemTimeStep / ZoneTimeStep ) )
1034
+ ENDIF
1035
+ ELSEIF ( DR_Flag == 1 )
1036
+ SET DR_Flag = 0
1037
+ SET ELWT_SP = SP
1038
+ ENDIF
1039
+ ENDIF
1040
+ EMS
1041
+ dr_program.setBody(body)
1042
+ elsif !dr_chill && dr_add_shed == 'Shed' # Chiller is not permitted to pick up unmet load when ice is deficient
1043
+ body = <<-EMS
1044
+ IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1045
+ IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1046
+ SET DR_Flag = 1
1047
+ SET ELWT_SP = EEWT + 10.0
1048
+ ELSEIF ( DR_Flag == 1 )
1049
+ SET DR_Flag = 0
1050
+ ENDIF
1051
+ ENDIF
1052
+ EMS
1053
+ dr_program.setBody(body)
1054
+ elsif dr_add_shed == 'Add'
1055
+ body = <<-EMS
1056
+ IF ( Month == #{dr_mon} ) && ( DayOfMonth == #{dr_day} )
1057
+ IF ( CurrentTime > #{dr_time} ) && ( CurrentTime <= #{dr_time + dr_dur} )
1058
+ SET DR_Flag = 1
1059
+ IF ( SOC < 0.99 ) && ( ICE_AV == 1 )
1060
+ SET ELWT_SP = #{chg_sp}
1061
+ ELSEIF SOC > 0.95
1062
+ SET ELWT_SP = SWT_SP
1063
+ ENDIF
1064
+ ELSEIF ( DR_Flag == 1 )
1065
+ SET DR_Flag = 0
1066
+ ENDIF
1067
+ ENDIF
1068
+ EMS
1069
+ dr_program.setBody(body)
1070
+ end
1071
+
1072
+ # Create DR EMS Program Calling Manager
1073
+ dr_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
1074
+ dr_pcm.setName('Demand_Response_PCM')
1075
+ dr_pcm.setCallingPoint('InsideHVACSystemIterationLoop')
1076
+ dr_pcm.addProgram(dr_program)
1077
+
1078
+ # EMS Output Variable(s)
1079
+ eout_drflag = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, dr_flag)
1080
+ eout_drflag.setName('Demand Response Flag')
1081
+ end
1082
+ ## END DR EVENT TESTER EMS ---------------------
1083
+
1084
+ ## Add Output Variables and Meters----------------------------------------------------------------------------------
1085
+
1086
+ # EMS Output Variable(s) - Chiller Limit Independent
1087
+ eout_chiller_cap = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_chiller_cap)
1088
+ eout_chiller_cap.setName('Chiller Nominal Capacity')
1089
+
1090
+ eout_tes_cap = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, evar_tes_cap)
1091
+ eout_tes_cap.setName('Ice Thermal Storage Capacity')
1092
+
1093
+ # Identify existing output variables
1094
+ vars = model.getOutputVariables
1095
+ var_names = []
1096
+ vars.each do |v|
1097
+ var_names << v.variableName
1098
+ end
1099
+
1100
+ # List names of desired output variables
1101
+ ovar_names = ['Ice Thermal Storage Cooling Rate',
1102
+ 'Ice Thermal Storage Cooling Charge Rate',
1103
+ 'Ice Thermal Storage Cooling Discharge Rate',
1104
+ 'Ice Thermal Storage Cooling Charge Energy',
1105
+ 'Ice Thermal Storage Cooling Discharge Energy',
1106
+ 'Ice Thermal Storage Cooling Discharge Energy',
1107
+ 'Ice Thermal Storage End Fraction',
1108
+ 'Ice Thermal Storage On Coil Fraction',
1109
+ 'Ice Thermal Storage Mass Flow Rate',
1110
+ 'Ice Thermal Storage Tank Mass Flow Rate',
1111
+ 'Ice Thermal Storage Bypass Mass Flow Rate',
1112
+ 'Ice Thermal Storage Fluid Inlet Temperature',
1113
+ 'Ice Thermal Storage Tank Outlet Temperature',
1114
+ 'Ice Thermal Storage Blended Outlet Temperature',
1115
+ 'Ice Thermal Storage Ancillary Electric Power',
1116
+ 'Ice Thermal Storage Ancillary Electric Energy',
1117
+ 'Chiller COP',
1118
+ 'Chiller Cycling Ratio',
1119
+ 'Chiller Part Load Ratio',
1120
+ 'Chiller Electric Power',
1121
+ 'Chiller Electric Energy',
1122
+ 'Chiller Evaporator Cooling Rate',
1123
+ 'Chiller Evaporator Cooling Energy',
1124
+ 'Chiller Condenser Heat Transfer Rate',
1125
+ 'Chiller Condenser Heat Transfer Energy',
1126
+ 'Chiller False Load Heat Transfer Rate',
1127
+ 'Chiller False Load Heat Transfer Energy',
1128
+ 'Chiller Evaporator Inlet Temperature',
1129
+ 'Chiller Evaporator Outlet Temperature',
1130
+ 'Chiller Evaporator Mass Flow Rate',
1131
+ 'Site Outdoor Air Drybulb Temperature',
1132
+ 'Site Outdoor Air Wetbulb Temperature']
1133
+
1134
+ # Create new output variables if they do not already exist
1135
+ ovars = []
1136
+ ovar_names.each do |nm|
1137
+ # if !var_names.include?(nm)
1138
+ ovars << OpenStudio::Model::OutputVariable.new(nm, model)
1139
+ # end
1140
+ end
1141
+
1142
+ # Create output variable for loop demand inlet temperature
1143
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1144
+ v.setKeyValue(ctes_loop.demandInletNode.name.to_s)
1145
+ ovars << v
1146
+
1147
+ # Create output variable for loop demand outlet temperature
1148
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1149
+ v.setKeyValue(ctes_loop.demandOutletNode.name.to_s)
1150
+ ovars << v
1151
+
1152
+ # Create output variable for loop supply inlet temperature
1153
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1154
+ v.setKeyValue(ctes_loop.supplyInletNode.name.to_s)
1155
+ ovars << v
1156
+
1157
+ # Create output variable for loop supply outlet temperature
1158
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1159
+ v.setKeyValue(ctes_loop.supplyOutletNode.name.to_s)
1160
+ ovars << v
1161
+
1162
+ # Create output variable for chiller inlet temperature
1163
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1164
+ v.setKeyValue(ctes_chiller.supplyInletModelObject.get.name.to_s)
1165
+ ovars << v
1166
+
1167
+ # Create output variable for chiller outlet temperature
1168
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1169
+ v.setKeyValue(ctes_chiller.supplyOutletModelObject.get.name.to_s)
1170
+ ovars << v
1171
+
1172
+ # Create output variable for ice tank inlet temperature
1173
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1174
+ v.setKeyValue(ctes.inletModelObject.get.name.to_s)
1175
+ ovars << v
1176
+
1177
+ # Create output variable for ice tank outlet temperature
1178
+ v = OpenStudio::Model::OutputVariable.new('System Node Temperature', model)
1179
+ v.setKeyValue(ctes.outletModelObject.get.name.to_s)
1180
+ ovars << v
1181
+
1182
+ # Create output variables for the new operating schedules
1183
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1184
+ v.setKeyValue(ctes_avail_sched.name.to_s)
1185
+ ovars << v
1186
+
1187
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1188
+ v.setKeyValue(ctes_temp_sched.name.to_s)
1189
+ ovars << v
1190
+
1191
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1192
+ v.setKeyValue(chill_temp_sched.name.to_s)
1193
+ ovars << v
1194
+
1195
+ # Create output variable for plant loop setpoint temperature - if new = true
1196
+ if new
1197
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
1198
+ v.setKeyValue(loop_sch_new.name.to_s)
1199
+ ovars << v
1200
+ end
1201
+
1202
+ # Create output variables for ice discharge performance curve
1203
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 1 Value', model)
1204
+ v.setKeyValue(ctes.dischargingCurve.name.to_s)
1205
+ v.setName('Discharge Curve Input Value 1')
1206
+ ovars << v
1207
+
1208
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 2 Value', model)
1209
+ v.setKeyValue(ctes.dischargingCurve.name.to_s)
1210
+ v.setName('Discharge Curve Input Value 2')
1211
+ ovars << v
1212
+
1213
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1214
+ v.setKeyValue(ctes.dischargingCurve.name.to_s)
1215
+ v.setName('Discharge Curve Output Value')
1216
+ ovars << v
1217
+
1218
+ # Create output variables for ice charge performance curve
1219
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 1 Value', model)
1220
+ v.setKeyValue(ctes.chargingCurve.name.to_s)
1221
+ v.setName('Charge Curve Input Value 1')
1222
+ ovars << v
1223
+
1224
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Input Variable 2 Value', model)
1225
+ v.setKeyValue(ctes.chargingCurve.name.to_s)
1226
+ v.setName('Charge Curve Input Value 2')
1227
+ ovars << v
1228
+
1229
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1230
+ v.setKeyValue(ctes.chargingCurve.name.to_s)
1231
+ v.setName('Charge Curve Output Value')
1232
+ ovars << v
1233
+
1234
+ # Create output variables for chiller performance
1235
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1236
+ v.setKeyValue(ctes_chiller.coolingCapacityFunctionOfTemperature.name.to_s)
1237
+ v.setName('Charge Curve Output Value')
1238
+ ovars << v
1239
+
1240
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1241
+ v.setKeyValue(ctes_chiller.electricInputToCoolingOutputRatioFunctionOfTemperature.name.to_s)
1242
+ v.setName('Charge Curve Output Value')
1243
+ ovars << v
1244
+
1245
+ v = OpenStudio::Model::OutputVariable.new('Performance Curve Output Value', model)
1246
+ v.setKeyValue(ctes_chiller.electricInputToCoolingOutputRatioFunctionOfPLR.name.to_s)
1247
+ v.setName('Charge Curve Output Value')
1248
+ ovars << v
1249
+
1250
+ if chiller_limit < 1.0 # flag for EMS use, following EMS vars only exist if previous script ran
1251
+
1252
+ # Create output variable for chiller limited capacity (from EMS Output Variable)
1253
+ v = OpenStudio::Model::OutputVariable.new(eout_chiller_limit.name.to_s, model)
1254
+ v.setName("#{ctes_chiller.name} Limited Capacity")
1255
+ v.setVariableName('Chiller Limited Capacity')
1256
+ ovars << v
1257
+
1258
+ # Create output variable for chiller limit counter (from EMS Output Variable)
1259
+ v = OpenStudio::Model::OutputVariable.new(eout_limit_counter.name.to_s, model)
1260
+ v.setName("#{ctes_chiller.name} Limit Counter [Zone Timesteps]")
1261
+ v.setVariableName('Chiller Limit Counter')
1262
+ ovars << v
1263
+
1264
+ end
1265
+
1266
+ # Create output variable for Demand Response Flag (from EMS Output Variable)
1267
+ if dr
1268
+
1269
+ v = OpenStudio::Model::OutputVariable.new(eout_drflag.name.to_s, model)
1270
+ v.setName('Demand Response Event Flag')
1271
+ v.setVariableName('Demand Response Flag')
1272
+ ovars << v
1273
+
1274
+ end
1275
+
1276
+ # Create output variable for TES Capacity (from EMS Global Variable)
1277
+ v = OpenStudio::Model::OutputVariable.new(eout_tes_cap.name.to_s, model)
1278
+ v.setName("#{ctes.name} Ice Thermal Storage Capacity [GJ]")
1279
+ v.setVariableName('Ice Thermal Storage Capacity')
1280
+ ovars << v
1281
+
1282
+ # Create output variable for chiller nominal capacity (from EMS Output Variable)
1283
+ v = OpenStudio::Model::OutputVariable.new(eout_chiller_cap.name.to_s, model)
1284
+ v.setName("#{ctes_chiller.name} Nominal Capacity [W]")
1285
+ v.setVariableName('Chiller Nominal Capacity')
1286
+ ovars << v
1287
+
1288
+ # Set variable reporting frequency for newly created output variables
1289
+ ovars.each do |var|
1290
+ var.setReportingFrequency(report_freq)
1291
+ end
1292
+
1293
+ # Register info about new output variables
1294
+ runner.registerInfo("#{ovars.size} chiller and ice storage output variables were added to the model.")
1295
+
1296
+ # Create new energy/specific end use meters
1297
+ omet_names = ['Pumps:Electricity',
1298
+ 'Fans:Electricity',
1299
+ 'Cooling:Electricity',
1300
+ 'Electricity:HVAC',
1301
+ 'Electricity:Plant',
1302
+ 'Electricity:Building',
1303
+ 'Electricity:Facility']
1304
+
1305
+ omet_names.each do |nm|
1306
+ omet = OpenStudio::Model::OutputMeter.new(model)
1307
+ omet.setName(nm)
1308
+ omet.setReportingFrequency(report_freq)
1309
+ omet.setMeterFileOnly(false)
1310
+ omet.setCumulative(false)
1311
+ end
1312
+
1313
+ # Register info about new output meters
1314
+ runner.registerInfo("#{omet_names.size} output meters were added to the model.")
1315
+
1316
+ ## Report Final Condition of Model----------------------------------------------------------------------------------
1317
+ total_storage = model.getThermalStorageIceDetaileds.size
1318
+ runner.registerFinalCondition("The model finished with #{total_storage} ice energy storage device(s).")
1319
+
1320
+ true
1321
+ end
1322
+ end
1323
+
1324
+ # register the measure to be used by the application
1325
+ AddCentralIceStorage.new.registerWithApplication