openstudio-load-flexibility-measures 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +36 -36
  3. data/.rubocop.yml +6 -9
  4. data/CHANGELOG.html +75 -75
  5. data/CHANGELOG.md +59 -54
  6. data/Gemfile +31 -7
  7. data/Jenkinsfile +11 -10
  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 -26
  25. data/lib/measures/add_hpwh/README.md.erb +41 -41
  26. data/lib/measures/add_hpwh/measure.rb +663 -645
  27. data/lib/measures/add_hpwh/measure.xml +402 -463
  28. data/lib/measures/add_hpwh/tests/SmallHotel-2A.osm +42893 -42893
  29. data/lib/measures/add_hpwh/tests/{add_hphw_test.rb → add_hpwh_test.rb} +137 -98
  30. data/lib/measures/add_packaged_ice_storage/LICENSE.md +26 -26
  31. data/lib/measures/add_packaged_ice_storage/README.html +185 -185
  32. data/lib/measures/add_packaged_ice_storage/README.md +189 -189
  33. data/lib/measures/add_packaged_ice_storage/measure.rb +694 -691
  34. data/lib/measures/add_packaged_ice_storage/measure.xml +245 -245
  35. data/lib/measures/add_packaged_ice_storage/resources/TESCurves.idf +1059 -1059
  36. data/lib/measures/add_packaged_ice_storage/tests/MeasureTest.osm +9507 -9507
  37. data/lib/measures/add_packaged_ice_storage/tests/add_packaged_ice_storage_test.rb +96 -96
  38. data/lib/openstudio/load_flexibility_measures/version.rb +40 -40
  39. data/lib/openstudio/load_flexibility_measures.rb +50 -50
  40. data/openstudio-load-flexibility-measures.gemspec +33 -34
  41. metadata +26 -26
@@ -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