Class: AddPackagedIceStorage

Inherits:
OpenStudio::Measure::EnergyPlusMeasure
  • Object
show all
Defined in:
lib/measures/add_packaged_ice_storage/measure.rb

Overview

start the measure

Instance Method Summary collapse

Instance Method Details

#arguments(workspace) ⇒ Object

define the arguments that the user will input



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/measures/add_packaged_ice_storage/measure.rb', line 61

def arguments(workspace)
  args = OpenStudio::Measure::OSArgumentVector.new

  # Add a delimiter for clarify

  delimiter = OpenStudio::Measure::OSArgument.makeStringArgument('delimiter', false)
  delimiter.setDisplayName('Select Coils to Replace:')
  delimiter.setDefaultValue('-----------------------------------------------------------------')
  args << delimiter

  # get existing dx coils for user selection

  coils = workspace.getObjectsByType('Coil:Cooling:DX:SingleSpeed'.to_IddObjectType)
  coils += workspace.getObjectsByType('Coil:Cooling:DX:TwoSpeed'.to_IddObjectType)
  coilhash = {}
  c = coils.sort { |a, b| a.getString(0).get <=> b.getString(0).get }
  c.each do |coil|
    c_name = coil.name.to_s
    coilhash[c_name] = true
  end

  # make boolean argument for selecting cooling coils to replace

  coilhash.each do |k, v|
    coil_selection = OpenStudio::Measure::OSArgument.makeBoolArgument(k, true)
    coil_selection.setDisplayName(k)
    coil_selection.setDefaultValue(v)
    args << coil_selection
  end

  ice_cap = OpenStudio::Measure::OSArgument.makeStringArgument('ice_cap', true)
  ice_cap.setDisplayName('Input the ice storage capacity [ton-hours]')
  ice_cap.setDescription('To specify by coil, in alphabetical order, enter values for each separated by comma.')
  ice_cap.setDefaultValue('AutoSize')
  args << ice_cap

  size_mult = OpenStudio::Measure::OSArgument.makeStringArgument('size_mult', false)
  size_mult.setDisplayName('Enter a sizing multiplier to manually adjust the autosize results for ice tank capacities')
  size_mult.setDefaultValue('1.0')
  args << size_mult

  # make argument for control method

  ctl = OpenStudio::Measure::OSArgument.makeChoiceArgument('ctl', ['ScheduledModes', 'EMSControlled'], true)
  ctl.setDisplayName('Select ice storage control method')
  ctl.setDefaultValue('EMSControlled')
  args << ctl

  # obtain default schedule names in TESCurves.idf. This allows users to manually add schedules to the idf and be able to access them in OS or PAT

  source_idf = OpenStudio::IdfFile.load(OpenStudio::Path.new(File.dirname(__FILE__) + '/resources/TESCurves.idf')).get
  schedules = source_idf.getObjectsByType('Schedule:Compact'.to_IddObjectType)
  schedule_names = OpenStudio::StringVector.new

  schedules.each do |sch|
    schedule_names << sch.name.to_s
  end

  # make argument for TES operating mode schedule

  sched = OpenStudio::Measure::OSArgument.makeChoiceArgument('sched', schedule_names, true)
  sched.setDisplayName('Select the operating mode schedule for the new TES coils')
  sched.setDescription('Use the fields below to set a simple daily ice charge/discharge schedule. Or, select from pre-defined options.')
  sched.setDefaultValue('Simple User Sched')
  args << sched

  # make arguement for weekend TES operation

  wknd = OpenStudio::Measure::OSArgument.makeBoolArgument('wknd', false)
  wknd.setDisplayName('Run TES on the weekends')
  wknd.setDescription('Select if building is occupied on weekends')
  wknd.setDefaultValue(true)
  args << wknd

  # make arguments for operating season

  season = OpenStudio::Measure::OSArgument.makeStringArgument('season', false)
  season.setDisplayName('Select season during which the ice cooling may be used')
  season.setDescription('Use MM/DD-MM/DD format')
  season.setDefaultValue('01/01-12/31')
  args << season

  # make arguments for simple charging period

  charge_start = OpenStudio::Measure::OSArgument.makeStringArgument('charge_start', false)
  charge_start.setDisplayName('Input start time for ice charge (hr:min)')
  charge_start.setDescription('Use 24 hour format')
  charge_start.setDefaultValue('22:00')
  args << charge_start

  charge_end = OpenStudio::Measure::OSArgument.makeStringArgument('charge_end', false)
  charge_end.setDisplayName('Input end time for ice charge (hr:min)')
  charge_end.setDescription('Use 24 hour format')
  charge_end.setDefaultValue('07:00')
  args << charge_end

  # make arguments for simple discharging period

  discharge_start = OpenStudio::Measure::OSArgument.makeStringArgument('discharge_start', false)
  discharge_start.setDisplayName('Input start time for ice discharge (hr:min)')
  discharge_start.setDescription("Use 24hour format.\nIf 'AutoSize' is selected for ice capacity, these inputs set an ice capacity sizing factor. Otherwise, these only affect discharging schedule.")
  discharge_start.setDefaultValue('12:00')
  args << discharge_start

  discharge_end = OpenStudio::Measure::OSArgument.makeStringArgument('discharge_end', false)
  discharge_end.setDisplayName('Input target end time for ice discharge (hr:min)')
  discharge_end.setDescription('Use 24 hour format')
  discharge_end.setDefaultValue('18:00')
  args << discharge_end

  args
  # end the arguments method

end

#descriptionObject

human readable description



47
48
49
# File 'lib/measures/add_packaged_ice_storage/measure.rb', line 47

def description
  'This measure removes the cooling coils in the model and replaces them with packaged air conditioning units with integrated ice storage.'
end

#modeler_descriptionObject

human readable description of modeling approach



52
53
54
55
56
57
58
# File 'lib/measures/add_packaged_ice_storage/measure.rb', line 52

def modeler_description
  "This measure applies to packaged single zone air conditioning systems or packaged variable air volume systems that were originally modeled with CoilSystem:Cooling:DX or AirLoopHVAC:UnitarySystem container objects. It adds a Coil:Cooling:DX:SingleSpeed:ThermalStorage coil object to each user-selected thermal zone and deletes the existing cooling coil.

  Users inputs are accepted for cooling coil size, ice storage size, system control method, modes of operation, and operating schedule.

  The measure requires schedule objects and performance curves from an included resource file TESCurves.idf. Output variables of typical interest are included as well."
end

#nameObject

human readable name



42
43
44
# File 'lib/measures/add_packaged_ice_storage/measure.rb', line 42

def name
  'Add Packaged Ice Storage'
end

#run(workspace, runner, user_arguments) ⇒ Object

define what happens when the measure is run



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
# File 'lib/measures/add_packaged_ice_storage/measure.rb', line 166

def run(workspace, runner, user_arguments)
  super(workspace, runner, user_arguments)

  # use the built-in error checking

  unless runner.validateUserArguments(arguments(workspace), user_arguments)
    return false
  end

  # load required TESCurves.idf. This contains all the TES performance curves and default schedules

  source_idf = OpenStudio::IdfFile.load(OpenStudio::Path.new(File.dirname(__FILE__) + '/resources/TESCurves.idf')).get

  # workspace.addObjects(idf_obj_vector) does not work here. Add each obj individually.

  source_idf.objects.each do |o|
    workspace.addObject(o)
  end
  runner.registerInfo("#{source_idf.objects.size} performance curves, schedule objects, and output variables were imported from 'TESCurves.idf'.\n\n")

  # assign user arguments to variables

  ice_cap = runner.getStringArgumentValue('ice_cap', user_arguments)                  # ice capacity value (in ton-hours)

  size_mult = runner.getStringArgumentValue('size_mult', user_arguments)              # size multiplier for ice tank capacity - use if autosize is excessively oversizing

  ctl = runner.getStringArgumentValue('ctl', user_arguments)                          # control method (schedule or EMS)

  sched = runner.getStringArgumentValue('sched', user_arguments)                      # select operating mode schedule (schedule objects located in resources\TESCurves.idf)

  wknd = runner.getBoolArgumentValue('wknd', user_arguments)                          # turn tes on/off for weekend operation

  season = runner.getStringArgumentValue('season', user_arguments)                    # set operating season for Simple User Sched

  charge_start = runner.getStringArgumentValue('charge_start', user_arguments)        # time ice charging begins

  charge_end = runner.getStringArgumentValue('charge_end', user_arguments)            # time ice charging ends

  discharge_start = runner.getStringArgumentValue('discharge_start', user_arguments)  # time ice discharge begins

  discharge_end = runner.getStringArgumentValue('discharge_end', user_arguments)      # time ice discharge ends


  # retrieve user selected coils and assign to vector

  coils = workspace.getObjectsByType('Coil:Cooling:DX:SingleSpeed'.to_IddObjectType)
  coils += workspace.getObjectsByType('Coil:Cooling:DX:TwoSpeed'.to_IddObjectType)
  coilhash = {}
  c = coils.sort { |a, b| a.getString(0).get <=> b.getString(0).get }
  c.each do |coil|
    c_name = coil.name.to_s
    coilhash[c_name] = true
  end

  coil_selection = []
  coilhash.each do |k, v|
    temp_var = runner.getBoolArgumentValue(k, user_arguments)
    coil_selection << k if temp_var
  end

  # create other useful variables

  replacement_count = 0                         # tracks number of coils replaced by measure

  time_size_factor = ''                         # sets Storage Capacity Sizing Factor {hr}

  discharge_cop = '63.6'                        # default COP for Ice Discharge

  curve_d_shr_ft = 'Discharge-SHR-fT-NREL'      # default curve for sensible heat ratio f(T) during ice discharge


  # convert string time values into floats for math comparisons

  # ds/de = discharge start/end, cs/ce = charge start/end

  ds = discharge_start.split(':')[0].to_f + (discharge_start.split(':')[1].to_f / 0.6)
  de = discharge_end.split(':')[0].to_f + (discharge_end.split(':')[1].to_f / 0.6)
  cs = charge_start.split(':')[0].to_f + (charge_start.split(':')[1].to_f / 0.6)
  ce = charge_end.split(':')[0].to_f + (charge_end.split(':')[1].to_f / 0.6)

  # #Check User Inputs and Define Variables

  hardcaps = []
  if ice_cap != 'AutoSize'
    if ice_cap == ''
      runner.registerWarning("No ice capacity was entered for 'User Input' selection, 'AutoSize' was used instead.")
      ice_cap = 'AutoSize'
    elsif ice_cap.split(',').size > 1
      runner.registerInfo('Ice storage tanks will be hardsized based on user inputs, assigned alphabetically.')
      ice_cap.split(',').each { |i| hardcaps.push((i.to_f * 0.0126608).to_s) }
      while hardcaps.size != coil_selection.size
        runner.registerInfo("No user-defined thermal storage capacity for #{coil_selection[hardcaps.size]}; unit will be AutoSized.")
        hardcaps.push('AutoSize')
      end
    else
      ice_cap = (ice_cap.to_f * 0.0126608).to_s # convert units from ton-hours to GJ

    end
  elsif sched == 'Simple User Sched'
    time_size_factor = ((de - ds) * size_mult.to_f).to_s
  elsif sched == 'TES Sched 2: 1-5 Peak'
    time_size_factor = (4.0 * size_mult.to_f).to_s
  elsif sched == 'TES Sched 3: 3-8 Peak'
    time_size_factor = (5.0 * size_mult.to_f).to_s
  elsif sched == 'TES Sched 4: GSS-T'
    time_size_factor = (3.0 * size_mult.to_f).to_s
  else
    time_size_factor = (4.0 * size_mult.to_f).to_s # sets default time size factor to 4 hours

  end

  # Check user schedule inputs and build schedule

  if sched == 'Simple User Sched'

    # find empty user input schedule object from TESCurves.idf import

    user_schedules = workspace.getObjectsByName('Simple User Sched')
    user_schedule = user_schedules[0]

    # check ice discharge times to ensure end occurs after start. Exit gracefully if it doesn't.

    if de < ds
      runner.registerError('Ice discharge end time occurs before the start time. If ice discharge is desired overnight, create a schedule object in ../resources/TESCurves.idf. Measure was not applied.')
      return false
    end

    # sets charge and discharge mode ** May be modified if Cool_Charge or Cool_Discharge modes become available

    charge_mode = 4
    discharge_mode = 5

    # format user input for cooling season values

    czn = season.split(/[\s-]/)
    cool_start = czn[0].to_s
    cool_end = czn[-1].to_s

    # set cooling season start periods

    a = 4 # index variable to ensure schedule is built properly under various conditions

    c = 3 # index variable to help ensure a weekday-only schedule is properly built


    if cool_start != '01/01'
      user_schedule.setString(2, "Through: #{cool_start}")
      user_schedule.setString(3, 'For: AllDays')
      user_schedule.setString(4, 'Until: 24:00')
      user_schedule.setString(5, '1')
      user_schedule.setString(7, 'For: AllDays')
      a = 8
      c = 7
    end

    # build user defined schedule object

    if cs > ce
      # assign times to schedule fields

      user_schedule.setString(a, "Until: #{charge_end}")
      user_schedule.setString(a + 1, charge_mode.to_s)
      user_schedule.setString(a + 2, "Until: #{discharge_start}")
      user_schedule.setString(a + 3, '1')
      user_schedule.setString(a + 4, "Until: #{discharge_end}")
      user_schedule.setString(a + 5, discharge_mode.to_s)
      user_schedule.setString(a + 6, "Until: #{charge_start}")
      user_schedule.setString(a + 7, '1')
      user_schedule.setString(a + 8, 'Until: 24:00')
      user_schedule.setString(a + 9, charge_mode.to_s)
      b = a + 10
    elsif charge_start != '00:00'
      user_schedule.setString(a, "Until: #{charge_end}")
      user_schedule.setString(a + 1, charge_mode.to_s)
      user_schedule.setString(a + 2, "Until: #{discharge_start}")
      user_schedule.setString(a + 3, '1')
      user_schedule.setString(a + 4, "Until: #{discharge_end}")
      user_schedule.setString(a + 5, discharge_mode.to_s)
      user_schedule.setString(a + 6, 'Until: 24:00')
      user_schedule.setString(a + 7, '1')
      b = a + 8
    else
      user_schedule.setString(a, "Until: #{charge_end}")
      user_schedule.setString(a + 1, charge_mode.to_s)
      user_schedule.setString(a + 2, "Until: #{discharge_start}")
      user_schedule.setString(a + 3, '1')
      user_schedule.setString(a + 4, "Until: #{discharge_end}")
      user_schedule.setString(a + 5, discharge_mode.to_s)
      user_schedule.setString(a + 6, 'Until: 24:00')
      user_schedule.setString(a + 7, '1')
      b = a + 8
    end

    # make weekend modification if necessary

    unless wknd
      user_schedule.setString(c, 'For: WeekDays')
      user_schedule.setString(b, 'For: Weekends')
      user_schedule.setString(b + 1, "Until: #{charge_end}")
      user_schedule.setString(b + 2, charge_mode.to_s)
      user_schedule.setString(b + 3, 'Until: 24:00')
      user_schedule.setString(b + 4, '1')
      b += 5
    end

    # complete cooling season schedule if not through 12/31

    if cool_end != '12/31'
      user_schedule.setString(c - 1, "Through: #{cool_end}")
      user_schedule.setString(b, 'Through: 12/31')
      user_schedule.setString(b + 1, 'For: AllDays')
      user_schedule.setString(b + 2, 'Until: 24:00')
      user_schedule.setString(b + 3, '1')
    end
  end

  # find objects of interest in the model (used to identify container objects, air loops, and thermal zones)

  cooling_coil_systems = workspace.getObjectsByType('CoilSystem:Cooling:DX'.to_IddObjectType)
  air_loops = workspace.getObjectsByType('AirLoopHVAC'.to_IddObjectType)
  branches = workspace.getObjectsByType('Branch'.to_IddObjectType)
  hvac_zone_mixers = workspace.getObjectsByType('AirLoopHVAC:ZoneMixer'.to_IddObjectType)
  zone_connections = workspace.getObjectsByType('ZoneHVAC:EquipmentConnections'.to_IddObjectType)
  unitary_generic_obj = workspace.getObjectsByType('AirLoopHVAC:UnitarySystem'.to_IddObjectType)
  node_lists = workspace.getObjectsByType('NodeList'.to_IddObjectType)

  # create vector of all cooling system container objects

  cooling_containers = OpenStudio::IdfObjectVector.new
  cooling_coil_systems.each do |coil_sys|
    if coil_selection.include?(coil_sys.getString(6).to_s)
      cooling_containers << coil_sys
    end
  end
  unitary_generic_obj.each do |unitary_sys|
    if coil_selection.include?(unitary_sys.getString(15).to_s)
      cooling_containers << unitary_sys
    end
  end

  # exit gracefully if original model does not have coilSystem:Cooling objects

  if cooling_containers.empty?
    runner.registerError('This measure only operates on the following EnergyPlus container objects: CoilSystem:Cooling:DX and AirLoopHVAC:UnitarySystem. Measure was not applied.')
  end

  # create TES object string template for use in replacing existing coils; incorporates user input variables

  new_tes_string =
    "Coil:Cooling:DX:SingleSpeed:ThermalStorage,
      NAME PLACEHOLDER,        !- Name
      ALWAYS_ON,               !- Availability Schedule Name
      #{ctl},                  !- Operating Mode Control Method
      #{sched},                !- Operation Mode Control Schedule Name
      Ice,                     !- Storage Type
      ,                        !- User Defined Fluid Type
      ,                        !- Fluid Storage Volume {m3}
      AutoSize,                !- Ice Storage Capacity {GJ}
      #{time_size_factor},     !- Storage Capacity Sizing Factor {hr}
      AMBIENT NODE,            !- Storage Tank Ambient Temperature Node Name
      7.913,                   !- Storage Tank to Ambient U-value Times Area Heat Transfer Coefficient {W/K}
      ,                        !- Fluid Storage Tank Rating Temperature {C}
      AutoSize,                !- Rated Evaporator Air Flow Rate {m3/s}
      EVAP IN NODE,            !- Evaporator Air Inlet Node Name
      EVAP OUT NODE,           !- Evaporator Air Outlet Node Name
      Yes,                     !- Cooling Only Mode Available
      AutoSize,                !- Cooling Only Mode Rated Total Evaporator Cooling Capacity {W}  **IB40 Limits: 10551 W (3 ton) to 70337 W (20 ton)**
      0.7,                     !- Cooling Only Mode Rated Sensible Heat Ratio
      3.23372055845678,        !- Cooling Only Mode Rated COP {W/W}
      Cool-Cap-fT,             !- Cooling Only Mode Total Evaporator Cooling Capacity Function of Temperature Curve Name
      ConstantCubic,           !- Cooling Only Mode Total Evaporator Cooling Capacity Function of Flow Fraction Curve Name
      Cool-EIR-fT,             !- Cooling Only Mode Energy Input Ratio Function of Temperature Curve Name
      ConstantCubic,           !- Cooling Only Mode Energy Input Ratio Function of Flow Fraction Curve Name
      Cool-PLF-fPLR,           !- Cooling Only Mode Part Load Fraction Correlation Curve Name
      Cool-SHR-fT,             !- Cooling Only Mode Sensible Heat Ratio Function of Temperature Curve Name
      Cool-SHR-fFF,            !- Cooling Only Mode Sensible Heat Ratio Function of Flow Fraction Curve Name
      No,                      !- Cooling And Charge Mode Available
      AutoSize,                !- Cooling And Charge Mode Rated Total Evaporator Cooling Capacity
      1.0,                     !- Cooling And Charge Mode Capacity Sizing Factor
      AutoSize,                !- Cooling And Charge Mode Rated Storage Charging Capacity
      0.86,                    !- Cooling And Charge Mode Storage Capacity Sizing Factor
      0.7,                     !- Cooling And Charge Mode Rated Sensible Heat Ratio
      3.66668443E+00,          !- Cooling And Charge Mode Cooling Rated COP
      2.17,                    !- Cooling And Charge Mode Charging Rated COP
      CoolCharge-Cool-Cap-fT,  !- Cooling And Charge Mode Total Evaporator Cooling Capacity Function of Temperature Curve Name
      ConstantCubic,           !- Cooling And Charge Mode Total Evaporator Cooling Capacity Function of Flow Fraction Curve Name
      CoolCharge-Cool-EIR-fT,  !- Cooling And Charge Mode Evaporator Energy Input Ratio Function of Temperature Curve Name
      ConstantCubic,           !- Cooling And Charge Mode Evaporator Energy Input Ratio Function of Flow Fraction Curve Name
      Cool-PLF-fPLR,           !- Cooling And Charge Mode Evaporator Part Load Fraction Correlation Curve Name
      CoolCharge-Charge-Cap-fT,!- Cooling And Charge Mode Storage Charge Capacity Function of Temperature Curve Name
      ConstantCubic,           !- Cooling And Charge Mode Storage Charge Capacity Function of Total Evaporator PLR Curve Name
      CoolCharge-Charge-EIR-fT,!- Cooling And Charge Mode Storage Energy Input Ratio Function of Temperature Curve Name
      ConstantCubic,           !- Cooling And Charge Mode Storage Energy Input Ratio Function of Flow Fraction Curve Name
      ConstantCubic,           !- Cooling And Charge Mode Storage Energy Part Load Fraction Correlation Curve Name
      Cool-SHR-fT,             !- Cooling And Charge Mode Sensible Heat Ratio Function of Temperature Curve Name
      Cool-SHR-fFF,            !- Cooling And Charge Mode Sensible Heat Ratio Function of Flow Fraction Curve Name
      No,                      !- Cooling And Discharge Mode Available
      ,                        !- Cooling And Discharge Mode Rated Total Evaporator Cooling Capacity {W}
      ,                        !- Cooling And Discharge Mode Evaporator Capacity Sizing Factor
      ,                        !- Cooling And Discharge Mode Rated Storage Discharging Capacity {W}
      ,                        !- Cooling And Discharge Mode Storage Discharge Capacity Sizing Factor
      ,                        !- Cooling And Discharge Mode Rated Sensible Heat Ratio
      ,                        !- Cooling And Discharge Mode Cooling Rated COP {W/W}
      ,                        !- Cooling And Discharge Mode Discharging Rated COP {W/W}
      ,                        !- Cooling And Discharge Mode Total Evaporator Cooling Capacity Function of Temperature Curve Name
      ,                        !- Cooling And Discharge Mode Total Evaporator Cooling Capacity Function of Flow Fraction Curve Name
      ,                        !- Cooling And Discharge Mode Evaporator Energy Input Ratio Function of Temperature Curve Name
      ,                        !- Cooling And Discharge Mode Evaporator Energy Input Ratio Function of Flow Fraction Curve Name
      ,                        !- Cooling And Discharge Mode Evaporator Part Load Fraction Correlation Curve Name
      ,                        !- Cooling And Discharge Mode Storage Discharge Capacity Function of Temperature Curve Name
      ,                        !- Cooling And Discharge Mode Storage Discharge Capacity Function of Flow Fraction Curve Name
      ,                        !- Cooling And Discharge Mode Storage Discharge Capacity Function of Total Evaporator PLR Curve Name
      ,                        !- Cooling And Discharge Mode Storage Energy Input Ratio Function of Temperature Curve Name
      ,                        !- Cooling And Discharge Mode Storage Energy Input Ratio Function of Flow Fraction Curve Name
      ,                        !- Cooling And Discharge Mode Storage Energy Part Load Fraction Correlation Curve Name
      ,                        !- Cooling And Discharge Mode Sensible Heat Ratio Function of Temperature Curve Name
      ,                        !- Cooling And Discharge Mode Sensible Heat Ratio Function of Flow Fraction Curve Name
      Yes,                     !- Charge Only Mode Available
      AutoSize,                !- Charge Only Mode Rated Storage Charging Capacity {W}
      0.8,                     !- Charge Only Mode Capacity Sizing Factor
      3.09,                    !- Charge Only Mode Charging Rated COP {W/W}
      ChargeOnly-Cap-fT,       !- Charge Only Mode Storage Charge Capacity Function of Temperature Curve Name
      ChargeOnly-EIR-fT,       !- Charge Only Mode Storage Energy Input Ratio Function of Temperature Curve Name
      Yes,                     !- Discharge Only Mode Available
      AutoSize,                !- Discharge Only Mode Rated Storage Discharging Capacity {W}
      1.37,                    !- Discharge Only Mode Capacity Sizing Factor
      0.64,                    !- Discharge Only Mode Rated Sensible Heat Ratio
      #{discharge_cop},        !- Discharge Only Mode Rated COP {W/W}
      Discharge-Cap-fT,        !- Discharge Only Mode Storage Discharge Capacity Function of Temperature Curve Name
      Discharge-Cap-fFF,       !- Discharge Only Mode Storage Discharge Capacity Function of Flow Fraction Curve Name
      ConstantBi,              !- Discharge Only Mode Energy Input Ratio Function of Temperature Curve Name
      ConstantCubic,           !- Discharge Only Mode Energy Input Ratio Function of Flow Fraction Curve Name
      ConstantCubic,           !- Discharge Only Mode Part Load Fraction Correlation Curve Name
      #{curve_d_shr_ft},       !- Discharge Only Mode Sensible Heat Ratio Function of Temperature Curve Name
      Discharge-SHR-fFF,       !- Discharge Only Mode Sensible Heat Ratio Function of Flow Fraction Curve Name
      0.0,                     !- Ancillary Electric Power {W}
      2.0,                     !- Cold Weather Operation Minimum Outdoor Air Temperature {C}
      0.0,                     !- Cold Weather Operation Ancillary Power {W}
      CONDENSER INLET NODE,    !- Condenser Air Inlet Node Name
      CONDENSER OUTLET NODE,   !- Condenser Air Outlet Node Name
      autocalculate,           !- Condenser Design Air Flow Rate {m3/s}
      1.25,                    !- Condenser Air Flow Sizing Factor
      AirCooled,               !- Condenser Type
      ,                        !- Evaporative Condenser Effectiveness {dimensionless}
      ,                        !- Evaporative Condenser Pump Rated Power Consumption {W}
      ,                        !- Basin Heater Capacity {W/K}
      ,                        !- Basin Heater Setpoint Temperature {C}
      ,                        !- Basin Heater Availability Schedule Name
      ,                        !- Supply Water Storage Tank Name
      ,                        !- Condensate Collection Water Storage Tank Name
      ,                        !- Storage Tank Plant Connection Inlet Node Name
      ,                        !- Storage Tank Plant Connection Outlet Node Name
      ,                        !- Storage Tank Plant Connection Design Flow Rate {m3/s}
      ,                        !- Storage Tank Plant Connection Heat Transfer Effectiveness
      ,                        !- Storage Tank Minimum Operating Limit Fluid Temperature {C}
      ;                        !- Storage Tank Maximum Operating Limit Fluid Temperature {C}"
  # end of new TES coil string


  # #Begin Coil Replacement

  # iterate through all CoilSystem:Cooling objects and replace existing coils with TES coils

  coil_selection.each do |sel_coil|
    # get workspace object for selected coil from name

    sel_coil = workspace.getObjectsByName(sel_coil)[0]
    ice_cap = hardcaps[replacement_count] unless hardcaps.empty?

    # get coil type in order to find get appropriate field keys

    # fields of interest: (0 - Max Cap, 1 - Rated COP, 2 - Inlet Node, 3 - Outlet Node)

    field_names = [] # may not be required. Check scope in Ruby documentation

    sel_type = sel_coil.iddObject.type.valueDescription.to_s
    if sel_type == 'Coil:Cooling:DX:SingleSpeed'
      field_names = ['Gross Rated Total Cooling Capacity', 'Gross Rated Cooling COP',
                     'Air Inlet Node Name', 'Air Outlet Node Name']
    elsif sel_type == 'Coil:Cooling:DX:TwoSpeed'
      field_names = ['High Speed Gross Rated Total Cooling Capacity', 'High Speed Gross Rated Cooling COP',
                     'Air Inlet Node Name', 'Air Outlet Node Name']
    end

    # get field indices associated with desired keys

    keys = []
    field_names.each do |fn|
      keys << sel_coil.iddObject.getFieldIndex(fn).to_i
    end

    # get old coil name and create new coil name

    old_coil_name = sel_coil.getString(0).to_s
    utss_name = "UTSS Coil #{replacement_count}"

    # grab inlet and outlet air nodes form selected coil

    inlet = sel_coil.getString(keys[2]).to_s
    outlet = sel_coil.getString(keys[3]).to_s

    # update inlet and outlet node names - not needed possibly add later if names become confusing


    # create local ambient node

    idf_oa_ambient = OpenStudio::IdfObject.new('OutdoorAir:Node'.to_IddObjectType)
    ws_oa_ambient = workspace.addObject(idf_oa_ambient)
    oa_ambient = ws_oa_ambient.get
    oa_ambient.setString(0, "#{utss_name} OA Ambient Node")

    # create new condenser inlet node

    idf_condenser_inlet = OpenStudio::IdfObject.new('OutdoorAir:Node'.to_IddObjectType)
    ws_condenser_inlet = workspace.addObject(idf_condenser_inlet)
    condenser_inlet = ws_condenser_inlet.get
    condenser_inlet.setString(0, "#{utss_name} Condenser Inlet Node")

    # create new condenser inlet node

    idf_condenser_outlet = OpenStudio::IdfObject.new('OutdoorAir:Node'.to_IddObjectType)
    ws_condenser_outlet = workspace.addObject(idf_condenser_outlet)
    condenser_outlet = ws_condenser_outlet.get
    condenser_outlet.setString(0, "#{utss_name} Condenser Out Node")

    # create a new UTSS object

    idf_coil_object = OpenStudio::IdfObject.load(new_tes_string)
    utss_obj = idf_coil_object.get
    ws_tes_obj = workspace.addObject(utss_obj)
    utss = ws_tes_obj.get
    utss.setString(0, utss_name)

    # get indices for required utss fields - reused variable names, consider changing if confusing

    field_names = ['Evaporator Air Inlet Node Name', 'Evaporator Air Outlet Node Name',
                   'Storage Tank Ambient Temperature Node Name',
                   'Condenser Air Inlet Node Name', 'Condenser Air Outlet Node Name']

    keys = []
    field_names.each do |fn|
      keys << utss.iddObject.getFieldIndex(fn).to_i
    end

    # updated required fields in utss object

    utss.setString(keys[0], inlet)              # air inlet node

    utss.setString(keys[1], outlet)             # air outlet node

    utss.setString(keys[2], oa_ambient.name.to_s)         # outdoor ambient node

    utss.setString(keys[3], condenser_inlet.name.to_s)    # condenser inlet node

    utss.setString(keys[4], condenser_outlet.name.to_s)   # condenser outlet node

    utss.setString(7, ice_cap) # hardsized thermal storage capacity


    # copy old coil information over to TES object (use low-speed info for 2spd coils)

    if sel_coil.iddObject.name == 'Coil:Cooling:DX:SingleSpeed'
      utss.setString(16, sel_coil.getString(2).get)
      utss.setString(18, sel_coil.getString(4).get)
      utss.setString(19, sel_coil.getString(9).get)
      utss.setString(20, sel_coil.getString(10).get)
      utss.setString(21, sel_coil.getString(11).get)
      utss.setString(22, sel_coil.getString(12).get)
      utss.setString(23, sel_coil.getString(13).get)
    elsif sel_coil.iddObject.name == 'Coil:Cooling:DX:TwoSpeed'
      utss.setString(16, sel_coil.getString(14).get)
      utss.setString(18, sel_coil.getString(16).get)
      utss.setString(19, sel_coil.getString(18).get)
      utss.setString(20, sel_coil.getString(10).get)
      utss.setString(21, sel_coil.getString(19).get)
      utss.setString(22, sel_coil.getString(12).get)
      utss.setString(23, sel_coil.getString(13).get)
    end

    # identify container object in which the coil is used

    cooling_containers.each do |cont|
      if cont.iddObject.type.valueDescription.to_s == 'CoilSystem:Cooling:DX'
        if cont.getString(6).to_s == old_coil_name
          cont.setString(5, 'Coil:Cooling:DX:SingleSpeed:ThermalStorage')
          cont.setString(6, utss_name)
          break
        end
      elsif cont.iddObject.type.valueDescription.to_s == 'AirLoopHVAC:UnitarySystem'
        if cont.getString(15).to_s == old_coil_name
          cont.setString(14, 'Coil:Cooling:DX:SingleSpeed:ThermalStorage')
          cont.setString(15, utss_name)
          break
        end
      end
    end

    # remove old coil

    workspace.removeObject(sel_coil.handle)

    # increment replacement count

    replacement_count += 1

    ## Add EMS Controller Components

    # create EMS intended schedule sensor once

    if replacement_count == 1
      idf_sched_sensor = OpenStudio::IdfObject.new('EnergyManagementSystem:Sensor'.to_IddObjectType)
      ws_sched_sensor = workspace.addObject(idf_sched_sensor)
      new_sched_sensor = ws_sched_sensor.get
      new_sched_sensor.setString(0, 'TESIntendedSchedule')
      new_sched_sensor.setString(1, sched)
      new_sched_sensor.setString(2, 'Schedule Value')
    end

    # clean-up variable names for EMS purposes (no spaces allowed)

    u_name = utss_name.gsub(/\s/, '_')

    # add EMS sensor for TES control

    idf_sensor = OpenStudio::IdfObject.new('EnergyManagementSystem:Sensor'.to_IddObjectType)
    ws_sensor = workspace.addObject(idf_sensor)
    new_sensor = ws_sensor.get
    new_sensor.setString(0, "#{u_name}_sTES")
    new_sensor.setString(1, utss_name)
    new_sensor.setString(2, 'Cooling Coil Ice Thermal Storage End Fraction')

    # add EMS actuator for TES control

    idf_actuator = OpenStudio::IdfObject.new('EnergyManagementSystem:Actuator'.to_IddObjectType)
    ws_actuator = workspace.addObject(idf_actuator)
    new_actuator = ws_actuator.get
    new_actuator.setString(0, "#{u_name}_OpMode")
    new_actuator.setString(1, utss_name)
    new_actuator.setString(2, 'Coil:Cooling:DX:SingleSpeed:ThermalStorage')
    new_actuator.setString(3, 'Operating Mode')

    # add Global Variable to track min SOC from previous use

    idf_gvar = OpenStudio::IdfObject.new('EnergyManagementSystem:GlobalVariable'.to_IddObjectType)
    ws_gvar = workspace.addObject(idf_gvar)
    new_gvar = ws_gvar.get
    new_gvar.setString(0, "#{u_name}_MinSOC")

    # add EMS program for TES control

    program_string = "
    EnergyManagementSystem:Program,
      #{u_name}_Control,      !- Name
      SET #{u_name}_OpMode = TESIntendedSchedule,
      IF CurrentEnvironment == 1,
        SET #{u_name}_MinSOC = 1,
      ENDIF,
      IF (#{u_name}_OpMode == 5),
        IF ( #{u_name}_sTES < 0.05 ),
          SET #{u_name}_OpMode = 1,
        ENDIF,
        SET #{u_name}_MinSOC = #{u_name}_sTES,
      ENDIF,
      IF (#{u_name}_OpMode == 4),
        IF ( #{u_name}_sTES > 0.99 ),
          SET #{u_name}_OpMode = 1,
        ENDIF,
      ENDIF;"

    idf_program = OpenStudio::IdfObject.load(program_string)
    idf_prgm = idf_program.get
    workspace.addObject(idf_prgm)

    # add EMS program calling manager for TES control

    idf_pcm = OpenStudio::IdfObject.new('EnergyManagementSystem:ProgramCallingManager'.to_IddObjectType)
    ws_pcm = workspace.addObject(idf_pcm)
    new_pcm = ws_pcm.get
    new_pcm.setString(0, "#{u_name}_TES_PrgmCallMgr")
    new_pcm.setString(1, 'AfterPredictorAfterHVACManagers')
    new_pcm.setString(2, "#{u_name}_Control")

    # register info

    # Coil replaced, Coil Added, EMS Program Added

    runner.registerInfo("Coil '#{old_coil_name}' was replaced with a unitary thermal storage system named" \
                        "'#{utss.name}' with a capacity of #{ice_cap} GJ.\n")
    # end of coil replacement routine

  end

  # additional output for schedule verification

  runner.registerInfo("The user-selected schedule for the ice unit operation is:\n\n#{user_schedule}")

  # register initial and final conditions

  runner.registerInitialCondition("The building started with #{cooling_containers.size} cooling coils.")
  runner.registerFinalCondition("A total of #{replacement_count} cooling coils were replaced with thermal storage coil systems.")
  true
end