openstudio-extension 0.2.0 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,7 +35,32 @@
35
35
 
36
36
  module OsLib_ModelGeneration
37
37
  # simple list of building types that are valid for get_space_types_from_building_type
38
- def get_building_types
38
+ # for general public use use extended = false
39
+ def get_building_types(extended = false)
40
+
41
+ # get building_types
42
+ if extended
43
+ doe = get_doe_building_types(true)
44
+ deer = get_deer_building_types(true)
45
+ else
46
+ doe = get_doe_building_types
47
+ deer = get_deer_building_types
48
+ end
49
+
50
+ # combine building_types
51
+ array = OpenStudio::StringVector.new
52
+ temp_array = doe.to_a + deer.to_a
53
+ temp_array.each do |i|
54
+ array << i
55
+ end
56
+
57
+ return array
58
+ end
59
+
60
+ # get_doe_building_types
61
+ # for general public use use extended = false
62
+ def get_doe_building_types(extended = false)
63
+ # DOE Prototypes
39
64
  array = OpenStudio::StringVector.new
40
65
  array << 'SecondarySchool'
41
66
  array << 'PrimarySchool'
@@ -54,25 +79,215 @@ module OsLib_ModelGeneration
54
79
  array << 'Hospital'
55
80
  array << 'Outpatient'
56
81
  array << 'SuperMarket'
82
+ array << 'Laboratory'
83
+ array << 'LargeDataCenterLowITE'
84
+ array << 'LargeDataCenterHighITE'
85
+ array << 'SmallDataCenterLowITE'
86
+ array << 'SmallDataCenterHighITE'
87
+
88
+ return array
89
+ end
90
+
91
+ # get_deer_building_types
92
+ # for general public use use extended = false
93
+ def get_deer_building_types(extended = false)
94
+ # DOE Prototypes
95
+ array = OpenStudio::StringVector.new
96
+ array << 'Asm'
97
+ array << 'DMo'
98
+ array << 'ECC'
99
+ array << 'EPr'
100
+ array << 'ERC'
101
+ array << 'ESe'
102
+ array << 'EUn'
103
+ array << 'GHs'
104
+ array << 'Gro'
105
+ array << 'Hsp'
106
+ array << 'Htl'
107
+ array << 'MBT'
108
+ array << 'MFm'
109
+ array << 'MLI'
110
+ array << 'Mtl'
111
+ array << 'Nrs'
112
+ array << 'OfL'
113
+ array << 'OfS'
114
+ array << 'RFF'
115
+ array << 'RSD'
116
+ array << 'Rt3'
117
+ array << 'RtL'
118
+ array << 'RtS'
119
+ array << 'SCn'
120
+ array << 'SFm'
121
+ array << 'SUn'
122
+ array << 'WRf'
57
123
 
58
124
  return array
59
125
  end
60
126
 
61
127
  # simple list of templates that are valid for get_space_types_from_building_type
62
- def get_templates
128
+ # for general public use use extended = false
129
+ def get_templates(extended = false)
130
+
131
+ # get templates
132
+ if extended
133
+ doe = get_doe_templates(true)
134
+ deer = get_deer_templates(true)
135
+ else
136
+ doe = get_doe_templates
137
+ deer = get_deer_templates
138
+ end
139
+
140
+ # combine templates
141
+ array = OpenStudio::StringVector.new
142
+ temp_array = doe.to_a + deer.to_a
143
+ temp_array.each do |i|
144
+ array << i
145
+ end
146
+
147
+ return array
148
+ end
149
+
150
+ # get_doe_templates
151
+ # for general public use use extended = false
152
+ def get_doe_templates(extended = false)
63
153
  array = OpenStudio::StringVector.new
64
154
  array << 'DOE Ref Pre-1980'
65
155
  array << 'DOE Ref 1980-2004'
66
156
  array << '90.1-2004'
67
157
  array << '90.1-2007'
68
- # array << '189.1-2009' # if turn this on need to update space_type_array for RetailStripmall
69
158
  array << '90.1-2010'
70
159
  array << '90.1-2013'
71
- array << 'NREL ZNE Ready 2017'
160
+ if extended
161
+ # array << '189.1-2009' # if turn this on need to update space_type_array for RetailStripmall
162
+ array << 'NREL ZNE Ready 2017'
163
+ end
164
+
165
+ return array
166
+ end
167
+
168
+ # get_deer_templates
169
+ # for general public use use extended = false
170
+ def get_deer_templates(extended = false)
171
+ array = OpenStudio::StringVector.new
172
+ array << 'DEER Pre-1975'
173
+ array << 'DEER 1985'
174
+ array << 'DEER 1996'
175
+ array << 'DEER 2003'
176
+ array << 'DEER 2007'
177
+ array << 'DEER 2011'
178
+ array << 'DEER 2014'
179
+ array << 'DEER 2015'
180
+ array << 'DEER 2017'
181
+ array << 'DEER 2020'
182
+ if extended
183
+ array << 'DEER 2025'
184
+ array << 'DEER 2030'
185
+ array << 'DEER 2035'
186
+ array << 'DEER 2040'
187
+ array << 'DEER 2045'
188
+ array << 'DEER 2050'
189
+ array << 'DEER 2055'
190
+ array << 'DEER 2060'
191
+ array << 'DEER 2065'
192
+ array << 'DEER 2070'
193
+ array << 'DEER 2075'
194
+ end
72
195
 
73
196
  return array
74
197
  end
75
198
 
199
+ # get_climate_zones
200
+ # for general public use use extended = false
201
+ def get_climate_zones(extended = false, extra = nil)
202
+
203
+ # get climate_zones
204
+ if extended && extra != nil
205
+ doe = get_doe_climate_zones(true, extra)
206
+ deer = get_deer_climate_zones(true, nil)
207
+ elsif extended
208
+ doe = get_doe_climate_zones(true, nil)
209
+ deer = get_deer_climate_zones(true, nil)
210
+ elsif extra != nil
211
+ doe = get_doe_climate_zones(false, extra)
212
+ deer = get_deer_climate_zones(false, nil)
213
+ else
214
+ doe = get_doe_climate_zones
215
+ deer = get_deer_climate_zones
216
+ end
217
+
218
+ # combine climate zones
219
+ array = OpenStudio::StringVector.new
220
+ temp_array = doe.to_a + deer.to_a
221
+ temp_array.each do |i|
222
+ array << i
223
+ end
224
+
225
+ return array
226
+ end
227
+
228
+ # get_doe_climate_zones
229
+ # for general public use use extended = false
230
+ def get_doe_climate_zones(extended = false, extra = nil)
231
+
232
+ # Lookup From Model should be added as an option where appropriate in the measure
233
+ cz_choices = OpenStudio::StringVector.new
234
+ if extra != nil
235
+ cz_choices << extra
236
+ end
237
+ cz_choices << 'ASHRAE 169-2013-1A'
238
+ cz_choices << 'ASHRAE 169-2013-1B'
239
+ cz_choices << 'ASHRAE 169-2013-2A'
240
+ cz_choices << 'ASHRAE 169-2013-2B'
241
+ cz_choices << 'ASHRAE 169-2013-3A'
242
+ cz_choices << 'ASHRAE 169-2013-3B'
243
+ cz_choices << 'ASHRAE 169-2013-3C'
244
+ cz_choices << 'ASHRAE 169-2013-4A'
245
+ cz_choices << 'ASHRAE 169-2013-4B'
246
+ cz_choices << 'ASHRAE 169-2013-4C'
247
+ cz_choices << 'ASHRAE 169-2013-5A'
248
+ cz_choices << 'ASHRAE 169-2013-5B'
249
+ cz_choices << 'ASHRAE 169-2013-5C'
250
+ cz_choices << 'ASHRAE 169-2013-6A'
251
+ cz_choices << 'ASHRAE 169-2013-6B'
252
+ cz_choices << 'ASHRAE 169-2013-7A'
253
+ cz_choices << 'ASHRAE 169-2013-8A'
254
+ if extended
255
+ cz_choices << 'ASHRAE 169-2013-0A'
256
+ cz_choices << 'ASHRAE 169-2013-0B'
257
+ end
258
+
259
+ return cz_choices
260
+ end
261
+
262
+ # get_deer_climate_zones
263
+ # for general public use use extended = false
264
+ def get_deer_climate_zones(extended = false, extra = nil)
265
+
266
+ # Lookup From Model should be added as an option where appropriate in the measure
267
+ cz_choices = OpenStudio::StringVector.new
268
+ if extra != nil
269
+ cz_choices << extra
270
+ end
271
+ cz_choices << 'CEC T24-CEC1'
272
+ cz_choices << 'CEC T24-CEC2'
273
+ cz_choices << 'CEC T24-CEC3'
274
+ cz_choices << 'CEC T24-CEC4'
275
+ cz_choices << 'CEC T24-CEC5'
276
+ cz_choices << 'CEC T24-CEC6'
277
+ cz_choices << 'CEC T24-CEC7'
278
+ cz_choices << 'CEC T24-CEC8'
279
+ cz_choices << 'CEC T24-CEC9'
280
+ cz_choices << 'CEC T24-CEC10'
281
+ cz_choices << 'CEC T24-CEC11'
282
+ cz_choices << 'CEC T24-CEC12'
283
+ cz_choices << 'CEC T24-CEC13'
284
+ cz_choices << 'CEC T24-CEC14'
285
+ cz_choices << 'CEC T24-CEC15'
286
+ cz_choices << 'CEC T24-CEC16'
287
+
288
+ return cz_choices
289
+ end
290
+
76
291
  # calculate aspect ratio from area and perimeter
77
292
  def calc_aspect_ratio(a, p)
78
293
  l = 0.25 * (p + Math.sqrt(p**2 - 16 * a))
@@ -84,74 +299,163 @@ module OsLib_ModelGeneration
84
299
 
85
300
  # Building Form Defaults from Table 4.2 in Achieving the 30% Goal: Energy and Cost Savings Analysis of ASHRAE Standard 90.1-2010
86
301
  # aspect ratio for NA replaced with floor area to perimeter ratio from prototype model
302
+ # currently no reason to split apart doe and deer inputs here
87
303
  def building_form_defaults(building_type)
88
304
  hash = {}
89
305
 
306
+ # DOE Prototypes
307
+
90
308
  # calculate aspect ratios not represented on Table 4.2
91
- primary_aspet_ratio = calc_aspect_ratio(73958.0, 2060.0)
92
- secondary_aspet_ratio = calc_aspect_ratio(128112.0, 2447.0)
93
- outpatient_aspet_ratio = calc_aspect_ratio(14782.0, 588.0)
309
+ primary_footprint = 73958.0
310
+ primary_p = 619.0 # wrote measure using calculate_perimeter method in os_lib_geometry
311
+ primary_ns_ew_ratio = 2.829268293 # estimated from ratio of ns/ew total wall area
312
+ primary_width = Math.sqrt(primary_footprint/primary_ns_ew_ratio)
313
+ primary_p_min = 2 * (primary_width + primary_width / primary_footprint)
314
+ primary_p_mult = primary_p / primary_p_min
315
+
316
+ secondary_footprint = 210887.0 / 2.0 # floor area divided by area instead of true footprint 128112.0)
317
+ secondary_p = 708.0 # wrote measure using calculate_perimeter method in os_lib_geometry
318
+ secondary_ns_ew_ratio = 2.069230769 # estimated from ratio of ns/ew total wall area
319
+ secondary_width = Math.sqrt(secondary_footprint/secondary_ns_ew_ratio)
320
+ secondary_p_min = 2 * (secondary_width + secondary_width / secondary_footprint)
321
+ secondary_p_mult = secondary_p / secondary_p_min
322
+
323
+ outpatient_footprint = 40946.0 / 3.0 # floor area divided by area instead of true footprint 17872.0)
324
+ outpatient_p = 537.0 # wrote measure using calculate_perimeter method in os_lib_geometry
325
+ outpatient_ns_ew_ratio = 1.56448737 # estimated from ratio of ns/ew total wall area
326
+ outpatient_width = Math.sqrt(outpatient_footprint/outpatient_ns_ew_ratio)
327
+ outpatient_p_min = 2 * (outpatient_width + outpatient_footprint/outpatient_width)
328
+ outpatient_p_mult = outpatient_p / outpatient_p_min
329
+
330
+ #primary_aspet_ratio = calc_aspect_ratio(73958.0, 2060.0)
331
+ #secondary_aspet_ratio = calc_aspect_ratio(128112.0, 2447.0)
332
+ #outpatient_aspet_ratio = calc_aspect_ratio(14782.0, 588.0)
94
333
  supermarket_a = 45001.0
95
334
  supermarket_p = 866.0
96
335
  supermarket_wwr = 1880.0 / (supermarket_p * 20.0)
97
- supermarket_aspet_ratio = calc_aspect_ratio(supermarket_a, supermarket_p)
98
-
99
- hash['SmallOffice'] = { aspect_ratio: 1.5, wwr: 0.15, typical_story: 10.0 }
100
- hash['MediumOffice'] = { aspect_ratio: 1.5, wwr: 0.33, typical_story: 13.0 }
101
- hash['LargeOffice'] = { aspect_ratio: 1.5, wwr: 0.15, typical_story: 13.0 }
102
- hash['RetailStandalone'] = { aspect_ratio: 1.28, wwr: 0.07, typical_story: 20.0 }
103
- hash['RetailStripmall'] = { aspect_ratio: 4.0, wwr: 0.11, typical_story: 17.0 }
104
- hash['PrimarySchool'] = { aspect_ratio: primary_aspet_ratio.round(1), wwr: 0.35, typical_story: 13.0 }
105
- hash['SecondarySchool'] = { aspect_ratio: secondary_aspet_ratio.round(1), wwr: 0.33, typical_story: 13.0 }
106
- hash['Outpatient'] = { aspect_ratio: outpatient_aspet_ratio.round(1), wwr: 0.20, typical_story: 10.0 }
107
- hash['Hospital'] = { aspect_ratio: 1.33, wwr: 0.16, typical_story: 14.0 }
108
- hash['SmallHotel'] = { aspect_ratio: 3.0, wwr: 0.11, typical_story: 9.0, first_story: 11.0 }
109
- hash['LargeHotel'] = { aspect_ratio: 5.1, wwr: 0.27, typical_story: 10.0, first_story: 13.0 }
336
+ supermarket_aspect_ratio = calc_aspect_ratio(supermarket_a, supermarket_p)
337
+
338
+ hash['SmallOffice'] = { aspect_ratio: 1.5, wwr: 0.15, typical_story: 10.0, perim_mult: 1.0 }
339
+ hash['MediumOffice'] = { aspect_ratio: 1.5, wwr: 0.33, typical_story: 13.0, perim_mult: 1.0 }
340
+ hash['LargeOffice'] = { aspect_ratio: 1.5, wwr: 0.15, typical_story: 13.0, perim_mult: 1.0 }
341
+ hash['RetailStandalone'] = { aspect_ratio: 1.28, wwr: 0.07, typical_story: 20.0, perim_mult: 1.0 }
342
+ hash['RetailStripmall'] = { aspect_ratio: 4.0, wwr: 0.11, typical_story: 17.0, perim_mult: 1.0 }
343
+ hash['PrimarySchool'] = { aspect_ratio: primary_ns_ew_ratio.round(1), wwr: 0.35, typical_story: 13.0, perim_mult: primary_p_mult.round(3) }
344
+ hash['SecondarySchool'] = { aspect_ratio: secondary_ns_ew_ratio.round(1), wwr: 0.33, typical_story: 13.0, perim_mult: secondary_p_mult.round(3) }
345
+ hash['Outpatient'] = { aspect_ratio: outpatient_ns_ew_ratio.round(1), wwr: 0.20, typical_story: 10.0, perim_mult: outpatient_p_mult.round(3) }
346
+ hash['Hospital'] = { aspect_ratio: 1.33, wwr: 0.16, typical_story: 14.0, perim_mult: 1.0 }
347
+ hash['SmallHotel'] = { aspect_ratio: 3.0, wwr: 0.11, typical_story: 9.0, first_story: 11.0, perim_mult: 1.0 }
348
+ hash['LargeHotel'] = { aspect_ratio: 5.1, wwr: 0.27, typical_story: 10.0, first_story: 13.0, perim_mult: 1.0 }
110
349
 
111
350
  # code in get_space_types_from_building_type is used to override building wwr with space type specific wwr
112
- hash['Warehouse'] = { aspect_ratio: 2.2, wwr: 0.0, typical_story: 28.0}
351
+ hash['Warehouse'] = { aspect_ratio: 2.2, wwr: 0.0, typical_story: 28.0, perim_mult: 1.0 }
113
352
 
114
- hash['QuickServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.14, typical_story: 10.0 }
115
- hash['FullServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.18, typical_story: 10.0 }
116
- hash['QuickServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.18, typical_story: 10.0 }
117
- hash['MidriseApartment'] = { aspect_ratio: 2.75, wwr: 0.15, typical_story: 10.0 }
118
- hash['HighriseApartment'] = { aspect_ratio: 2.75, wwr: 0.15, typical_story: 10.0 }
353
+ hash['QuickServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.14, typical_story: 10.0, perim_mult: 1.0 }
354
+ hash['FullServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.18, typical_story: 10.0, perim_mult: 1.0 }
355
+ hash['QuickServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.18, typical_story: 10.0, perim_mult: 1.0 }
356
+ hash['MidriseApartment'] = { aspect_ratio: 2.75, wwr: 0.15, typical_story: 10.0, perim_mult: 1.0 }
357
+ hash['HighriseApartment'] = { aspect_ratio: 2.75, wwr: 0.15, typical_story: 10.0, perim_mult: 1.0 }
119
358
  # SuperMarket inputs come from prototype model
120
- hash['SuperMarket'] = { aspect_ratio: supermarket_aspet_ratio.round(1), wwr: supermarket_wwr.round(2), typical_story: 20.0 }
359
+ hash['SuperMarket'] = { aspect_ratio: supermarket_aspect_ratio.round(1), wwr: supermarket_wwr.round(2), typical_story: 20.0, perim_mult: 1.0 }
360
+
361
+ # Add Laboratory and Data Centers
362
+ hash['Laboratory'] = { aspect_ratio: 1.33, wwr: 0.12, typical_story: 10.0, perim_mult: 1.0 }
363
+ hash['LargeDataCenterLowITE'] = { aspect_ratio: 1.67, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }
364
+ hash['LargeDataCenterHighITE'] = { aspect_ratio: 1.67, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }
365
+ hash['SmallDataCenterLowITE'] = { aspect_ratio: 1.5, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }
366
+ hash['SmallDataCenterHighITE'] = { aspect_ratio: 1.5, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }
367
+
368
+ # DEER Prototypes
369
+ hash['Asm'] = { aspect_ratio: 1.0, wwr: 0.19, typical_story: 15.0 }
370
+ hash['ECC'] = { aspect_ratio: 4.0, wwr: 0.25, typical_story: 13.0 }
371
+ hash['EPr'] = { aspect_ratio: 2.0, wwr: 0.16, typical_story: 12.0 }
372
+ hash['ERC'] = { aspect_ratio: 1.7, wwr: 0.03, typical_story: 12.0 }
373
+ hash['ESe'] = { aspect_ratio: 1.0, wwr: 0.15, typical_story: 13.0 }
374
+ hash['EUn'] = { aspect_ratio: 2.5, wwr: 0.3, typical_story: 14.0 }
375
+ hash['Gro'] = { aspect_ratio: 1.0, wwr: 0.07, typical_story: 25.0 }
376
+ hash['Hsp'] = { aspect_ratio: 1.5, wwr: 0.11, typical_story: 13.0 }
377
+ hash['Htl'] = { aspect_ratio: 3.0, wwr: 0.23, typical_story: 9.5, first_story: 12.0 }
378
+ hash['MBT'] = { aspect_ratio: 10.7, wwr: 0.12, typical_story: 15.0 }
379
+ hash['MFm'] = { aspect_ratio: 1.4, wwr: 0.24, typical_story: 9.5 }
380
+ hash['MLI'] = { aspect_ratio: 1.0, wwr: 0.01, typical_story: 35.0 }
381
+ hash['Mtl'] = { aspect_ratio: 5.1, wwr: 0.41, typical_story: 9.0 }
382
+ hash['Nrs'] = { aspect_ratio: 10.3, wwr: 0.2, typical_story: 13.0 }
383
+ hash['OfL'] = { aspect_ratio: 1.5, wwr: 0.33, typical_story: 12.0 }
384
+ hash['OfS'] = { aspect_ratio: 1.5, wwr: 0.33, typical_story: 12.0 }
385
+ hash['RFF'] = { aspect_ratio: 1.0, wwr: 0.25, typical_story: 13.0 }
386
+ hash['RSD'] = { aspect_ratio: 1.0, wwr: 0.13, typical_story: 13.0 }
387
+ hash['Rt3'] = { aspect_ratio: 1.0, wwr: 0.02, typical_story: 20.8 }
388
+ hash['RtL'] = { aspect_ratio: 1.0, wwr: 0.03, typical_story: 20.5 }
389
+ hash['RtS'] = { aspect_ratio: 1.0, wwr: 0.13, typical_story: 12.0 }
390
+ hash['SCn'] = { aspect_ratio: 1.0, wwr: 0.01, typical_story: 48.0 }
391
+ hash['SUn'] = { aspect_ratio: 1.0, wwr: 0.01, typical_story: 48.0 }
392
+ hash['WRf'] = { aspect_ratio: 1.6, wwr: 0.0, typical_story: 32.0 }
121
393
 
122
394
  return hash[building_type]
123
395
  end
124
396
 
125
397
  # create hash of space types and generic ratios of building floor area
398
+ # currently no reason to split apart doe and deer inputs here
126
399
  def get_space_types_from_building_type(building_type, template, whole_building = true)
127
400
  hash = {}
128
401
 
129
402
  # TODO: - Confirm that these work for all standards
130
-
403
+ # DOE Prototypes
131
404
  if building_type == 'SecondarySchool'
132
- hash['Auditorium'] = { ratio: 0.0504, space_type_gen: true, default: false }
133
- hash['Cafeteria'] = { ratio: 0.0319, space_type_gen: true, default: false }
134
- hash['Classroom'] = { ratio: 0.3528, space_type_gen: true, default: true }
135
- hash['Corridor'] = { ratio: 0.2144, space_type_gen: true, default: false }
136
- hash['Gym'] = { ratio: 0.1646, space_type_gen: true, default: false }
137
- hash['Kitchen'] = { ratio: 0.0110, space_type_gen: true, default: false }
138
- hash['Library'] = { ratio: 0.0429, space_type_gen: true, default: false } # not in prototype
139
- hash['Lobby'] = { ratio: 0.0214, space_type_gen: true, default: false }
140
- hash['Mechanical'] = { ratio: 0.0349, space_type_gen: true, default: false }
141
- hash['Office'] = { ratio: 0.0543, space_type_gen: true, default: false }
142
- hash['Restroom'] = { ratio: 0.0214, space_type_gen: true, default: false }
405
+ if ['DOE Ref Pre-1980', 'DOE Ref 1980-2004'].include?(template)
406
+ hash['Auditorium'] = { ratio: 0.0504, space_type_gen: true, default: false, story_height: 26.0 }
407
+ hash['Cafeteria'] = { ratio: 0.0319, space_type_gen: true, default: false }
408
+ hash['Classroom'] = { ratio: 0.3528, space_type_gen: true, default: true }
409
+ hash['Corridor'] = { ratio: 0.2144, space_type_gen: true, default: false, circ: true }
410
+ hash['Gym'] = { ratio: 0.1009, space_type_gen: true, default: false , story_height: 26.0 }
411
+ hash['Gym - audience'] = { ratio: 0.0637, space_type_gen: true, default: false , story_height: 26.0 }
412
+ hash['Kitchen'] = { ratio: 0.0110, space_type_gen: true, default: false }
413
+ hash['Library'] = { ratio: 0.0429, space_type_gen: true, default: false }
414
+ hash['Lobby'] = { ratio: 0.0214, space_type_gen: true, default: false }
415
+ hash['Mechanical'] = { ratio: 0.0349, space_type_gen: true, default: false }
416
+ hash['Office'] = { ratio: 0.0543, space_type_gen: true, default: false }
417
+ hash['Restroom'] = { ratio: 0.0214, space_type_gen: true, default: false }
418
+ else
419
+ hash['Auditorium'] = { ratio: 0.0504, space_type_gen: true, default: false, story_height: 26.0 }
420
+ hash['Cafeteria'] = { ratio: 0.0319, space_type_gen: true, default: false }
421
+ hash['Classroom'] = { ratio: 0.3041, space_type_gen: true, default: true }
422
+ hash['ComputerRoom'] = { ratio: 0.0487, space_type_gen: true, default: true }
423
+ hash['Corridor'] = { ratio: 0.2144, space_type_gen: true, default: false, circ: true }
424
+ hash['Gym'] = { ratio: 0.1646, space_type_gen: true, default: false , story_height: 26.0 }
425
+ hash['Kitchen'] = { ratio: 0.0110, space_type_gen: true, default: false }
426
+ hash['Library'] = { ratio: 0.0429, space_type_gen: true, default: false }
427
+ hash['Lobby'] = { ratio: 0.0214, space_type_gen: true, default: false }
428
+ hash['Mechanical'] = { ratio: 0.0349, space_type_gen: true, default: false }
429
+ hash['Office'] = { ratio: 0.0543, space_type_gen: true, default: false }
430
+ hash['Restroom'] = { ratio: 0.0214, space_type_gen: true, default: false }
431
+ end
143
432
  elsif building_type == 'PrimarySchool'
144
- hash['Cafeteria'] = { ratio: 0.0458, space_type_gen: true, default: false }
145
- hash['Classroom'] = { ratio: 0.5610, space_type_gen: true, default: true }
146
- hash['Corridor'] = { ratio: 0.1633, space_type_gen: true, default: false }
147
- hash['Gym'] = { ratio: 0.0520, space_type_gen: true, default: false }
148
- hash['Kitchen'] = { ratio: 0.0244, space_type_gen: true, default: false }
149
- # TODO: - confirm if Library is 0.0 for all templates
150
- hash['Library'] = { ratio: 0.0, space_type_gen: true, default: false }
151
- hash['Lobby'] = { ratio: 0.0249, space_type_gen: true, default: false }
152
- hash['Mechanical'] = { ratio: 0.0367, space_type_gen: true, default: false }
153
- hash['Office'] = { ratio: 0.0642, space_type_gen: true, default: false }
154
- hash['Restroom'] = { ratio: 0.0277, space_type_gen: true, default: false }
433
+ if ['DOE Ref Pre-1980', 'DOE Ref 1980-2004'].include?(template)
434
+ # updated to 2004 which includes library vs. pre-1980
435
+ hash['Cafeteria'] = { ratio: 0.0458, space_type_gen: true, default: false }
436
+ hash['Classroom'] = { ratio: 0.5610, space_type_gen: true, default: true }
437
+ hash['Corridor'] = { ratio: 0.1633, space_type_gen: true, default: false, circ: true }
438
+ hash['Gym'] = { ratio: 0.0520, space_type_gen: true, default: false }
439
+ hash['Kitchen'] = { ratio: 0.0244, space_type_gen: true, default: false }
440
+ hash['Library'] = { ratio: 0.0, space_type_gen: true, default: false } # no library in model
441
+ hash['Lobby'] = { ratio: 0.0249, space_type_gen: true, default: false }
442
+ hash['Mechanical'] = { ratio: 0.0367, space_type_gen: true, default: false }
443
+ hash['Office'] = { ratio: 0.0642, space_type_gen: true, default: false }
444
+ hash['Restroom'] = { ratio: 0.0277, space_type_gen: true, default: false }
445
+ else
446
+ # updated to 2004 which includes library vs. pre-1980
447
+ hash['Cafeteria'] = { ratio: 0.0458, space_type_gen: true, default: false }
448
+ hash['Classroom'] = { ratio: 0.4793, space_type_gen: true, default: true }
449
+ hash['ComputerRoom'] = { ratio: 0.0236, space_type_gen: true, default: true }
450
+ hash['Corridor'] = { ratio: 0.1633, space_type_gen: true, default: false, circ: true }
451
+ hash['Gym'] = { ratio: 0.0520, space_type_gen: true, default: false}
452
+ hash['Kitchen'] = { ratio: 0.0244, space_type_gen: true, default: false }
453
+ hash['Library'] = { ratio: 0.0581, space_type_gen: true, default: false }
454
+ hash['Lobby'] = { ratio: 0.0249, space_type_gen: true, default: false }
455
+ hash['Mechanical'] = { ratio: 0.0367, space_type_gen: true, default: false }
456
+ hash['Office'] = { ratio: 0.0642, space_type_gen: true, default: false }
457
+ hash['Restroom'] = { ratio: 0.0277, space_type_gen: true, default: false }
458
+ end
155
459
  elsif building_type == 'SmallOffice'
156
460
  # TODO: - populate Small, Medium, and Large office for whole_building false
157
461
  if whole_building
@@ -160,7 +464,7 @@ module OsLib_ModelGeneration
160
464
  hash['SmallOffice - Breakroom'] = { ratio: 0.99, space_type_gen: true, default: false }
161
465
  hash['SmallOffice - ClosedOffice'] = { ratio: 0.99, space_type_gen: true, default: false }
162
466
  hash['SmallOffice - Conference'] = { ratio: 0.99, space_type_gen: true, default: false }
163
- hash['SmallOffice - Corridor'] = { ratio: 0.99, space_type_gen: true, default: false }
467
+ hash['SmallOffice - Corridor'] = { ratio: 0.99, space_type_gen: true, default: false, circ: true }
164
468
  hash['SmallOffice - Elec/MechRoom'] = { ratio: 0.99, space_type_gen: true, default: false }
165
469
  hash['SmallOffice - Lobby'] = { ratio: 0.99, space_type_gen: true, default: false }
166
470
  hash['SmallOffice - OpenOffice'] = { ratio: 0.99, space_type_gen: true, default: true }
@@ -178,7 +482,7 @@ module OsLib_ModelGeneration
178
482
  hash['MediumOffice - Breakroom'] = { ratio: 0.99, space_type_gen: true, default: false }
179
483
  hash['MediumOffice - ClosedOffice'] = { ratio: 0.99, space_type_gen: true, default: false }
180
484
  hash['MediumOffice - Conference'] = { ratio: 0.99, space_type_gen: true, default: false }
181
- hash['MediumOffice - Corridor'] = { ratio: 0.99, space_type_gen: true, default: false }
485
+ hash['MediumOffice - Corridor'] = { ratio: 0.99, space_type_gen: true, default: false, circ: true }
182
486
  hash['MediumOffice - Elec/MechRoom'] = { ratio: 0.99, space_type_gen: true, default: false }
183
487
  hash['MediumOffice - Lobby'] = { ratio: 0.99, space_type_gen: true, default: false }
184
488
  hash['MediumOffice - OpenOffice'] = { ratio: 0.99, space_type_gen: true, default: true }
@@ -197,7 +501,7 @@ module OsLib_ModelGeneration
197
501
  hash['BreakRoom'] = { ratio: 0.99, space_type_gen: true, default: false }
198
502
  hash['ClosedOffice'] = { ratio: 0.99, space_type_gen: true, default: false }
199
503
  hash['Conference'] = { ratio: 0.99, space_type_gen: true, default: false }
200
- hash['Corridor'] = { ratio: 0.99, space_type_gen: true, default: false }
504
+ hash['Corridor'] = { ratio: 0.99, space_type_gen: true, default: false, circ: true }
201
505
  hash['Elec/MechRoom'] = { ratio: 0.99, space_type_gen: true, default: false }
202
506
  hash['IT_Room'] = { ratio: 0.99, space_type_gen: true, default: false }
203
507
  hash['Lobby'] = { ratio: 0.99, space_type_gen: true, default: false }
@@ -218,7 +522,7 @@ module OsLib_ModelGeneration
218
522
  hash['BreakRoom'] = { ratio: 0.99, space_type_gen: true, default: false }
219
523
  hash['ClosedOffice'] = { ratio: 0.99, space_type_gen: true, default: false }
220
524
  hash['Conference'] = { ratio: 0.99, space_type_gen: true, default: false }
221
- hash['Corridor'] = { ratio: 0.99, space_type_gen: true, default: false }
525
+ hash['Corridor'] = { ratio: 0.99, space_type_gen: true, default: false, circ: true }
222
526
  hash['Elec/MechRoom'] = { ratio: 0.99, space_type_gen: true, default: false }
223
527
  hash['IT_Room'] = { ratio: 0.99, space_type_gen: true, default: false }
224
528
  hash['Lobby'] = { ratio: 0.99, space_type_gen: true, default: false }
@@ -235,7 +539,7 @@ module OsLib_ModelGeneration
235
539
  end
236
540
  elsif building_type == 'SmallHotel'
237
541
  if ['DOE Ref Pre-1980', 'DOE Ref 1980-2004'].include?(template)
238
- hash['Corridor'] = { ratio: 0.1313, space_type_gen: true, default: false }
542
+ hash['Corridor'] = { ratio: 0.1313, space_type_gen: true, default: false, circ: true }
239
543
  hash['Elec/MechRoom'] = { ratio: 0.0038, space_type_gen: true, default: false }
240
544
  hash['ElevatorCore'] = { ratio: 0.0113, space_type_gen: true, default: false }
241
545
  hash['Exercise'] = { ratio: 0.0081, space_type_gen: true, default: false }
@@ -250,7 +554,7 @@ module OsLib_ModelGeneration
250
554
  hash['Stair'] = { ratio: 0.0400, space_type_gen: true, default: false }
251
555
  hash['Storage'] = { ratio: 0.0325, space_type_gen: true, default: false }
252
556
  else
253
- hash['Corridor'] = { ratio: 0.1313, space_type_gen: true, default: false }
557
+ hash['Corridor'] = { ratio: 0.1313, space_type_gen: true, default: false, circ: true }
254
558
  hash['Elec/MechRoom'] = { ratio: 0.0038, space_type_gen: true, default: false }
255
559
  hash['ElevatorCore'] = { ratio: 0.0113, space_type_gen: true, default: false }
256
560
  hash['Exercise'] = { ratio: 0.0081, space_type_gen: true, default: false }
@@ -270,7 +574,7 @@ module OsLib_ModelGeneration
270
574
  hash['Banquet'] = { ratio: 0.0585, space_type_gen: true, default: false }
271
575
  hash['Basement'] = { ratio: 0.1744, space_type_gen: false, default: false }
272
576
  hash['Cafe'] = { ratio: 0.0166, space_type_gen: true, default: false }
273
- hash['Corridor'] = { ratio: 0.1736, space_type_gen: true, default: false }
577
+ hash['Corridor'] = { ratio: 0.1736, space_type_gen: true, default: false, circ: true }
274
578
  hash['GuestRoom'] = { ratio: 0.4099, space_type_gen: true, default: true }
275
579
  hash['Kitchen'] = { ratio: 0.0091, space_type_gen: true, default: false }
276
580
  hash['Laundry'] = { ratio: 0.0069, space_type_gen: true, default: false }
@@ -281,7 +585,7 @@ module OsLib_ModelGeneration
281
585
  elsif building_type == 'Warehouse'
282
586
  hash['Bulk'] = { ratio: 0.6628, space_type_gen: true, default: true }
283
587
  hash['Fine'] = { ratio: 0.2882, space_type_gen: true, default: false }
284
- hash['Office'] = { ratio: 0.0490, space_type_gen: true, default: false, wwr: 0.71 }
588
+ hash['Office'] = { ratio: 0.0490, space_type_gen: true, default: false, wwr: 0.71, story_height: 14.0 }
285
589
  elsif building_type == 'RetailStandalone'
286
590
  hash['Back_Space'] = { ratio: 0.1656, space_type_gen: true, default: false }
287
591
  hash['Entry'] = { ratio: 0.0052, space_type_gen: true, default: false }
@@ -299,22 +603,22 @@ module OsLib_ModelGeneration
299
603
  hash['Kitchen'] = { ratio: 0.2728, space_type_gen: true, default: false }
300
604
  elsif building_type == 'MidriseApartment'
301
605
  hash['Apartment'] = { ratio: 0.8727, space_type_gen: true, default: true }
302
- hash['Corridor'] = { ratio: 0.0991, space_type_gen: true, default: false }
606
+ hash['Corridor'] = { ratio: 0.0991, space_type_gen: true, default: false, circ: true }
303
607
  hash['Office'] = { ratio: 0.0282, space_type_gen: true, default: false }
304
608
  elsif building_type == 'HighriseApartment'
305
609
  hash['Apartment'] = { ratio: 0.8896, space_type_gen: true, default: true }
306
- hash['Corridor'] = { ratio: 0.0991, space_type_gen: true, default: false }
610
+ hash['Corridor'] = { ratio: 0.0991, space_type_gen: true, default: false, circ: true }
307
611
  hash['Office'] = { ratio: 0.0113, space_type_gen: true, default: false }
308
612
  elsif building_type == 'Hospital'
309
613
  hash['Basement'] = { ratio: 0.1667, space_type_gen: false, default: false }
310
- hash['Corridor'] = { ratio: 0.1741, space_type_gen: true, default: false }
614
+ hash['Corridor'] = { ratio: 0.1741, space_type_gen: true, default: false, circ: true }
311
615
  hash['Dining'] = { ratio: 0.0311, space_type_gen: true, default: false }
312
616
  hash['ER_Exam'] = { ratio: 0.0099, space_type_gen: true, default: false }
313
617
  hash['ER_NurseStn'] = { ratio: 0.0551, space_type_gen: true, default: false }
314
618
  hash['ER_Trauma'] = { ratio: 0.0025, space_type_gen: true, default: false }
315
619
  hash['ER_Triage'] = { ratio: 0.0050, space_type_gen: true, default: false }
316
620
  hash['ICU_NurseStn'] = { ratio: 0.0298, space_type_gen: true, default: false }
317
- hash['ICE_Open'] = { ratio: 0.0275, space_type_gen: true, default: false }
621
+ hash['ICU_Open'] = { ratio: 0.0275, space_type_gen: true, default: false }
318
622
  hash['ICU_PatRm'] = { ratio: 0.0115, space_type_gen: true, default: false }
319
623
  hash['Kitchen'] = { ratio: 0.0414, space_type_gen: true, default: false }
320
624
  hash['Lab'] = { ratio: 0.0236, space_type_gen: true, default: false }
@@ -336,7 +640,7 @@ module OsLib_ModelGeneration
336
640
  hash['Elec/MechRoom'] = { ratio: 0.0109, space_type_gen: true, default: false }
337
641
  hash['ElevatorPumpRoom'] = { ratio: 0.0022, space_type_gen: true, default: false }
338
642
  hash['Exam'] = { ratio: 0.1029, space_type_gen: true, default: true }
339
- hash['Hall'] = { ratio: 0.1924, space_type_gen: true, default: false }
643
+ hash['Hall'] = { ratio: 0.1924, space_type_gen: true, default: false, circ: true }
340
644
  hash['IT_Room'] = { ratio: 0.0027, space_type_gen: true, default: false }
341
645
  hash['Janitor'] = { ratio: 0.0672, space_type_gen: true, default: false }
342
646
  hash['Lobby'] = { ratio: 0.0152, space_type_gen: true, default: false }
@@ -372,6 +676,142 @@ module OsLib_ModelGeneration
372
676
  hash['Meeting'] = { ratio: 0.99, space_type_gen: true, default: true }
373
677
  hash['Restroom'] = { ratio: 0.99, space_type_gen: true, default: true }
374
678
  hash['Vestibule'] = { ratio: 0.99, space_type_gen: true, default: true }
679
+ elsif building_type == 'Laboratory'
680
+ hash['Office'] = { ratio: 0.50, space_type_gen: true, default: true }
681
+ hash['Open lab'] = { ratio: 0.35, space_type_gen: true, default: true }
682
+ hash['Equipment corridor'] = { ratio: 0.05, space_type_gen: true, default: true }
683
+ hash['Lab with fume hood'] = { ratio: 0.10, space_type_gen: true, default: true }
684
+ elsif building_type == 'LargeDataCenterHighITE'
685
+ hash['StandaloneDataCenter'] = { ratio: 1.0, space_type_gen: true, default: true }
686
+ elsif building_type == 'LargeDataCenterLowITE'
687
+ hash['StandaloneDataCenter'] = { ratio: 1.0, space_type_gen: true, default: true }
688
+ elsif building_type == 'SmallDataCenterHighITE'
689
+ hash['ComputerRoom'] = { ratio: 1.0, space_type_gen: true, default: true }
690
+ elsif building_type == 'SmallDataCenterLowITE'
691
+ hash['ComputerRoom'] = { ratio: 1.0, space_type_gen: true, default: true }
692
+ # DEER Prototypes
693
+ elsif building_type == 'Asm'
694
+ hash['Auditorium'] = { ratio: 0.7658, space_type_gen: true, default: true }
695
+ hash['OfficeGeneral'] = { ratio: 0.2342, space_type_gen: true, default: false }
696
+ elsif building_type == 'ECC'
697
+ hash['Classroom'] = { ratio: 0.5558, space_type_gen: true, default: true }
698
+ hash['CompRoomClassRm'] = { ratio: 0.0319, space_type_gen: true, default: false }
699
+ hash['Shop'] = { ratio: 0.1249, space_type_gen: true, default: false }
700
+ hash['Dining'] = { ratio: 0.0876, space_type_gen: true, default: false }
701
+ hash['Kitchen'] = { ratio: 0.0188, space_type_gen: true, default: false }
702
+ hash['OfficeGeneral'] = { ratio: 0.181, space_type_gen: true, default: false }
703
+ elsif building_type == 'EPr'
704
+ hash['Classroom'] = { ratio: 0.53, space_type_gen: true, default: true }
705
+ hash['CorridorStairway'] = { ratio: 0.1, space_type_gen: true, default: false }
706
+ hash['Dining'] = { ratio: 0.15, space_type_gen: true, default: false }
707
+ hash['Gymnasium'] = { ratio: 0.15, space_type_gen: true, default: false }
708
+ hash['Kitchen'] = { ratio: 0.07, space_type_gen: true, default: false }
709
+ elsif building_type == 'ERC'
710
+ hash['Classroom'] = { ratio: 0.5, space_type_gen: true, default: true }
711
+ elsif building_type == 'ESe'
712
+ hash['Classroom'] = { ratio: 0.488, space_type_gen: true, default: true }
713
+ hash['CompRoomClassRm'] = { ratio: 0.021, space_type_gen: true, default: false }
714
+ hash['CorridorStairway'] = { ratio: 0.1, space_type_gen: true, default: false }
715
+ hash['Dining'] = { ratio: 0.15, space_type_gen: true, default: false }
716
+ hash['Gymnasium'] = { ratio: 0.15, space_type_gen: true, default: false }
717
+ hash['Kitchen'] = { ratio: 0.07, space_type_gen: true, default: false }
718
+ hash['OfficeGeneral'] = { ratio: 0.021, space_type_gen: true, default: true }
719
+ elsif building_type == 'EUn'
720
+ hash['Dining'] = { ratio: 0.0238, space_type_gen: true, default: false }
721
+ hash['Classroom'] = { ratio: 0.3056, space_type_gen: true, default: false }
722
+ hash['OfficeGeneral'] = { ratio: 0.3422, space_type_gen: true, default: true }
723
+ hash['CompRoomClassRm'] = { ratio: 0.038, space_type_gen: true, default: false }
724
+ hash['Kitchen'] = { ratio: 0.0105, space_type_gen: true, default: false }
725
+ hash['CorridorStairway'] = { ratio: 0.03, space_type_gen: true, default: false }
726
+ hash['FacMaint'] = { ratio: 0.08, space_type_gen: true, default: false }
727
+ hash['DormitoryRoom'] = { ratio: 0.1699, space_type_gen: true, default: false }
728
+ elsif building_type == 'Gro'
729
+ hash['GrocSales'] = { ratio: 0.8002, space_type_gen: true, default: true }
730
+ hash['RefWalkInCool'] = { ratio: 0.0312, space_type_gen: true, default: false }
731
+ hash['OfficeGeneral'] = { ratio: 0.07, space_type_gen: true, default: false }
732
+ hash['RefFoodPrep'] = { ratio: 0.0253, space_type_gen: true, default: false }
733
+ hash['RefWalkInFreeze'] = { ratio: 0.0162, space_type_gen: true, default: false }
734
+ hash['IndLoadDock'] = { ratio: 0.057, space_type_gen: true, default: false }
735
+ elsif building_type == 'Hsp'
736
+ hash['HspSurgOutptLab'] = { ratio: 0.2317, space_type_gen: true, default: false }
737
+ hash['Dining'] = { ratio: 0.0172, space_type_gen: true, default: false }
738
+ hash['Kitchen'] = { ratio: 0.0075, space_type_gen: true, default: false }
739
+ hash['OfficeGeneral'] = { ratio: 0.3636, space_type_gen: true, default: false }
740
+ hash['PatientRoom'] = { ratio: 0.38, space_type_gen: true, default: true }
741
+ elsif building_type == 'Htl'
742
+ hash['Dining'] = { ratio: 0.004, space_type_gen: true, default: false }
743
+ hash['BarCasino'] = { ratio: 0.005, space_type_gen: true, default: false }
744
+ hash['HotelLobby'] = { ratio: 0.0411, space_type_gen: true, default: false }
745
+ hash['OfficeGeneral'] = { ratio: 0.0205, space_type_gen: true, default: false }
746
+ hash['GuestRmCorrid'] = { ratio: 0.1011, space_type_gen: true, default: false }
747
+ hash['Laundry'] = { ratio: 0.0205, space_type_gen: true, default: false }
748
+ hash['GuestRmOcc'] = { ratio: 0.64224, space_type_gen: true, default: true }
749
+ hash['GuestRmUnOcc'] = { ratio: 0.16056, space_type_gen: true, default: true }
750
+ hash['Kitchen'] = { ratio: 0.005, space_type_gen: true, default: false }
751
+ elsif building_type == 'MBT'
752
+ hash['CompRoomData'] = { ratio: 0.02, space_type_gen: true, default: false }
753
+ hash['Laboratory'] = { ratio: 0.4534, space_type_gen: true, default: true }
754
+ hash['CorridorStairway'] = { ratio: 0.2, space_type_gen: true, default: false }
755
+ hash['Conference'] = { ratio: 0.02, space_type_gen: true, default: false }
756
+ hash['Dining'] = { ratio: 0.03, space_type_gen: true, default: false }
757
+ hash['OfficeOpen'] = { ratio: 0.2666, space_type_gen: true, default: false }
758
+ hash['Kitchen'] = { ratio: 0.01, space_type_gen: true, default: false }
759
+ elsif building_type == 'MFm'
760
+ hash['ResLiving'] = { ratio: 0.9297, space_type_gen: true, default: true }
761
+ hash['ResPublicArea'] = { ratio: 0.0725, space_type_gen: true, default: false }
762
+ elsif building_type == 'MLI'
763
+ hash['StockRoom'] = { ratio: 0.2, space_type_gen: true, default: false }
764
+ hash['Work'] = { ratio: 0.8, space_type_gen: true, default: true }
765
+ elsif building_type == 'Mtl'
766
+ hash['OfficeGeneral'] = { ratio: 0.02, space_type_gen: true, default: false }
767
+ hash['GuestRmCorrid'] = { ratio: 0.649, space_type_gen: true, default: true }
768
+ hash['Laundry'] = { ratio: 0.016, space_type_gen: true, default: false }
769
+ hash['GuestRmOcc'] = { ratio: 0.25208, space_type_gen: true, default: false }
770
+ hash['GuestRmUnOcc'] = { ratio: 0.06302, space_type_gen: true, default: false }
771
+ elsif building_type == 'Nrs'
772
+ hash['CorridorStairway'] = { ratio: 0.0555, space_type_gen: true, default: false }
773
+ hash['Dining'] = { ratio: 0.105, space_type_gen: true, default: false }
774
+ hash['Kitchen'] = { ratio: 0.045, space_type_gen: true, default: false }
775
+ hash['OfficeGeneral'] = { ratio: 0.35, space_type_gen: true, default: false }
776
+ hash['PatientRoom'] = { ratio: 0.4445, space_type_gen: true, default: true }
777
+ elsif building_type == 'OfL'
778
+ hash['LobbyWaiting'] = { ratio: 0.0412, space_type_gen: true, default: false }
779
+ hash['OfficeSmall'] = { ratio: 0.3704, space_type_gen: true, default: false }
780
+ hash['OfficeOpen'] = { ratio: 0.5296, space_type_gen: true, default: true }
781
+ hash['MechElecRoom'] = { ratio: 0.0588, space_type_gen: true, default: false }
782
+ elsif building_type == 'OfS'
783
+ hash['Hall'] = { ratio: 0.3141, space_type_gen: true, default: false }
784
+ hash['OfficeSmall'] = { ratio: 0.6859, space_type_gen: true, default: true }
785
+ elsif building_type == 'RFF'
786
+ hash['Dining'] = { ratio: 0.3997, space_type_gen: true, default: false }
787
+ hash['Kitchen'] = { ratio: 0.4, space_type_gen: true, default: true }
788
+ hash['LobbyWaiting'] = { ratio: 0.1501, space_type_gen: true, default: false }
789
+ hash['Restroom'] = { ratio: 0.0501, space_type_gen: true, default: false }
790
+ elsif building_type == 'RSD'
791
+ hash['Restroom'] = { ratio: 0.0357, space_type_gen: true, default: false }
792
+ hash['Dining'] = { ratio: 0.5353, space_type_gen: true, default: true }
793
+ hash['LobbyWaiting'] = { ratio: 0.1429, space_type_gen: true, default: false }
794
+ hash['Kitchen'] = { ratio: 0.2861, space_type_gen: true, default: false }
795
+ elsif building_type == 'Rt3'
796
+ hash['RetailSales'] = { ratio: 1.0, space_type_gen: true, default: true }
797
+ elsif building_type == 'RtL'
798
+ hash['OfficeGeneral'] = { ratio: 0.0359, space_type_gen: true, default: false }
799
+ hash['Work'] = { ratio: 0.04, space_type_gen: true, default: false }
800
+ hash['StockRoom'] = { ratio: 0.091, space_type_gen: true, default: false }
801
+ hash['RetailSales'] = { ratio: 0.8219, space_type_gen: true, default: true }
802
+ hash['Kitchen'] = { ratio: 0.0113, space_type_gen: true, default: false }
803
+ elsif building_type == 'RtS'
804
+ hash['RetailSales'] = { ratio: 0.8, space_type_gen: true, default: true }
805
+ hash['StockRoom'] = { ratio: 0.2, space_type_gen: true, default: false }
806
+ elsif building_type == 'SCn'
807
+ hash['WarehouseCond'] = { ratio: 1.0, space_type_gen: true, default: true }
808
+ elsif building_type == 'SUn'
809
+ hash['WarehouseUnCond'] = { ratio: 1.0, space_type_gen: true, default: true }
810
+ elsif building_type == 'WRf'
811
+ hash['IndLoadDock'] = { ratio: 0.08, space_type_gen: true, default: false }
812
+ hash['OfficeGeneral'] = { ratio: 0.02, space_type_gen: true, default: false }
813
+ hash['RefStorFreezer'] = { ratio: 0.4005, space_type_gen: true, default: false }
814
+ hash['RefStorCooler'] = { ratio: 0.4995, space_type_gen: true, default: true }
375
815
  else
376
816
  return false
377
817
  end
@@ -588,12 +1028,14 @@ module OsLib_ModelGeneration
588
1028
 
589
1029
  # set primary space type to building default space type
590
1030
  space_types = bar_hash[:space_types].sort_by { |k, v| v[:floor_area] }
591
- model.getBuilding.setSpaceType(space_types.last.first)
1031
+ if space_types.last.first.class.to_s == 'OpenStudio::Model::SpaceType'
1032
+ model.getBuilding.setSpaceType(space_types.last.first)
1033
+ end
592
1034
 
593
1035
  end
594
1036
 
595
1037
  # makeSpacesFromPolygons
596
- OsLib_Geometry.makeSpacesFromPolygons(runner, model, footprints, bar_hash[:floor_height], bar_hash[:num_stories], bar_hash[:center_of_footprint], story_hash)
1038
+ new_spaces = OsLib_Geometry.makeSpacesFromPolygons(runner, model, footprints, bar_hash[:floor_height], bar_hash[:num_stories], bar_hash[:center_of_footprint], story_hash)
597
1039
 
598
1040
  # put all of the spaces in the model into a vector for intersection and surface matching
599
1041
  spaces = OpenStudio::Model::SpaceVector.new
@@ -601,19 +1043,136 @@ module OsLib_ModelGeneration
601
1043
  spaces << space
602
1044
  end
603
1045
 
1046
+ # flag for intersection and matching type
1047
+ diagnostic_intersect = true
1048
+
604
1049
  # only intersect if make_mid_story_surfaces_adiabatic false
605
- if !(bar_hash[:make_mid_story_surfaces_adiabatic])
606
- # intersect surfaces
607
- # (when bottom floor has many space types and one above doesn't will end up with heavily subdivided floor. Maybe use adiabatic and don't intersect floor/ceilings)
608
- intersect_surfaces = true
609
- if intersect_surfaces
610
- OpenStudio::Model.intersectSurfaces(spaces)
611
- runner.registerInfo('Intersecting surfaces, this will create additional geometry.')
1050
+ if diagnostic_intersect
1051
+
1052
+ model.getPlanarSurfaces.each do |surface|
1053
+ array = []
1054
+ vertices = surface.vertices
1055
+ fixed = false
1056
+ vertices.each do |vertex|
1057
+ next if fixed
1058
+ if array.include?(vertex)
1059
+ # create a new set of vertices
1060
+ new_vertices = OpenStudio::Point3dVector.new
1061
+ array_b = []
1062
+ surface.vertices.each do |vertex_b|
1063
+ next if array_b.include?(vertex_b)
1064
+ new_vertices << vertex_b
1065
+ array_b << vertex_b
1066
+ end
1067
+ surface.setVertices(new_vertices)
1068
+ num_removed = vertices.size - surface.vertices.size
1069
+ runner.registerWarning("#{surface.name} has duplicate vertices. Started with #{vertices.size} vertices, removed #{num_removed}.")
1070
+ fixed = true
1071
+ else
1072
+ array << vertex
1073
+ end
1074
+ end
1075
+ end
1076
+
1077
+ # remove collinear points in a surface
1078
+ model.getPlanarSurfaces.each do |surface|
1079
+ new_vertices = OpenStudio.removeCollinear(surface.vertices)
1080
+ starting_count = surface.vertices.size
1081
+ final_count = new_vertices.size
1082
+ if final_count < starting_count
1083
+ runner.registerWarning("Removing #{starting_count - final_count} collinear vertices from #{surface.name}.")
1084
+ surface.setVertices(new_vertices)
1085
+ end
1086
+ end
1087
+
1088
+ # remove duplicate surfaces in a space (should be done after remove duplicate and collinear points)
1089
+ model.getSpaces.each do |space|
1090
+
1091
+ # secondary array to compare against
1092
+ surfaces_b = space.surfaces.sort
1093
+
1094
+ space.surfaces.sort.each do |surface_a|
1095
+
1096
+ # delete from secondary array
1097
+ surfaces_b.delete(surface_a)
1098
+
1099
+ surfaces_b.each do |surface_b|
1100
+ next if surface_a == surface_b # dont' test against same surface
1101
+ if surface_a.equalVertices(surface_b)
1102
+ runner.registerWarning("#{surface_a.name} and #{surface_b.name} in #{space.name} have duplicate geometry, removing #{surface_b.name}.")
1103
+ surface_b.remove
1104
+ elsif surface_a.reverseEqualVertices(surface_b)
1105
+ # todo - add logic to determine which face naormal is reversed and which is correct
1106
+ runner.registerWarning("#{surface_a.name} and #{surface_b.name} in #{space.name} have reversed geometry, removing #{surface_b.name}.")
1107
+ surface_b.remove
1108
+ end
1109
+
1110
+ end
1111
+
1112
+ end
612
1113
  end
613
- end
614
1114
 
615
- # match surfaces for each space in the vector
616
- OpenStudio::Model.matchSurfaces(spaces)
1115
+ if !(bar_hash[:make_mid_story_surfaces_adiabatic])
1116
+ # intersect and surface match two pair by pair
1117
+ spaces_b = model.getSpaces.sort
1118
+ # looping through vector of each space
1119
+ model.getSpaces.sort.each do |space_a|
1120
+ spaces_b.delete(space_a)
1121
+ spaces_b.each do |space_b|
1122
+ #runner.registerInfo("Intersecting and matching surfaces between #{space_a.name} and #{space.name}")
1123
+ spaces_temp = OpenStudio::Model::SpaceVector.new
1124
+ spaces_temp << space_a
1125
+ spaces_temp << space_b
1126
+ # intersect and sort
1127
+ OpenStudio::Model.intersectSurfaces(spaces_temp)
1128
+ OpenStudio::Model.matchSurfaces(spaces_temp)
1129
+ end
1130
+ end
1131
+ runner.registerInfo('Intersecting and matching surfaces in model, this will create additional geometry.')
1132
+ else #elsif bar_hash[:double_loaded_corridor] # only intersect spaces in each story, not between wtory
1133
+ model.getBuilding.buildingStories.each do |story|
1134
+ # intersect and surface match two pair by pair
1135
+ spaces_b = story.spaces.sort
1136
+ # looping through vector of each space
1137
+ story.spaces.sort.each do |space_a|
1138
+ spaces_b.delete(space_a)
1139
+ spaces_b.each do |space_b|
1140
+ spaces_temp = OpenStudio::Model::SpaceVector.new
1141
+ spaces_temp << space_a
1142
+ spaces_temp << space_b
1143
+ # intersect and sort
1144
+ OpenStudio::Model.intersectSurfaces(spaces_temp)
1145
+ OpenStudio::Model.matchSurfaces(spaces_temp)
1146
+ end
1147
+ end
1148
+ runner.registerInfo("Intersecting and matching surfaces in story #{story.name}, this will create additional geometry.")
1149
+ end
1150
+ end
1151
+
1152
+ else
1153
+
1154
+ if !(bar_hash[:make_mid_story_surfaces_adiabatic])
1155
+ # intersect surfaces
1156
+ # (when bottom floor has many space types and one above doesn't will end up with heavily subdivided floor. Maybe use adiabatic and don't intersect floor/ceilings)
1157
+ intersect_surfaces = true
1158
+ if intersect_surfaces
1159
+ OpenStudio::Model.intersectSurfaces(spaces)
1160
+ OpenStudio::Model.matchSurfaces(spaces)
1161
+ runner.registerInfo('Intersecting and matching surfaces in model, this will create additional geometry.')
1162
+ end
1163
+ else #elsif bar_hash[:double_loaded_corridor] # only intersect spaces in each story, not between wtory
1164
+ model.getBuilding.buildingStories.each do |story|
1165
+ story_spaces = OpenStudio::Model::SpaceVector.new
1166
+ story.spaces.sort.each do |space|
1167
+ story_spaces << space
1168
+ end
1169
+ OpenStudio::Model.intersectSurfaces(story_spaces)
1170
+ OpenStudio::Model.matchSurfaces(story_spaces)
1171
+ runner.registerInfo("Intersecting and matching surfaces in story #{story.name}, this will create additional geometry.")
1172
+ end
1173
+ end
1174
+
1175
+ end
617
1176
 
618
1177
  # set boundary conditions if not already set when geometry was created
619
1178
  # todo - update this to use space original z value vs. story name
@@ -621,6 +1180,7 @@ module OsLib_ModelGeneration
621
1180
  model.getBuildingStorys.each do |story|
622
1181
  next if !story.name.to_s.include?('Story B')
623
1182
  story.spaces.each do |space|
1183
+ next if not new_spaces.include?(space)
624
1184
  space.surfaces.each do |surface|
625
1185
  next if surface.surfaceType != 'Wall'
626
1186
  next if surface.outsideBoundaryCondition != 'Outdoors'
@@ -631,10 +1191,13 @@ module OsLib_ModelGeneration
631
1191
  end
632
1192
 
633
1193
  # sort stories (by name for now but need better way)
634
- # todo - need to change this so doesn't create issues when models have existing stories and spaces. Should be able to run it multiple times
635
1194
  sorted_stories = {}
636
- model.getBuildingStorys.each do |story|
637
- sorted_stories[story.name.to_s] = story
1195
+ new_spaces.each do |space|
1196
+ next if ! space.buildingStory.is_initialized
1197
+ story = space.buildingStory.get
1198
+ if ! sorted_stories.has_key?(name.to_s)
1199
+ sorted_stories[story.name.to_s] = story
1200
+ end
638
1201
  end
639
1202
 
640
1203
  # flag space types that have wwr overrides
@@ -651,7 +1214,7 @@ module OsLib_ModelGeneration
651
1214
  adiabatic_ceiling = true
652
1215
  end
653
1216
 
654
- # make all mid story floor and celings adiabiatc if requested
1217
+ # make all mid story floor and ceilings adiabatic if requested
655
1218
  if bar_hash[:make_mid_story_surfaces_adiabatic]
656
1219
  if i > 0
657
1220
  adiabatic_floor = true
@@ -665,6 +1228,7 @@ module OsLib_ModelGeneration
665
1228
  party_wall_facades = stories_flat[i][:story_party_walls]
666
1229
 
667
1230
  story.spaces.each do |space|
1231
+ next if not new_spaces.include?(space)
668
1232
  space.surfaces. each do |surface|
669
1233
  # set floor to adiabatic if requited
670
1234
  if adiabatic_floor && surface.surfaceType == 'Floor'
@@ -693,11 +1257,11 @@ module OsLib_ModelGeneration
693
1257
  space_type = surface.space.get.spaceType.get
694
1258
 
695
1259
  # see if space type has wwr value
696
- bar_hash[:space_types].each do |k,v|
697
- if v.has_key?(:space_type) && space_type == v[:space_type]
1260
+ bar_hash[:space_types].each do |k, v|
1261
+ if v.key?(:space_type) && space_type == v[:space_type]
698
1262
 
699
- # if matching space type specifies a wwr then override the orientaiton specific recommendations for this surface.
700
- if v.has_key?(:wwr)
1263
+ # if matching space type specifies a wwr then override the orientation specific recommendations for this surface.
1264
+ if v.key?(:wwr)
701
1265
  wwr_n = v[:wwr]
702
1266
  wwr_e = v[:wwr]
703
1267
  wwr_s = v[:wwr]
@@ -742,12 +1306,25 @@ module OsLib_ModelGeneration
742
1306
  end
743
1307
 
744
1308
  # report space types with custom wwr values
745
- space_type_wwr_overrides.each do |space_type,wwr|
746
- runner.registerInfo("For #{space_type.name} the default building wwr was replaced with a space type specifc value of #{wwr}")
1309
+ space_type_wwr_overrides.each do |space_type, wwr|
1310
+ runner.registerInfo("For #{space_type.name} the default building wwr was replaced with a space type specfic value of #{wwr}")
747
1311
  end
748
1312
 
1313
+ new_floor_area_si = 0.0
1314
+ new_spaces.each do |space|
1315
+ new_floor_area_si += space.floorArea * space.multiplier
1316
+ end
1317
+ new_floor_area_ip = OpenStudio.convert(new_floor_area_si, 'm^2', 'ft^2').get
1318
+
749
1319
  final_floor_area_ip = OpenStudio.convert(model.getBuilding.floorArea, 'm^2', 'ft^2').get
750
- runner.registerInfo("Created Bar envlope with floor area of #{OpenStudio.toNeatString(final_floor_area_ip, 0, true)} (ft^2)")
1320
+ if new_floor_area_ip == final_floor_area_ip
1321
+ runner.registerInfo("Created bar envelope with floor area of #{OpenStudio.toNeatString(new_floor_area_ip, 0, true)} ft^2.")
1322
+ else
1323
+ runner.registerInfo("Created bar envelope with floor area of #{OpenStudio.toNeatString(new_floor_area_ip, 0, true)} ft^2. Total building area is #{OpenStudio.toNeatString(final_floor_area_ip, 0, true)} ft^2.")
1324
+ end
1325
+
1326
+ return new_spaces
1327
+
751
1328
  end
752
1329
 
753
1330
  # make selected surfaces adiabatic
@@ -814,4 +1391,1888 @@ module OsLib_ModelGeneration
814
1391
 
815
1392
  return bar
816
1393
  end
1394
+
1395
+ def bar_hash_setup_run(runner,model,args,length,width,floor_height_si,center_of_footprint,space_types_hash,num_stories)
1396
+ # create envelope
1397
+ # populate bar_hash and create envelope with data from envelope_data_hash and user arguments
1398
+ bar_hash = {}
1399
+ bar_hash[:length] = length
1400
+ bar_hash[:width] = width
1401
+ bar_hash[:num_stories_below_grade] = args['num_stories_below_grade']
1402
+ bar_hash[:num_stories_above_grade] = args['num_stories_above_grade']
1403
+ bar_hash[:floor_height] = floor_height_si
1404
+ bar_hash[:center_of_footprint] = center_of_footprint
1405
+ bar_hash[:bar_division_method] = args['bar_division_method']
1406
+ bar_hash[:make_mid_story_surfaces_adiabatic] = args['make_mid_story_surfaces_adiabatic']
1407
+ bar_hash[:space_types] = space_types_hash
1408
+ bar_hash[:building_wwr_n] = args['wwr']
1409
+ bar_hash[:building_wwr_s] = args['wwr']
1410
+ bar_hash[:building_wwr_e] = args['wwr']
1411
+ bar_hash[:building_wwr_w] = args['wwr']
1412
+
1413
+ # round up non integer stoires to next integer
1414
+ num_stories_round_up = num_stories.ceil
1415
+ runner.registerInfo("Making bar with length of #{OpenStudio.toNeatString(OpenStudio.convert(length, 'm', 'ft').get, 0, true)} ft and width of #{OpenStudio.toNeatString(OpenStudio.convert(width, 'm', 'ft').get, 0, true)} ft")
1416
+
1417
+ # party_walls_array to be used by orientation specific or fractional party wall values
1418
+ party_walls_array = [] # this is an array of arrays, where each entry is effective building story with array of directions
1419
+
1420
+ if args['party_wall_stories_north'] + args['party_wall_stories_south'] + args['party_wall_stories_east'] + args['party_wall_stories_west'] > 0
1421
+
1422
+ # loop through effective number of stories add orientation specific party walls per user arguments
1423
+ num_stories_round_up.times do |i|
1424
+ test_value = i + 1 - bar_hash[:num_stories_below_grade]
1425
+
1426
+ array = []
1427
+ if args['party_wall_stories_north'] >= test_value
1428
+ array << 'north'
1429
+ end
1430
+ if args['party_wall_stories_south'] >= test_value
1431
+ array << 'south'
1432
+ end
1433
+ if args['party_wall_stories_east'] >= test_value
1434
+ array << 'east'
1435
+ end
1436
+ if args['party_wall_stories_west'] >= test_value
1437
+ array << 'west'
1438
+ end
1439
+
1440
+ # populate party_wall_array for this story
1441
+ party_walls_array << array
1442
+ end
1443
+ end
1444
+
1445
+ # calculate party walls if using party_wall_fraction method
1446
+ if args['party_wall_fraction'] > 0 && !party_walls_array.empty?
1447
+ runner.registerWarning('Both orientation and fractional party wall values arguments were populated, will ignore fractional party wall input')
1448
+ elsif args['party_wall_fraction'] > 0
1449
+
1450
+ # orientation of long and short side of building will vary based on building rotation
1451
+
1452
+ # full story ext wall area
1453
+ typical_length_facade_area = length * floor_height_si
1454
+ typical_width_facade_area = width * floor_height_si
1455
+
1456
+ # top story ext wall area, may be partial story
1457
+ partial_story_multiplier = (1.0 - args['num_stories_above_grade'].ceil + args['num_stories_above_grade'])
1458
+ area_multiplier = partial_story_multiplier
1459
+ edge_multiplier = Math.sqrt(area_multiplier)
1460
+ top_story_length = length * edge_multiplier
1461
+ top_story_width = width * edge_multiplier
1462
+ top_story_length_facade_area = top_story_length * floor_height_si
1463
+ top_story_width_facade_area = top_story_width * floor_height_si
1464
+
1465
+ total_exterior_wall_area = 2 * (length + width) * (args['num_stories_above_grade'].ceil - 1.0) * floor_height_si + 2 * (top_story_length + top_story_width) * floor_height_si
1466
+ target_party_wall_area = total_exterior_wall_area * args['party_wall_fraction']
1467
+
1468
+ width_counter = 0
1469
+ width_area = 0.0
1470
+ facade_area = typical_width_facade_area
1471
+ until (width_area + facade_area >= target_party_wall_area) || (width_counter == args['num_stories_above_grade'].ceil * 2)
1472
+ # update facade area for top story
1473
+ if width_counter == args['num_stories_above_grade'].ceil - 1 || width_counter == args['num_stories_above_grade'].ceil * 2 - 1
1474
+ facade_area = top_story_width_facade_area
1475
+ else
1476
+ facade_area = typical_width_facade_area
1477
+ end
1478
+
1479
+ width_counter += 1
1480
+ width_area += facade_area
1481
+
1482
+ end
1483
+ width_area_remainder = target_party_wall_area - width_area
1484
+
1485
+ length_counter = 0
1486
+ length_area = 0.0
1487
+ facade_area = typical_length_facade_area
1488
+ until (length_area + facade_area >= target_party_wall_area) || (length_counter == args['num_stories_above_grade'].ceil * 2)
1489
+ # update facade area for top story
1490
+ if length_counter == args['num_stories_above_grade'].ceil - 1 || length_counter == args['num_stories_above_grade'].ceil * 2 - 1
1491
+ facade_area = top_story_length_facade_area
1492
+ else
1493
+ facade_area = typical_length_facade_area
1494
+ end
1495
+
1496
+ length_counter += 1
1497
+ length_area += facade_area
1498
+ end
1499
+ length_area_remainder = target_party_wall_area - length_area
1500
+
1501
+ # get rotation and best fit to adjust orientation for fraction party wall
1502
+ rotation = args['building_rotation'] % 360.0 # should result in value between 0 and 360
1503
+ card_dir_array = [0.0, 90.0, 180.0, 270.0, 360.0]
1504
+ # reverse array to properly handle 45, 135, 225, and 315
1505
+ best_fit = card_dir_array.reverse.min_by { |x| (x.to_f - rotation).abs }
1506
+
1507
+ if ![90.0, 270.0].include? best_fit
1508
+ width_card_dir = ['east', 'west']
1509
+ length_card_dir = ['north', 'south']
1510
+ else # if rotation is closest to 90 or 270 then reverse which orientation is used for length and width
1511
+ width_card_dir = ['north', 'south']
1512
+ length_card_dir = ['east', 'west']
1513
+ end
1514
+
1515
+ # if dont' find enough on short sides
1516
+ if width_area_remainder <= typical_length_facade_area
1517
+
1518
+ num_stories_round_up.times do |i|
1519
+ if i + 1 <= args['num_stories_below_grade']
1520
+ party_walls_array << []
1521
+ next
1522
+ end
1523
+ if i + 1 - args['num_stories_below_grade'] <= width_counter
1524
+ if i + 1 - args['num_stories_below_grade'] <= width_counter - args['num_stories_above_grade']
1525
+ party_walls_array << width_card_dir
1526
+ else
1527
+ party_walls_array << [width_card_dir.first]
1528
+ end
1529
+ else
1530
+ party_walls_array << []
1531
+ end
1532
+ end
1533
+
1534
+ else # use long sides instead
1535
+
1536
+ num_stories_round_up.times do |i|
1537
+ if i + 1 <= args['num_stories_below_grade']
1538
+ party_walls_array << []
1539
+ next
1540
+ end
1541
+ if i + 1 - args['num_stories_below_grade'] <= length_counter
1542
+ if i + 1 - args['num_stories_below_grade'] <= length_counter - args['num_stories_above_grade']
1543
+ party_walls_array << length_card_dir
1544
+ else
1545
+ party_walls_array << [length_card_dir.first]
1546
+ end
1547
+ else
1548
+ party_walls_array << []
1549
+ end
1550
+ end
1551
+
1552
+ end
1553
+
1554
+ # TODO: - currently won't go past making two opposing sets of walls party walls. Info and registerValue are after create_bar in measure.rb
1555
+
1556
+ end
1557
+
1558
+ # populate bar hash with story information
1559
+ bar_hash[:stories] = {}
1560
+ num_stories_round_up.times do |i|
1561
+ if party_walls_array.empty?
1562
+ party_walls = []
1563
+ else
1564
+ party_walls = party_walls_array[i]
1565
+ end
1566
+
1567
+ # add below_partial_story
1568
+ if num_stories.ceil > num_stories && i == num_stories_round_up - 2
1569
+ below_partial_story = true
1570
+ else
1571
+ below_partial_story = false
1572
+ end
1573
+
1574
+ # bottom_story_ground_exposed_floor and top_story_exterior_exposed_roof already setup as bool
1575
+ bar_hash[:stories]["key #{i}"] = { story_party_walls: party_walls, story_min_multiplier: 1, story_included_in_building_area: true, below_partial_story: below_partial_story, bottom_story_ground_exposed_floor: args['bottom_story_ground_exposed_floor'], top_story_exterior_exposed_roof: args['top_story_exterior_exposed_roof'] }
1576
+ end
1577
+
1578
+ # create bar
1579
+ new_spaces = create_bar(runner, model, bar_hash, args['story_multiplier'])
1580
+
1581
+ # check expect roof and wall area
1582
+ target_footprint = bar_hash[:length] * bar_hash[:width]
1583
+ ground_floor_area = 0.0
1584
+ roof_area = 0.0
1585
+ new_spaces.each do |space|
1586
+ space.surfaces.each do |surface|
1587
+ if surface.surfaceType == "Floor" && surface.outsideBoundaryCondition == "Ground"
1588
+ ground_floor_area += surface.netArea
1589
+ elsif surface.surfaceType == "RoofCeiling" && surface.outsideBoundaryCondition == "Outdoors"
1590
+ roof_area += surface.netArea
1591
+ end
1592
+ end
1593
+ end
1594
+ # todo - extend to address when top and or bottom story are not exposed via argument
1595
+ if ground_floor_area > target_footprint + 0.001 || roof_area > target_footprint + 0.001
1596
+ #runner.registerError("Ground exposed floor or Roof area is larger than footprint, likely inter-floor surface matching and intersection error.")
1597
+ #return false
1598
+
1599
+ # not providing adiabatic work around when top story is partial story.
1600
+ if args['num_stories_above_grade'].to_f != args['num_stories_above_grade'].ceil
1601
+ runner.registerError("Ground exposed floor or Roof area is larger than footprint, likely inter-floor surface matching and intersection error.")
1602
+ return false
1603
+ else
1604
+ runner.registerInfo("Ground exposed floor or Roof area is larger than footprint, likely inter-floor surface matching and intersection error, altering impacted surfaces boundary condition to be adiabatic.")
1605
+ match_error = true
1606
+ end
1607
+ else
1608
+ match_error = false
1609
+ end
1610
+
1611
+ # todo - should be able to remove this fix after OpenStudio intersection issue is fixed. At that time turn the above message into an error with return false after it
1612
+ if match_error
1613
+
1614
+ # identify z value of top and bottom story
1615
+ bottom_story = nil
1616
+ top_story = nil
1617
+ new_spaces.each do |space|
1618
+ story = space.buildingStory.get
1619
+ nom_z = story.nominalZCoordinate.get
1620
+ if bottom_story.nil?
1621
+ bottom_story = nom_z
1622
+ elsif bottom_story > nom_z
1623
+ bottom_story = nom_z
1624
+ end
1625
+ if top_story.nil?
1626
+ top_story = nom_z
1627
+ elsif top_story < nom_z
1628
+ top_story = nom_z
1629
+ end
1630
+ end
1631
+
1632
+ # change boundary condition and intersection as needed.
1633
+ new_spaces.each do |space|
1634
+ if space.buildingStory.get.nominalZCoordinate.get > bottom_story
1635
+ # change floors
1636
+ space.surfaces.each do |surface|
1637
+ next if not surface.surfaceType == "Floor" && surface.outsideBoundaryCondition == "Ground"
1638
+ surface.setOutsideBoundaryCondition("Adiabatic")
1639
+ end
1640
+ end
1641
+ if space.buildingStory.get.nominalZCoordinate.get < top_story
1642
+ # change ceilings
1643
+ space.surfaces.each do |surface|
1644
+ next if not surface.surfaceType == "RoofCeiling" && surface.outsideBoundaryCondition == "Outdoors"
1645
+ surface.setOutsideBoundaryCondition("Adiabatic")
1646
+ end
1647
+ end
1648
+ end
1649
+
1650
+ end
1651
+
1652
+
1653
+ end
1654
+
1655
+ # bar_from_building_type_ratios
1656
+ # used for varieties of measures that create bar from building type ratios
1657
+ def bar_from_building_type_ratios(model, runner, user_arguments)
1658
+
1659
+ # assign the user inputs to variables
1660
+ args = OsLib_HelperMethods.createRunVariables(runner, model, user_arguments, arguments(model))
1661
+ if !args then return false end
1662
+
1663
+ # add in arguments that may not be passed in
1664
+ if !args.has_key?("double_loaded_corridor")
1665
+ args["double_loaded_corridor"] = "None" # use None when not in measure building type data may not contain this
1666
+ end
1667
+ if ! args.has_key?("perim_mult")
1668
+ args["perim_mult"] = 1.0 # will not make two bars for extended perimeter
1669
+ end
1670
+
1671
+ # lookup and replace argument values from upstream measures
1672
+ if args['use_upstream_args'] == true
1673
+ args.each do |arg, value|
1674
+ next if arg == 'use_upstream_args' # this argument should not be changed
1675
+ value_from_osw = OsLib_HelperMethods.check_upstream_measure_for_arg(runner, arg)
1676
+ if !value_from_osw.empty?
1677
+ runner.registerInfo("Replacing argument named #{arg} from current measure with a value of #{value_from_osw[:value]} from #{value_from_osw[:measure_name]}.")
1678
+ new_val = value_from_osw[:value]
1679
+ # TODO: - make code to handle non strings more robust. check_upstream_measure_for_arg could pass back the argument type
1680
+ if arg == 'total_bldg_floor_area'
1681
+ args[arg] = new_val.to_f
1682
+ elsif arg == 'num_stories_above_grade'
1683
+ args[arg] = new_val.to_f
1684
+ elsif arg == 'zipcode'
1685
+ args[arg] = new_val.to_i
1686
+ else
1687
+ args[arg] = new_val
1688
+ end
1689
+ end
1690
+ end
1691
+ end
1692
+
1693
+ # check expected values of double arguments
1694
+ fraction_args = ['bldg_type_b_fract_bldg_area',
1695
+ 'bldg_type_c_fract_bldg_area',
1696
+ 'bldg_type_d_fract_bldg_area',
1697
+ 'wwr', 'party_wall_fraction']
1698
+ fraction = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 0.0, 'max' => 1.0, 'min_eq_bool' => true, 'max_eq_bool' => true, 'arg_array' => fraction_args)
1699
+
1700
+ positive_args = ['total_bldg_floor_area']
1701
+ positive = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 0.0, 'max' => nil, 'min_eq_bool' => false, 'max_eq_bool' => false, 'arg_array' => positive_args)
1702
+
1703
+ one_or_greater_args = ['num_stories_above_grade']
1704
+ one_or_greater = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 1.0, 'max' => nil, 'min_eq_bool' => true, 'max_eq_bool' => false, 'arg_array' => one_or_greater_args)
1705
+
1706
+ non_neg_args = ['num_stories_below_grade',
1707
+ 'floor_height',
1708
+ 'ns_to_ew_ratio',
1709
+ 'party_wall_stories_north',
1710
+ 'party_wall_stories_south',
1711
+ 'party_wall_stories_east',
1712
+ 'party_wall_stories_west',
1713
+ 'single_floor_area',
1714
+ 'bar_width',]
1715
+ non_neg = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 0.0, 'max' => nil, 'min_eq_bool' => true, 'max_eq_bool' => false, 'arg_array' => non_neg_args)
1716
+
1717
+ # return false if any errors fail
1718
+ if !fraction then return false end
1719
+ if !positive then return false end
1720
+ if !one_or_greater then return false end
1721
+ if !non_neg then return false end
1722
+
1723
+ # if aspect ratio, story height or wwr have argument value of 0 then use smart building type defaults
1724
+ building_form_defaults = building_form_defaults(args['bldg_type_a'])
1725
+
1726
+ # store list of defaulted items
1727
+ defaulted_args = []
1728
+
1729
+ if args['ns_to_ew_ratio'] == 0.0
1730
+ args['ns_to_ew_ratio'] = building_form_defaults[:aspect_ratio]
1731
+ runner.registerInfo("0.0 value for aspect ratio will be replaced with smart default for #{args['bldg_type_a']} of #{building_form_defaults[:aspect_ratio]}.")
1732
+ end
1733
+
1734
+ if args['perim_mult'] == 0.0
1735
+ # if this is not defined then use default of 1.0
1736
+ if !building_form_defaults.has_key?(:perim_mult)
1737
+ args['perim_mult'] = 1.0
1738
+ else
1739
+ args['perim_mult'] = building_form_defaults[:perim_mult]
1740
+ end
1741
+ runner.registerInfo("0.0 value for minimum perimeter multiplier will be replaced with smart default for #{args['bldg_type_a']} of #{building_form_defaults[:perim_mult]}.")
1742
+ elsif args['perim_mult'] < 1.0
1743
+ runner.registerError("Other than the smart default value of 0, the minimum perimeter multiplier should be equal to 1.0 or greater.")
1744
+ return false
1745
+ end
1746
+
1747
+ if args['floor_height'] == 0.0
1748
+ args['floor_height'] = building_form_defaults[:typical_story]
1749
+ runner.registerInfo("0.0 value for floor height will be replaced with smart default for #{args['bldg_type_a']} of #{building_form_defaults[:typical_story]}.")
1750
+ defaulted_args << 'floor_height'
1751
+ end
1752
+ # because of this can't set wwr to 0.0. If that is desired then we can change this to check for 1.0 instead of 0.0
1753
+ if args['wwr'] == 0.0
1754
+ args['wwr'] = building_form_defaults[:wwr]
1755
+ runner.registerInfo("0.0 value for window to wall ratio will be replaced with smart default for #{args['bldg_type_a']} of #{building_form_defaults[:wwr]}.")
1756
+ end
1757
+
1758
+ # check that sum of fractions for b,c, and d is less than 1.0 (so something is left for primary building type)
1759
+ bldg_type_a_fract_bldg_area = 1.0 - args['bldg_type_b_fract_bldg_area'] - args['bldg_type_c_fract_bldg_area'] - args['bldg_type_d_fract_bldg_area']
1760
+ if bldg_type_a_fract_bldg_area <= 0.0
1761
+ runner.registerError('Primary Building Type fraction of floor area must be greater than 0. Please lower one or more of the fractions for Building Type B-D.')
1762
+ return false
1763
+ end
1764
+
1765
+ # Make the standard applier
1766
+ standard = Standard.build("#{args['template']}")
1767
+
1768
+ # report initial condition of model
1769
+ runner.registerInitialCondition("The building started with #{model.getSpaces.size} spaces.")
1770
+
1771
+ # determine of ns_ew needs to be mirrored
1772
+ mirror_ns_ew = false
1773
+ rotation = model.getBuilding.northAxis
1774
+ if rotation > 45.0 && rotation < 135.0
1775
+ mirror_ns_ew = true
1776
+ elsif rotation > 45.0 && rotation < 135.0
1777
+ mirror_ns_ew = true
1778
+ end
1779
+
1780
+ # remove non-resource objects not removed by removing the building
1781
+ remove_non_resource_objects(runner, model)
1782
+
1783
+ # rename building to infer template in downstream measure
1784
+ name_array = [args['template'], args['bldg_type_a']]
1785
+ if args['bldg_type_b_fract_bldg_area'] > 0 then name_array << args['bldg_type_b'] end
1786
+ if args['bldg_type_c_fract_bldg_area'] > 0 then name_array << args['bldg_type_c'] end
1787
+ if args['bldg_type_d_fract_bldg_area'] > 0 then name_array << args['bldg_type_d'] end
1788
+ model.getBuilding.setName(name_array.join('|').to_s)
1789
+
1790
+ # hash to whole building type data
1791
+ building_type_hash = {}
1792
+
1793
+ # gather data for bldg_type_a
1794
+ building_type_hash[args['bldg_type_a']] = {}
1795
+ building_type_hash[args['bldg_type_a']][:frac_bldg_area] = bldg_type_a_fract_bldg_area
1796
+ #building_type_hash[args['bldg_type_a']][:num_units] = args['bldg_type_a_num_units']
1797
+ building_type_hash[args['bldg_type_a']][:space_types] = get_space_types_from_building_type(args['bldg_type_a'], args['template'], true)
1798
+
1799
+ # gather data for bldg_type_b
1800
+ if args['bldg_type_b_fract_bldg_area'] > 0
1801
+ building_type_hash[args['bldg_type_b']] = {}
1802
+ building_type_hash[args['bldg_type_b']][:frac_bldg_area] = args['bldg_type_b_fract_bldg_area']
1803
+ #building_type_hash[args['bldg_type_b']][:num_units] = args['bldg_type_b_num_units']
1804
+ building_type_hash[args['bldg_type_b']][:space_types] = get_space_types_from_building_type(args['bldg_type_b'], args['template'], true)
1805
+ end
1806
+
1807
+ # gather data for bldg_type_c
1808
+ if args['bldg_type_c_fract_bldg_area'] > 0
1809
+ building_type_hash[args['bldg_type_c']] = {}
1810
+ building_type_hash[args['bldg_type_c']][:frac_bldg_area] = args['bldg_type_c_fract_bldg_area']
1811
+ #building_type_hash[args['bldg_type_c']][:num_units] = args['bldg_type_c_num_units']
1812
+ building_type_hash[args['bldg_type_c']][:space_types] = get_space_types_from_building_type(args['bldg_type_c'], args['template'], true)
1813
+ end
1814
+
1815
+ # gather data for bldg_type_d
1816
+ if args['bldg_type_d_fract_bldg_area'] > 0
1817
+ building_type_hash[args['bldg_type_d']] = {}
1818
+ building_type_hash[args['bldg_type_d']][:frac_bldg_area] = args['bldg_type_d_fract_bldg_area']
1819
+ #building_type_hash[args['bldg_type_d']][:num_units] = args['bldg_type_d_num_units']
1820
+ building_type_hash[args['bldg_type_d']][:space_types] = get_space_types_from_building_type(args['bldg_type_d'], args['template'], true)
1821
+ end
1822
+
1823
+ # creating space types for requested building types
1824
+ building_type_hash.each do |building_type, building_type_hash|
1825
+ runner.registerInfo("Creating Space Types for #{building_type}.")
1826
+
1827
+ # mapping building_type name is needed for a few methods
1828
+ building_type = standard.model_get_lookup_name(building_type)
1829
+
1830
+ # create space_type_map from array
1831
+ sum_of_ratios = 0.0
1832
+ building_type_hash[:space_types] = building_type_hash[:space_types].sort_by { |k, v| v[:ratio] }.to_h
1833
+ building_type_hash[:space_types].each do |space_type_name, hash|
1834
+ next if hash[:space_type_gen] == false # space types like undeveloped and basement are skipped.
1835
+
1836
+ # create space type
1837
+ space_type = OpenStudio::Model::SpaceType.new(model)
1838
+ space_type.setStandardsBuildingType(building_type)
1839
+ space_type.setStandardsSpaceType(space_type_name)
1840
+ space_type.setName("#{building_type} #{space_type_name}")
1841
+
1842
+ # set color
1843
+ test = standard.space_type_apply_rendering_color(space_type) # this uses openstudio-standards
1844
+ if !test
1845
+ # todo - once fixed in standards un-comment this
1846
+ #runner.registerWarning("Could not find color for #{args['template']} #{space_type.name}")
1847
+ end
1848
+
1849
+ # extend hash to hold new space type object
1850
+ hash[:space_type] = space_type
1851
+
1852
+ # add to sum_of_ratios counter for adjustment multiplier
1853
+ sum_of_ratios += hash[:ratio]
1854
+ end
1855
+
1856
+ # store multiplier needed to adjust sum of ratios to equal 1.0
1857
+ building_type_hash[:ratio_adjustment_multiplier] = 1.0 / sum_of_ratios
1858
+ end
1859
+
1860
+ # calculate length and with of bar
1861
+ total_bldg_floor_area_si = OpenStudio.convert(args['total_bldg_floor_area'], 'ft^2', 'm^2').get
1862
+ single_floor_area_si = OpenStudio.convert(args['single_floor_area'], 'ft^2', 'm^2').get
1863
+
1864
+ # store number of stories
1865
+ num_stories = args['num_stories_below_grade'] + args['num_stories_above_grade']
1866
+
1867
+ # handle user-assigned single floor plate size condition
1868
+ if args['single_floor_area'] > 0.0
1869
+ footprint_si = single_floor_area_si
1870
+ total_bldg_floor_area_si = footprint_si * num_stories.to_f
1871
+ runner.registerWarning('User-defined single floor area was used for calculation of total building floor area')
1872
+ # add warning if custom_height_bar is true and applicable building type is selected
1873
+ if args['custom_height_bar']
1874
+ runner.registerWarning("Cannot use custom height bar with single floor area method, will not create custom height bar.")
1875
+ args['custom_height_bar'] = false
1876
+ end
1877
+ else
1878
+ footprint_si = nil
1879
+ end
1880
+
1881
+ # populate space_types_hash
1882
+ space_types_hash = {}
1883
+ multi_height_space_types_hash = {}
1884
+ custom_story_heights = []
1885
+ if args['space_type_sort_logic'] == 'Building Type > Size'
1886
+ building_type_hash = building_type_hash.sort_by { |k, v| v[:frac_bldg_area] }
1887
+ end
1888
+ building_type_hash.each do |building_type, building_type_hash|
1889
+
1890
+ if args["double_loaded_corridor"] == "Primary Space Type"
1891
+
1892
+ # see if building type has circulation space type, if so then merge that along with default space type into hash key in place of space type
1893
+ default_st = nil
1894
+ circ_st = nil
1895
+ building_type_hash[:space_types].each do |space_type_name, hash|
1896
+ if hash[:default] then default_st = space_type_name end
1897
+ if hash[:circ] then circ_st = space_type_name end
1898
+ end
1899
+
1900
+ # update building hash
1901
+ if !default_st.nil? && !circ_st.nil?
1902
+ runner.registerInfo("Combining #{default_st} and #{circ_st} into a group representing a double loaded corridor")
1903
+
1904
+ # add new item
1905
+ building_type_hash[:space_types]["Double Loaded Corridor"] = {}
1906
+ double_loaded_st = building_type_hash[:space_types]["Double Loaded Corridor"]
1907
+ double_loaded_st[:ratio] = building_type_hash[:space_types][default_st][:ratio] + building_type_hash[:space_types][circ_st][:ratio]
1908
+ double_loaded_st[:double_loaded_corridor] = true
1909
+ double_loaded_st[:space_type] = model.getBuilding
1910
+ double_loaded_st[:children] = {}
1911
+ building_type_hash[:space_types][default_st][:orig_ratio] = building_type_hash[:ratio_adjustment_multiplier] * building_type_hash[:frac_bldg_area] * building_type_hash[:space_types][default_st][:ratio]
1912
+ building_type_hash[:space_types][circ_st][:orig_ratio] = building_type_hash[:ratio_adjustment_multiplier] * building_type_hash[:frac_bldg_area] * building_type_hash[:space_types][circ_st][:ratio]
1913
+ building_type_hash[:space_types][default_st][:name] = default_st
1914
+ building_type_hash[:space_types][circ_st][:name] = circ_st
1915
+ double_loaded_st[:children][:default] = building_type_hash[:space_types][default_st]
1916
+ double_loaded_st[:children][:circ] = building_type_hash[:space_types][circ_st]
1917
+ double_loaded_st[:orig_ratio] = 0.0
1918
+
1919
+ # zero out ratios from old item (don't delete because I still want the space types made)
1920
+ building_type_hash[:space_types][default_st][:ratio] = 0.0
1921
+ building_type_hash[:space_types][circ_st][:ratio] = 0.0
1922
+ end
1923
+ end
1924
+
1925
+ building_type_hash[:space_types].each do |space_type_name, hash|
1926
+ next if hash[:space_type_gen] == false
1927
+
1928
+ space_type = hash[:space_type]
1929
+ ratio_of_bldg_total = hash[:ratio] * building_type_hash[:ratio_adjustment_multiplier] * building_type_hash[:frac_bldg_area]
1930
+ final_floor_area = ratio_of_bldg_total * total_bldg_floor_area_si # I think I can just pass ratio but passing in area is cleaner
1931
+
1932
+ # only add custom height space if 0 is used for floor_height
1933
+ if defaulted_args.include?('floor_height') && hash.key?(:story_height) && args['custom_height_bar']
1934
+ multi_height_space_types_hash[space_type] = { floor_area: final_floor_area, space_type: space_type, story_height: hash[:story_height] }
1935
+ if hash.key?(:orig_ratio) then multi_height_space_types_hash[space_type][:orig_ratio] = hash[:orig_ratio] end
1936
+ custom_story_heights << hash[:story_height]
1937
+ if args['wwr'] == 0 && hash.key?(:wwr)
1938
+ multi_height_space_types_hash[space_type][:wwr] = hash[:wwr]
1939
+ end
1940
+ else
1941
+ # only add wwr if 0 used for wwr arg and if space type has wwr as key
1942
+ space_types_hash[space_type] = { floor_area: final_floor_area, space_type: space_type }
1943
+ if hash.key?(:orig_ratio) then space_types_hash[space_type][:orig_ratio] = hash[:orig_ratio] end
1944
+ if args['wwr'] == 0 && hash.key?(:wwr)
1945
+ space_types_hash[space_type][:wwr] = hash[:wwr]
1946
+ end
1947
+ if hash[:double_loaded_corridor]
1948
+ space_types_hash[space_type][:children] = hash[:children]
1949
+ end
1950
+ end
1951
+ end
1952
+ end
1953
+
1954
+ # resort if not sorted by building type
1955
+ if args['space_type_sort_logic'] == "Size"
1956
+ # added code to convert to hash. I use sort_by 3 other times, but those seem to be working fine as is now.
1957
+ space_types_hash = Hash[space_types_hash.sort_by { |k, v| v[:floor_area] }]
1958
+ end
1959
+
1960
+ # calculate targets for testing
1961
+ target_areas = {} # used for checks
1962
+ target_areas_cust_height = 0.0
1963
+ space_types_hash.each do |k,v|
1964
+ if v.key?(:orig_ratio)
1965
+ target_areas[k] = v[:orig_ratio] * total_bldg_floor_area_si
1966
+ else
1967
+ target_areas[k] = v[:floor_area]
1968
+ end
1969
+ end
1970
+ multi_height_space_types_hash.each do |k,v|
1971
+ if v.key?(:orig_ratio)
1972
+ target_areas[k] = v[:orig_ratio] * total_bldg_floor_area_si
1973
+ target_areas_cust_height += v[:orig_ratio] * total_bldg_floor_area_si
1974
+ else
1975
+ target_areas[k] = v[:floor_area]
1976
+ target_areas_cust_height += v[:floor_area]
1977
+ end
1978
+ end
1979
+
1980
+ # gather inputs
1981
+ if footprint_si.nil?
1982
+ footprint_si = (total_bldg_floor_area_si - target_areas_cust_height) / num_stories.to_f
1983
+ end
1984
+ floor_height_si = OpenStudio.convert(args['floor_height'], 'ft', 'm').get
1985
+ min_allow_size = OpenStudio.convert(15.0,'ft','m').get
1986
+ specified_bar_width_si = OpenStudio.convert(args['bar_width'],'ft','m').get
1987
+
1988
+ # set custom width
1989
+ if specified_bar_width_si > 0
1990
+ runner.registerInfo("Ignoring perimeter multiplier argument when non zero width argument is used")
1991
+ if footprint_si / specified_bar_width_si >= min_allow_size
1992
+ width = specified_bar_width_si
1993
+ length = footprint_si / width
1994
+ else
1995
+ length = min_allow_size
1996
+ width = footprint_si / length
1997
+ runner.registerWarning("User specified width results in a length that is too short, adjusting width to be narrower than specified.")
1998
+ end
1999
+ width_cust_height = specified_bar_width_si
2000
+ else
2001
+ width = Math.sqrt(footprint_si / args['ns_to_ew_ratio'])
2002
+ length = footprint_si / width
2003
+ width_cust_height = Math.sqrt(target_areas_cust_height / args['ns_to_ew_ratio'])
2004
+ end
2005
+ length_cust_height = target_areas_cust_height / width_cust_height
2006
+ if args['perim_mult'] > 1.0 && target_areas_cust_height > 0.0
2007
+ # todo - update tests that hit this warning
2008
+ runner.registerWarning("Ignoring perimeter multiplier for bar that represents custom height spaces.")
2009
+ end
2010
+
2011
+ # check if dual bar is needed
2012
+ dual_bar = false
2013
+ if specified_bar_width_si > 0.0 && args['bar_division_method'] == 'Multiple Space Types - Individual Stories Sliced'
2014
+ if length/width != args['ns_to_ew_ratio']
2015
+
2016
+ if args['ns_to_ew_ratio'] >= 1.0 && args['ns_to_ew_ratio'] > length/width
2017
+ runner.registerWarning("Can't meet target aspect ratio of #{args['ns_to_ew_ratio']}, Lowering it to #{length/width} ")
2018
+ args['ns_to_ew_ratio'] = length/width
2019
+ elsif args['ns_to_ew_ratio'] < 1.0 && args['ns_to_ew_ratio'] > length/width
2020
+ runner.registerWarning("Can't meet target aspect ratio of #{args['ns_to_ew_ratio']}, Increasing it to #{length/width} ")
2021
+ args['ns_to_ew_ratio'] = length/width
2022
+ else
2023
+ # check if each bar would be longer then 15 feet, then set as dual bar and override perimeter multiplier
2024
+ length_alt1 = ((args['ns_to_ew_ratio'] * footprint_si) / width + 2 * args['ns_to_ew_ratio'] * width - 2 * width)/(1 + args['ns_to_ew_ratio'])
2025
+ length_alt2 = length - length_alt1
2026
+ if [length_alt1,length_alt2].min >= min_allow_size
2027
+ dual_bar = true
2028
+ else
2029
+ runner.registerInfo("Second bar would be below minimum length, will model as single bar")
2030
+ # swap length and width if single bar and aspect ratio less than 1
2031
+ if args['ns_to_ew_ratio'] < 1.0
2032
+ width = length
2033
+ length = specified_bar_width_si
2034
+ end
2035
+ end
2036
+ end
2037
+ end
2038
+ elsif args['perim_mult'] > 1.0 && args['bar_division_method'] == 'Multiple Space Types - Individual Stories Sliced'
2039
+ runner.registerInfo("You selected a perimeter multiplier greater than 1.0 for a supported bar division method. This will result in two detached rectangular buildings if secondary bar meets minimum size requirements.")
2040
+ dual_bar = true
2041
+ elsif args['perim_mult'] > 1.0
2042
+ runner.registerWarning("You selected a perimeter multiplier greater than 1.0 but didn't select a bar division method that supports this. The value for this argument will be ignored by the measure")
2043
+ end
2044
+
2045
+ # calculations for dual bar, which later will be setup to run create_bar twice
2046
+ if dual_bar
2047
+ min_perim = 2 * width + 2 * length
2048
+ target_area = footprint_si
2049
+ target_perim = min_perim * args['perim_mult']
2050
+ tol_testing = 0.00001
2051
+ dual_bar_calc_approach = nil # stretched, adiabatic_ends_bar_b, dual_bar
2052
+ runner.registerInfo("Minimum rectangle is #{OpenStudio.toNeatString(OpenStudio.convert(length, 'm', 'ft').get, 0, true)} ft x #{OpenStudio.toNeatString(OpenStudio.convert(width, 'm', 'ft').get, 0, true)} ft with an area of #{OpenStudio.toNeatString(OpenStudio.convert(length * width, 'm^2', 'ft^2').get, 0, true)} ft^2. Perimeter is #{OpenStudio.toNeatString(OpenStudio.convert(min_perim, 'm', 'ft').get, 0, true)} ft.")
2053
+ runner.registerInfo("Target dual bar perimeter is #{OpenStudio.toNeatString(OpenStudio.convert(target_perim, 'm', 'ft').get, 0, true)} ft.")
2054
+
2055
+ # determine which of the three paths to hit target perimeter multiplier are possible
2056
+ # A use dual bar non adiabatic
2057
+ # B use dual bar adiabatic
2058
+ # C use stretched bar (requires model to miss ns/ew ratio)
2059
+
2060
+ # custom quadratic equation to solve two bars with common width 2l^2 - p*l + 4a = 0
2061
+ if target_perim**2 - 32 * footprint_si > 0
2062
+ if specified_bar_width_si > 0
2063
+ runner.registerInfo("Ignoring perimeter multiplier argument and using use specified bar width.")
2064
+ dual_double_end_width = specified_bar_width_si
2065
+ dual_double_end_length = footprint_si / dual_double_end_width
2066
+ else
2067
+ dual_double_end_length = 0.25 * (target_perim + Math.sqrt(target_perim**2 - 32 * footprint_si))
2068
+ dual_double_end_width = footprint_si / dual_double_end_length
2069
+ end
2070
+
2071
+ # now that stretched bar is made, determine where to split it and rotate
2072
+ bar_a_length = (args['ns_to_ew_ratio'] * (dual_double_end_length + dual_double_end_width) - dual_double_end_width)/(1 + args['ns_to_ew_ratio'])
2073
+ bar_b_length = dual_double_end_length - bar_a_length
2074
+ area_a = bar_a_length * dual_double_end_width
2075
+ area_b = bar_b_length * dual_double_end_width
2076
+ else
2077
+ # this will throw it to adiabatic ends test
2078
+ bar_a_length = 0
2079
+ bar_b_length = 0
2080
+ end
2081
+
2082
+ if bar_a_length >= min_allow_size && bar_b_length >= min_allow_size
2083
+ dual_bar_calc_approach = 'dual_bar'
2084
+ else
2085
+ # adiabatic bar input calcs
2086
+ if target_perim**2 - 16 * footprint_si > 0
2087
+ adiabatic_dual_double_end_length = 0.25 * (target_perim + Math.sqrt(target_perim**2 - 16 * footprint_si))
2088
+ adiabatic_dual_double_end_width = footprint_si / adiabatic_dual_double_end_length
2089
+ # test for unexpected
2090
+ unexpected = false
2091
+ if (target_area - adiabatic_dual_double_end_length*adiabatic_dual_double_end_width).abs > tol_testing then unexpected = true end
2092
+ if specified_bar_width_si == 0
2093
+ if (target_perim - (adiabatic_dual_double_end_length * 2 + adiabatic_dual_double_end_width * 2)).abs > tol_testing then unexpected = true end
2094
+ end
2095
+ if unexpected
2096
+ runner.registerWarning("Unexpected values for dual rectangle adiabatic ends bar b.")
2097
+ end
2098
+ # now that stretched bar is made, determine where to split it and rotate
2099
+ adiabatic_bar_a_length = (args['ns_to_ew_ratio'] * (adiabatic_dual_double_end_length + adiabatic_dual_double_end_width))/(1 + args['ns_to_ew_ratio'])
2100
+ adiabatic_bar_b_length = adiabatic_dual_double_end_length - adiabatic_bar_a_length
2101
+ adiabatic_area_a = adiabatic_bar_a_length * adiabatic_dual_double_end_width
2102
+ adiabatic_area_b = adiabatic_bar_b_length * adiabatic_dual_double_end_width
2103
+ else
2104
+ # this will throw it stretched single bar
2105
+ adiabatic_bar_a_length = 0
2106
+ adiabatic_bar_b_length = 0
2107
+ end
2108
+ if adiabatic_bar_a_length >= min_allow_size && adiabatic_bar_b_length >= min_allow_size
2109
+ dual_bar_calc_approach = 'adiabatic_ends_bar_b'
2110
+ else
2111
+ dual_bar_calc_approach = 'stretched'
2112
+ end
2113
+ end
2114
+
2115
+ # apply prescribed approach for stretched or dual bar
2116
+ if dual_bar_calc_approach == 'dual_bar'
2117
+ runner.registerInfo("Stretched #{OpenStudio.toNeatString(OpenStudio.convert(dual_double_end_length, 'm', 'ft').get, 0, true)} ft x #{OpenStudio.toNeatString(OpenStudio.convert(dual_double_end_width, 'm', 'ft').get, 0, true)} ft rectangle has an area of #{OpenStudio.toNeatString(OpenStudio.convert(dual_double_end_length * dual_double_end_width, 'm^2', 'ft^2').get, 0, true)} ft^2. When split in two the perimeter will be #{OpenStudio.toNeatString(OpenStudio.convert(dual_double_end_length * 2 + dual_double_end_width * 4, 'm', 'ft').get, 0, true)} ft")
2118
+ if (target_area - dual_double_end_length*dual_double_end_width).abs > tol_testing || (target_perim - (dual_double_end_length * 2 + dual_double_end_width * 4)).abs > tol_testing
2119
+ runner.registerWarning("Unexpected values for dual rectangle.")
2120
+ end
2121
+
2122
+ runner.registerInfo("For stretched split bar, to match target ns/ew aspect ratio #{OpenStudio.toNeatString(OpenStudio.convert(bar_a_length, 'm', 'ft').get, 0, true)} ft of bar should be horizontal, with #{OpenStudio.toNeatString(OpenStudio.convert(bar_b_length, 'm', 'ft').get, 0, true)} ft turned 90 degrees. Combined area is #{OpenStudio.toNeatString(OpenStudio.convert(area_a + area_b, 'm^2', 'ft^2').get, 0, true)} ft^2. Combined perimeter is #{OpenStudio.toNeatString(OpenStudio.convert(bar_a_length*2 + bar_b_length*2 + dual_double_end_width*4, 'm', 'ft').get, 0, true)} ft")
2123
+ if (target_area - (area_a + area_b)).abs > tol_testing || (target_perim - (bar_a_length*2 + bar_b_length*2 + dual_double_end_width*4)).abs > tol_testing
2124
+ runner.registerWarning("Unexpected values for rotated dual rectangle")
2125
+ end
2126
+ elsif dual_bar_calc_approach == 'adiabatic_ends_bar_b'
2127
+ runner.registerInfo("Can't hit target perimeter with two rectangles, need to make two ends adiabatic")
2128
+
2129
+ runner.registerInfo("For dual bar with adiabatic ends on bar b, to reach target aspect ratio #{OpenStudio.toNeatString(OpenStudio.convert(adiabatic_bar_a_length, 'm', 'ft').get, 0, true)} ft of bar should be north/south, with #{OpenStudio.toNeatString(OpenStudio.convert(adiabatic_bar_b_length, 'm', 'ft').get, 0, true)} ft turned 90 degrees. Combined area is #{OpenStudio.toNeatString(OpenStudio.convert(adiabatic_area_a + adiabatic_area_b, 'm^2', 'ft^2').get, 0, true)} ft^2}. Combined perimeter is #{OpenStudio.toNeatString(OpenStudio.convert(adiabatic_bar_a_length*2 + adiabatic_bar_b_length*2 + adiabatic_dual_double_end_width*2, 'm', 'ft').get, 0, true)} ft")
2130
+ if (target_area - (adiabatic_area_a + adiabatic_area_b)).abs > tol_testing || (target_perim - (adiabatic_bar_a_length*2 + adiabatic_bar_b_length*2 + adiabatic_dual_double_end_width*2)).abs > tol_testing
2131
+ runner.registerWarning("Unexpected values for rotated dual rectangle adiabatic ends bar b")
2132
+ end
2133
+ else # stretched bar
2134
+ dual_bar = false
2135
+
2136
+ stretched_length = 0.25 * (target_perim + Math.sqrt(target_perim**2 - 16 * footprint_si))
2137
+ stretched_width = footprint_si / stretched_length
2138
+ if (target_area - stretched_length*stretched_width).abs > tol_testing || (target_perim - (stretched_length + stretched_width)*2) > tol_testing
2139
+ runner.registerWarning("Unexpected values for single stretched")
2140
+ end
2141
+
2142
+ width = stretched_width
2143
+ length = stretched_length
2144
+ runner.registerInfo("Creating a dual bar to match the target minimum perimeter multiplier at the given aspect ratio would result in a bar with edge shorter than #{OpenStudio.toNeatString(OpenStudio.convert(min_allow_size, 'm', 'ft').get, 0, true)} ft. Will create a single stretched bar instead that hits the target perimeter with a slightly different ns/ew aspect ratio.")
2145
+ end
2146
+ end
2147
+
2148
+ bars = {}
2149
+ bars['primary'] = {}
2150
+ if dual_bar
2151
+ if mirror_ns_ew && dual_bar_calc_approach == 'dual_bar'
2152
+ bars['primary'][:length] = dual_double_end_width
2153
+ bars['primary'][:width] = bar_a_length
2154
+ elsif dual_bar_calc_approach == 'dual_bar'
2155
+ bars['primary'][:length] = bar_a_length
2156
+ bars['primary'][:width] = dual_double_end_width
2157
+ elsif mirror_ns_ew
2158
+ bars['primary'][:length] = adiabatic_dual_double_end_width
2159
+ bars['primary'][:width] = adiabatic_bar_a_length
2160
+ else
2161
+ bars['primary'][:length] = adiabatic_bar_a_length
2162
+ bars['primary'][:width] = adiabatic_dual_double_end_width
2163
+ end
2164
+ else
2165
+ if mirror_ns_ew
2166
+ bars['primary'][:length] = width
2167
+ bars['primary'][:width] = length
2168
+ else
2169
+ bars['primary'][:length] = length
2170
+ bars['primary'][:width] = width
2171
+ end
2172
+ end
2173
+ bars['primary'][:floor_height_si] = floor_height_si # can make use of this when breaking out multi-height spaces
2174
+ bars['primary'][:num_stories] = num_stories
2175
+ bars['primary'][:center_of_footprint] = OpenStudio::Point3d.new(0.0,0.0,0.0)
2176
+ space_types_hash_secondary = {}
2177
+ if dual_bar
2178
+ # loop through each story and move portion for other bar to its own hash
2179
+ primary_footprint = bars['primary'][:length] * bars['primary'][:width]
2180
+ secondary_footprint = target_area - primary_footprint
2181
+ footprint_counter = primary_footprint
2182
+ secondary_footprint_counter = secondary_footprint
2183
+ story_counter = 0
2184
+ pri_sec_tol = 0.0001 #m^2
2185
+ pri_sec_min_area = 0.0001 #m^2
2186
+ space_types_hash.each do |k,v|
2187
+ space_type_left = v[:floor_area]
2188
+
2189
+ # do not go to next space type until this one is evaulate, which may span stories
2190
+ until space_type_left == 0.0 || story_counter >= num_stories
2191
+
2192
+ # use secondary footprint if any left
2193
+ if secondary_footprint_counter > 0.0
2194
+ hash_area = [space_type_left,secondary_footprint_counter].min
2195
+
2196
+ # confirm that the part of space type use or what is left is greater than min allowed value
2197
+ projected_space_type_left = space_type_left - hash_area
2198
+ test_a = if hash_area >= pri_sec_min_area then true else false end
2199
+ test_b = if projected_space_type_left >= pri_sec_min_area || projected_space_type_left == 0.0 then true else false end
2200
+ test_c = if k == space_types_hash.keys.last then true else false end # if last space type accept sliver, no other space to infil
2201
+ if (test_a && test_b) || test_c
2202
+ if space_types_hash_secondary.has_key?(k)
2203
+ # add to what was added for previous story
2204
+ space_types_hash_secondary[k][:floor_area] += hash_area
2205
+ else
2206
+ # add new space type to hash
2207
+ if v.has_key?(:children)
2208
+ space_types_hash_secondary[k] = {:floor_area => hash_area, :space_type => v[:space_type], :children => v[:children],}
2209
+ else
2210
+ space_types_hash_secondary[k] = {:floor_area => hash_area, :space_type => v[:space_type]}
2211
+ end
2212
+ end
2213
+ space_types_hash[k][:floor_area] -= hash_area
2214
+ secondary_footprint_counter -= hash_area
2215
+ space_type_left -= hash_area
2216
+ else
2217
+ runner.registerInfo("Shifting space types between bars to avoid sliver of #{k.name}.")
2218
+ end
2219
+ end
2220
+
2221
+ # remove space if entirely used up by secondary bar
2222
+ if space_types_hash[k][:floor_area] <= pri_sec_tol
2223
+ space_types_hash.delete(k)
2224
+ space_type_left = 0.0
2225
+ else
2226
+ # then look at primary bar
2227
+ hash_area_pri = [space_type_left,footprint_counter].min
2228
+ footprint_counter -= hash_area_pri
2229
+ space_type_left -= hash_area_pri
2230
+ end
2231
+
2232
+ # reset counter when full
2233
+ if footprint_counter <= pri_sec_tol && secondary_footprint_counter <= pri_sec_tol
2234
+ # check if this is partial top floor
2235
+ story_counter += 1
2236
+ if num_stories < story_counter + 1
2237
+ footprint_counter = primary_footprint * (num_stories - story_counter)
2238
+ secondary_footprint_counter = secondary_footprint * (num_stories - story_counter)
2239
+ else
2240
+ footprint_counter = primary_footprint
2241
+ secondary_footprint_counter = secondary_footprint
2242
+ end
2243
+ end
2244
+ end
2245
+ end
2246
+ end
2247
+
2248
+ # setup bar_hash and run create_bar
2249
+ bars['primary'][:space_types_hash] = space_types_hash
2250
+ bars['primary'][:args] = args
2251
+ v = bars['primary']
2252
+ bar_hash_setup_run(runner,model,v[:args],v[:length],v[:width],v[:floor_height_si],v[:center_of_footprint],v[:space_types_hash],v[:num_stories])
2253
+
2254
+ # store offset value for multiple bars
2255
+ if args.has_key?('bar_sep_dist_mult') && args['bar_sep_dist_mult'] > 0.0
2256
+ offset_val = num_stories.ceil * floor_height_si * args['bar_sep_dist_mult']
2257
+ elsif args.has_key?('bar_sep_dist_mult')
2258
+ runner.registerWarning("Positive valu eis required for bar_sep_dist_mult, ignoring input and using value of 0.1")
2259
+ offset_val = num_stories.ceil * floor_height_si * 0.1
2260
+ else
2261
+ offset_val = num_stories.ceil * floor_height_si * 10.0
2262
+ end
2263
+
2264
+ if dual_bar
2265
+ args2 = args.clone
2266
+ bars['secondary'] = {}
2267
+ if mirror_ns_ew && dual_bar_calc_approach == 'dual_bar'
2268
+ bars['secondary'][:length] = bar_b_length
2269
+ bars['secondary'][:width] = dual_double_end_width
2270
+ elsif dual_bar_calc_approach == 'dual_bar'
2271
+ bars['secondary'][:length] = dual_double_end_width
2272
+ bars['secondary'][:width] = bar_b_length
2273
+ elsif mirror_ns_ew
2274
+ bars['secondary'][:length] = adiabatic_bar_b_length
2275
+ bars['secondary'][:width] = adiabatic_dual_double_end_width
2276
+ args2['party_wall_stories_east'] = num_stories.ceil
2277
+ args2['party_wall_stories_west'] = num_stories.ceil
2278
+ else
2279
+ bars['secondary'][:length] = adiabatic_dual_double_end_width
2280
+ bars['secondary'][:width] = adiabatic_bar_b_length
2281
+ args2['party_wall_stories_south'] = num_stories.ceil
2282
+ args2['party_wall_stories_north'] = num_stories.ceil
2283
+ end
2284
+ bars['secondary'][:floor_height_si] = floor_height_si # can make use of this when breaking out multi-height spaces
2285
+ bars['secondary'][:num_stories] = num_stories
2286
+ bars['secondary'][:space_types_hash] = space_types_hash_secondary
2287
+ if dual_bar_calc_approach == 'adiabatic_ends_bar_b'
2288
+ # warn that combination of dual bar with low perimeter multiplier and use of party wall may result in discrepency between target and actual adiabatic walls
2289
+ if args['party_wall_fraction'] > 0 || args['party_wall_stories_north'] > 0 || args['party_wall_stories_south'] > 0 || args['party_wall_stories_east'] > 0 || args['party_wall_stories_west'] > 0
2290
+ runner.registerWarning('The combination of low perimeter multiplier and use of non zero party wall inputs may result in discrepency between target and actual adiabatic walls. This is due to the need to create adiabatic walls on secondary bar to maintian target building perimeter.')
2291
+ else
2292
+ runner.registerInfo('Adiabatic ends added to secondary bar because target perimeter multiplier could not be met with two full rectangular footprints.')
2293
+ end
2294
+ bars['secondary'][:center_of_footprint] = OpenStudio::Point3d.new(adiabatic_bar_a_length * 0.5 + adiabatic_dual_double_end_width * 0.5 + offset_val,adiabatic_bar_b_length * 0.5 + adiabatic_dual_double_end_width * 0.5 + offset_val,0.0)
2295
+ else
2296
+ bars['secondary'][:center_of_footprint] = OpenStudio::Point3d.new(bar_a_length * 0.5 + dual_double_end_width * 0.5 + offset_val,bar_b_length * 0.5 + dual_double_end_width * 0.5 + offset_val,0.0)
2297
+ end
2298
+ bars['secondary'][:args] = args2
2299
+
2300
+ # setup bar_hash and run create_bar
2301
+ v = bars['secondary']
2302
+ bar_hash_setup_run(runner,model,v[:args],v[:length],v[:width],v[:floor_height_si],v[:center_of_footprint],v[:space_types_hash],v[:num_stories])
2303
+
2304
+ end
2305
+
2306
+ # future development (up against primary bar run intersection and surface matching after add all bars, avoid interior windows)
2307
+ # I could loop through each space type and give them unique height but for now will just take largest height and make bar of that height, which is fine for prototypes
2308
+ if multi_height_space_types_hash.size > 0
2309
+ args3 = args.clone
2310
+ bars['custom_height'] = {}
2311
+ if mirror_ns_ew
2312
+ bars['custom_height'][:length] = width_cust_height
2313
+ bars['custom_height'][:width] = length_cust_height
2314
+ else
2315
+ bars['custom_height'][:length] = length_cust_height
2316
+ bars['custom_height'][:width] = width_cust_height
2317
+ end
2318
+ if args['party_wall_stories_east'] + args['party_wall_stories_west'] + args['party_wall_stories_south'] + args['party_wall_stories_north'] > 0.0
2319
+ runner.registerWarning("Ignorning party wall inputs for custom height bar")
2320
+ end
2321
+
2322
+ # disable party walls
2323
+ args3['party_wall_stories_east'] = 0
2324
+ args3['party_wall_stories_west'] = 0
2325
+ args3['party_wall_stories_south'] = 0
2326
+ args3['party_wall_stories_north'] = 0
2327
+
2328
+ # setup stories
2329
+ args3['num_stories_below_grade'] = 0
2330
+ args3['num_stories_above_grade'] = 1
2331
+
2332
+ bars['custom_height'][:floor_height_si] = floor_height_si # can make use of this when breaking out multi-height spaces
2333
+ bars['custom_height'][:num_stories] = num_stories
2334
+ bars['custom_height'][:center_of_footprint] = OpenStudio::Point3d.new(bars['primary'][:length] * -0.5 - length_cust_height * 0.5 - offset_val,0.0,0.0)
2335
+ bars['custom_height'][:floor_height_si] = OpenStudio.convert(custom_story_heights.max,'ft','m').get
2336
+ bars['custom_height'][:num_stories] = 1
2337
+ bars['custom_height'][:space_types_hash] = multi_height_space_types_hash
2338
+ bars['custom_height'][:args] = args3
2339
+
2340
+ v = bars['custom_height']
2341
+ bar_hash_setup_run(runner,model,v[:args],v[:length],v[:width],v[:floor_height_si],v[:center_of_footprint],v[:space_types_hash],v[:num_stories])
2342
+ end
2343
+
2344
+ # diagnostic log
2345
+ sum_actual = 0.0
2346
+ sum_target = 0.0
2347
+ throw_error = false
2348
+
2349
+ # check expected floor areas against actual
2350
+ model.getSpaceTypes.sort.each do |space_type|
2351
+ next if !target_areas.key? space_type # space type in model not part of building type(s), maybe issue warning
2352
+
2353
+ # convert to IP
2354
+ actual_ip = OpenStudio.convert(space_type.floorArea, 'm^2', 'ft^2').get
2355
+ target_ip = OpenStudio.convert(target_areas[space_type], 'm^2', 'ft^2').get
2356
+ sum_actual += actual_ip
2357
+ sum_target += target_ip
2358
+
2359
+ if (space_type.floorArea - target_areas[space_type]).abs >= 1.0
2360
+
2361
+ if !args['bar_division_method'].include? 'Single Space Type'
2362
+ runner.registerError("#{space_type.name} doesn't have the expected floor area (actual #{OpenStudio.toNeatString(actual_ip, 0, true)} ft^2, target #{OpenStudio.toNeatString(target_ip, 0, true)} ft^2)")
2363
+ throw_error = true
2364
+ else
2365
+ # will see this if use Single Space type division method on multi-use building or single building type without whole building space type
2366
+ runner.registerWarning("#{space_type.name} doesn't have the expected floor area (actual #{OpenStudio.toNeatString(actual_ip, 0, true)} ft^2, target #{OpenStudio.toNeatString(target_ip, 0, true)} ft^2)")
2367
+ end
2368
+
2369
+ end
2370
+ end
2371
+
2372
+ # report summary then throw error
2373
+ if throw_error
2374
+ runner.registerError("Sum of actual floor area is #{sum_actual} ft^2, sum of target floor area is #{sum_target}.")
2375
+ return false
2376
+ end
2377
+
2378
+ # check party wall fraction by looping through surfaces
2379
+ if args['party_wall_fraction'] > 0
2380
+ actual_ext_wall_area = model.getBuilding.exteriorWallArea
2381
+ actual_party_wall_area = 0.0
2382
+ model.getSurfaces.each do |surface|
2383
+ next if surface.outsideBoundaryCondition != 'Adiabatic'
2384
+ next if surface.surfaceType != 'Wall'
2385
+ actual_party_wall_area += surface.grossArea * surface.space.get.multiplier
2386
+ end
2387
+ actual_party_wall_fraction = actual_party_wall_area / (actual_party_wall_area + actual_ext_wall_area)
2388
+ runner.registerInfo("Target party wall fraction is #{args['party_wall_fraction']}. Realized fraction is #{actual_party_wall_fraction.round(2)}")
2389
+ runner.registerValue('party_wall_fraction_actual', actual_party_wall_fraction)
2390
+ end
2391
+
2392
+ # check ns/ew aspect ratio (harder to check when party walls are added)
2393
+ wall_and_window_by_orientation = OsLib_Geometry.getExteriorWindowAndWllAreaByOrientation(model,model.getSpaces)
2394
+ wall_ns = (wall_and_window_by_orientation['northWall'] + wall_and_window_by_orientation['southWall'])
2395
+ wall_ew = wall_and_window_by_orientation['eastWall'] + wall_and_window_by_orientation['westWall']
2396
+ wall_ns_ip = OpenStudio.convert(wall_ns,'m^2','ft^2').get
2397
+ wall_ew_ip = OpenStudio.convert(wall_ew,'m^2','ft^2').get
2398
+ runner.registerValue('wall_area_ip',wall_ns_ip + wall_ew_ip,'ft^2')
2399
+ runner.registerValue('ns_wall_area_ip',wall_ns_ip,'ft^2')
2400
+ runner.registerValue('ew_wall_area_ip',wall_ew_ip,'ft^2')
2401
+ # for now using perimeter of ground floor and average story area (building area / num_stories)
2402
+ runner.registerValue('floor_area_to_perim_ratio',model.getBuilding.floorArea / (OsLib_Geometry.calculate_perimeter(model) * num_stories))
2403
+ runner.registerValue('bar_width',OpenStudio.convert(bars['primary'][:width],'m','ft').get,'ft')
2404
+
2405
+ if args['party_wall_fraction'] > 0 || args['party_wall_stories_north'] > 0 || args['party_wall_stories_south'] > 0 || args['party_wall_stories_east'] > 0 || args['party_wall_stories_west'] > 0
2406
+ runner.registerInfo("Target facade area by orientation not validated when party walls are applied")
2407
+ elsif args['num_stories_above_grade'] != args['num_stories_above_grade'].ceil
2408
+ runner.registerInfo("Target facade area by orientation not validated when partial top story is used")
2409
+ elsif dual_bar_calc_approach == 'stretched'
2410
+ runner.registerInfo("Target facade area by orientation not validated when single stretched bar has to be used to meet target minimum perimeter multiplier")
2411
+ elsif defaulted_args.include?('floor_height') && args['custom_height_bar'] && multi_height_space_types_hash.size > 0
2412
+ runner.registerInfo("Target facade area by orientation not validated when a dedicated bar is added for space types with custom heights")
2413
+ elsif args['bar_width'] > 0
2414
+ runner.registerInfo("Target facade area by orientation not validated when a dedicated custom bar width is defined")
2415
+ else
2416
+
2417
+ # adjust length versus width based on building rotation
2418
+ if mirror_ns_ew
2419
+ wall_target_ns_ip = 2 * OpenStudio.convert(width,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2420
+ wall_target_ew_ip = 2 * OpenStudio.convert(length,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2421
+ else
2422
+ wall_target_ns_ip = 2 * OpenStudio.convert(length,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2423
+ wall_target_ew_ip = 2 * OpenStudio.convert(width,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2424
+ end
2425
+ flag_error = false
2426
+ if (wall_target_ns_ip - wall_ns_ip).abs > 0.1
2427
+ runner.registerError("North/South walls don't have the expected area (actual #{OpenStudio.toNeatString(wall_ns_ip, 4, true)} ft^2, target #{OpenStudio.toNeatString(wall_target_ns_ip, 4, true)} ft^2)")
2428
+ flag_error = true
2429
+ end
2430
+ if (wall_target_ew_ip - wall_ew_ip).abs > 0.1
2431
+ runner.registerError("East/West walls don't have the expected area (actual #{OpenStudio.toNeatString(wall_ew_ip, 4, true)} ft^2, target #{OpenStudio.toNeatString(wall_target_ew_ip, 4, true)} ft^2)")
2432
+ flag_error = true
2433
+ end
2434
+ if flag_error
2435
+ return false
2436
+ end
2437
+ end
2438
+
2439
+ # test for excessive exterior roof area (indication of problem with intersection and or surface matching)
2440
+ ext_roof_area = model.getBuilding.exteriorSurfaceArea - model.getBuilding.exteriorWallArea
2441
+ expected_roof_area = args['total_bldg_floor_area'] / (args['num_stories_above_grade'] + args['num_stories_below_grade']).to_f
2442
+ if ext_roof_area > expected_roof_area && single_floor_area_si == 0.0 # only test if using whole-building area input
2443
+ runner.registerError('Roof area larger than expected, may indicate problem with inter-floor surface intersection or matching.')
2444
+ return false
2445
+ end
2446
+
2447
+ # set building rotation
2448
+ initial_rotation = model.getBuilding.northAxis
2449
+ if args['building_rotation'] != initial_rotation
2450
+ model.getBuilding.setNorthAxis(args['building_rotation'])
2451
+ runner.registerInfo("Set Building Rotation to #{model.getBuilding.northAxis}. Rotation altered after geometry generation is completed, as a result party wall orientation and aspect ratio may not reflect input values.")
2452
+ end
2453
+
2454
+ # report final condition of model
2455
+ runner.registerFinalCondition("The building finished with #{model.getSpaces.size} spaces.")
2456
+
2457
+ return true
2458
+ end
2459
+
2460
+ # typical
2461
+ # used for varieties of measures that create typical building from model
2462
+ def typical_building_from_model(model, runner, user_arguments)
2463
+
2464
+ # assign the user inputs to variables
2465
+ args = OsLib_HelperMethods.createRunVariables(runner, model, user_arguments, arguments(model))
2466
+ if !args then return false end
2467
+
2468
+ # lookup and replace argument values from upstream measures
2469
+ if args['use_upstream_args'] == true
2470
+ args.each do |arg, value|
2471
+ next if arg == 'use_upstream_args' # this argument should not be changed
2472
+ value_from_osw = OsLib_HelperMethods.check_upstream_measure_for_arg(runner, arg)
2473
+ if !value_from_osw.empty?
2474
+ runner.registerInfo("Replacing argument named #{arg} from current measure with a value of #{value_from_osw[:value]} from #{value_from_osw[:measure_name]}.")
2475
+ new_val = value_from_osw[:value]
2476
+ # TODO: - make code to handle non strings more robust. check_upstream_measure_for_arg coudl pass bakc the argument type
2477
+ if arg == 'total_bldg_floor_area'
2478
+ args[arg] = new_val.to_f
2479
+ elsif arg == 'num_stories_above_grade'
2480
+ args[arg] = new_val.to_f
2481
+ elsif arg == 'zipcode'
2482
+ args[arg] = new_val.to_i
2483
+ else
2484
+ args[arg] = new_val
2485
+ end
2486
+ end
2487
+ end
2488
+ end
2489
+
2490
+ # validate fraction parking
2491
+ fraction = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 0.0, 'max' => 1.0, 'min_eq_bool' => true, 'max_eq_bool' => true, 'arg_array' => ['onsite_parking_fraction'])
2492
+ if !fraction then return false end
2493
+
2494
+ # validate unmet hours tolerance
2495
+ unmet_hours_tolerance_valid = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 0.0, 'max' => 5.0, 'min_eq_bool' => true, 'max_eq_bool' => true, 'arg_array' => ['unmet_hours_tolerance'])
2496
+ if !unmet_hours_tolerance_valid then return false end
2497
+
2498
+ # validate weekday hours of operation
2499
+ wkdy_op_hrs_start_time_hr = nil
2500
+ wkdy_op_hrs_start_time_min = nil
2501
+ wkdy_op_hrs_duration_hr = nil
2502
+ wkdy_op_hrs_duration_min = nil
2503
+ if args['modify_wkdy_op_hrs']
2504
+ # weekday start time hr
2505
+ wkdy_op_hrs_start_time_hr = args['wkdy_op_hrs_start_time'].floor
2506
+ if wkdy_op_hrs_start_time_hr < 0 || wkdy_op_hrs_start_time_hr > 24
2507
+ runner.registerError("Weekday operating hours start time hrs must be between 0 and 24. #{args['wkdy_op_hrs_start_time']} was entered.")
2508
+ return false
2509
+ end
2510
+
2511
+ # weekday start time min
2512
+ wkdy_op_hrs_start_time_min = (60.0 * (args['wkdy_op_hrs_start_time'] - args['wkdy_op_hrs_start_time'].floor)).floor
2513
+ if wkdy_op_hrs_start_time_min < 0 || wkdy_op_hrs_start_time_min > 59
2514
+ runner.registerError("Weekday operating hours start time mins must be between 0 and 59. #{args['wkdy_op_hrs_start_time']} was entered.")
2515
+ return false
2516
+ end
2517
+
2518
+ # weekday duration hr
2519
+ wkdy_op_hrs_duration_hr = args['wkdy_op_hrs_duration'].floor
2520
+ if wkdy_op_hrs_duration_hr < 0 || wkdy_op_hrs_duration_hr > 24
2521
+ runner.registerError("Weekday operating hours duration hrs must be between 0 and 24. #{args['wkdy_op_hrs_duration']} was entered.")
2522
+ return false
2523
+ end
2524
+
2525
+ # weekday duration min
2526
+ wkdy_op_hrs_duration_min = (60.0 * (args['wkdy_op_hrs_duration'] - args['wkdy_op_hrs_duration'].floor)).floor
2527
+ if wkdy_op_hrs_duration_min < 0 || wkdy_op_hrs_duration_min > 59
2528
+ runner.registerError("Weekday operating hours duration mins must be between 0 and 59. #{args['wkdy_op_hrs_duration']} was entered.")
2529
+ return false
2530
+ end
2531
+
2532
+ # check that weekday start time plus duration does not exceed 24 hrs
2533
+ if (wkdy_op_hrs_start_time_hr + wkdy_op_hrs_duration_hr + (wkdy_op_hrs_start_time_min + wkdy_op_hrs_duration_min)/60.0) > 24.0
2534
+ runner.registerInfo("Weekday start time of #{args['wkdy_op_hrs_start']} plus duration of #{args['wkdy_op_hrs_duration']} is more than 24 hrs, hours of operation overlap midnight.")
2535
+ end
2536
+ end
2537
+
2538
+ # validate weekend hours of operation
2539
+ wknd_op_hrs_start_time_hr = nil
2540
+ wknd_op_hrs_start_time_min = nil
2541
+ wknd_op_hrs_duration_hr = nil
2542
+ wknd_op_hrs_duration_min = nil
2543
+ if args['modify_wknd_op_hrs']
2544
+ # weekend start time hr
2545
+ wknd_op_hrs_start_time_hr = args['wknd_op_hrs_start_time'].floor
2546
+ if wknd_op_hrs_start_time_hr < 0 || wknd_op_hrs_start_time_hr > 24
2547
+ runner.registerError("Weekend operating hours start time hrs must be between 0 and 24. #{args['wknd_op_hrs_start_time_change']} was entered.")
2548
+ return false
2549
+ end
2550
+
2551
+ # weekend start time min
2552
+ wknd_op_hrs_start_time_min = (60.0 * (args['wknd_op_hrs_start_time'] - args['wknd_op_hrs_start_time'].floor)).floor
2553
+ if wknd_op_hrs_start_time_min < 0 || wknd_op_hrs_start_time_min > 59
2554
+ runner.registerError("Weekend operating hours start time mins must be between 0 and 59. #{args['wknd_op_hrs_start_time_change']} was entered.")
2555
+ return false
2556
+ end
2557
+
2558
+ # weekend duration hr
2559
+ wknd_op_hrs_duration_hr = args['wknd_op_hrs_duration'].floor
2560
+ if wknd_op_hrs_duration_hr < 0 || wknd_op_hrs_duration_hr > 24
2561
+ runner.registerError("Weekend operating hours duration hrs must be between 0 and 24. #{args['wknd_op_hrs_duration']} was entered.")
2562
+ return false
2563
+ end
2564
+
2565
+ # weekend duration min
2566
+ wknd_op_hrs_duration_min = (60.0 * (args['wknd_op_hrs_duration'] - args['wknd_op_hrs_duration'].floor)).floor
2567
+ if wknd_op_hrs_duration_min < 0 || wknd_op_hrs_duration_min > 59
2568
+ runner.registerError("Weekend operating hours duration min smust be between 0 and 59. #{args['wknd_op_hrs_duration']} was entered.")
2569
+ return false
2570
+ end
2571
+
2572
+ # check that weekend start time plus duration does not exceed 24 hrs
2573
+ if (wknd_op_hrs_start_time_hr + wknd_op_hrs_duration_hr + (wknd_op_hrs_start_time_min + wknd_op_hrs_duration_min)/60.0) > 24.0
2574
+ runner.registerInfo("Weekend start time of #{args['wknd_op_hrs_start']} plus duration of #{args['wknd_op_hrs_duration']} is more than 24 hrs, hours of operation overlap midnight.")
2575
+ end
2576
+ end
2577
+
2578
+ # report initial condition of model
2579
+ initial_objects = model.getModelObjects.size
2580
+ runner.registerInitialCondition("The building started with #{initial_objects} objects.")
2581
+
2582
+ # open channel to log messages
2583
+ reset_log
2584
+
2585
+ # Make the standard applier
2586
+ standard = Standard.build((args['template']).to_s)
2587
+
2588
+ # validate climate zone
2589
+ if !args.has_key?('climate_zone') || args['climate_zone'] == 'Lookup From Model'
2590
+ climate_zone = standard.model_get_building_climate_zone_and_building_type(model)['climate_zone']
2591
+ runner.registerInfo("Using climate zone #{climate_zone} from model")
2592
+ else
2593
+ climate_zone = args['climate_zone']
2594
+ runner.registerInfo("Using climate zone #{climate_zone} from user arguments")
2595
+ end
2596
+ if climate_zone == ''
2597
+ runner.registerError("Could not determine climate zone from measure arguments or model.")
2598
+ return false
2599
+ end
2600
+
2601
+ # make sure daylight savings is turned on up prior to any sizing runs being done.
2602
+ if args['enable_dst']
2603
+ start_date = '2nd Sunday in March'
2604
+ end_date = '1st Sunday in November'
2605
+
2606
+ runperiodctrl_daylgtsaving = model.getRunPeriodControlDaylightSavingTime
2607
+ runperiodctrl_daylgtsaving.setStartDate(start_date)
2608
+ runperiodctrl_daylgtsaving.setEndDate(end_date)
2609
+ end
2610
+
2611
+ # add internal loads to space types
2612
+ if args['add_space_type_loads']
2613
+
2614
+ # remove internal loads
2615
+ if args['remove_objects']
2616
+ model.getSpaceLoads.each do |instance|
2617
+ next if instance.name.to_s.include?('Elevator') # most prototype building types model exterior elevators with name Elevator
2618
+ next if instance.to_InternalMass.is_initialized
2619
+ next if instance.to_WaterUseEquipment.is_initialized
2620
+ instance.remove
2621
+ end
2622
+ model.getDesignSpecificationOutdoorAirs.each(&:remove)
2623
+ model.getDefaultScheduleSets.each(&:remove)
2624
+ end
2625
+
2626
+ model.getSpaceTypes.each do |space_type|
2627
+ # Don't add infiltration here; will be added later in the script
2628
+ test = standard.space_type_apply_internal_loads(space_type, true, true, true, true, true, false)
2629
+ if test == false
2630
+ runner.registerWarning("Could not add loads for #{space_type.name}. Not expected for #{args['template']}")
2631
+ next
2632
+ end
2633
+
2634
+ # apply internal load schedules
2635
+ # the last bool test it to make thermostat schedules. They are now added in HVAC section instead of here
2636
+ standard.space_type_apply_internal_load_schedules(space_type, true, true, true, true, true, true, false)
2637
+
2638
+ # extend space type name to include the args['template']. Consider this as well for load defs
2639
+ space_type.setName("#{space_type.name} - #{args['template']}")
2640
+ runner.registerInfo("Adding loads to space type named #{space_type.name}")
2641
+ end
2642
+
2643
+ # warn if spaces in model without space type
2644
+ spaces_without_space_types = []
2645
+ model.getSpaces.each do |space|
2646
+ next if space.spaceType.is_initialized
2647
+ spaces_without_space_types << space
2648
+ end
2649
+ if !spaces_without_space_types.empty?
2650
+ runner.registerWarning("#{spaces_without_space_types.size} spaces do not have space types assigned, and wont' receive internal loads from standards space type lookups.")
2651
+ end
2652
+ end
2653
+
2654
+ # identify primary building type (used for construction, and ideally HVAC as well)
2655
+ building_types = {}
2656
+ model.getSpaceTypes.each do |space_type|
2657
+ # populate hash of building types
2658
+ if space_type.standardsBuildingType.is_initialized
2659
+ bldg_type = space_type.standardsBuildingType.get
2660
+ if !building_types.key?(bldg_type)
2661
+ building_types[bldg_type] = space_type.floorArea
2662
+ else
2663
+ building_types[bldg_type] += space_type.floorArea
2664
+ end
2665
+ else
2666
+ runner.registerWarning("Can't identify building type for #{space_type.name}")
2667
+ end
2668
+ end
2669
+ primary_bldg_type = building_types.key(building_types.values.max) # TODO: - this fails if no space types, or maybe just no space types with standards
2670
+ lookup_building_type = standard.model_get_lookup_name(primary_bldg_type) # Used for some lookups in the standards gem
2671
+ model.getBuilding.setStandardsBuildingType(primary_bldg_type)
2672
+
2673
+ # make construction set and apply to building
2674
+ if args['add_constructions']
2675
+
2676
+ # remove default construction sets
2677
+ if args['remove_objects']
2678
+ model.getDefaultConstructionSets.each(&:remove)
2679
+ end
2680
+
2681
+ # TODO: - allow building type and space type specific constructions set selection.
2682
+ if ['SmallHotel', 'LargeHotel', 'MidriseApartment', 'HighriseApartment'].include?(primary_bldg_type)
2683
+ is_residential = 'Yes'
2684
+ else
2685
+ is_residential = 'No'
2686
+ end
2687
+ bldg_def_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, is_residential)
2688
+ if bldg_def_const_set.is_initialized
2689
+ bldg_def_const_set = bldg_def_const_set.get
2690
+ if is_residential then bldg_def_const_set.setName("Res #{bldg_def_const_set.name}") end
2691
+ model.getBuilding.setDefaultConstructionSet(bldg_def_const_set)
2692
+ runner.registerInfo("Adding default construction set named #{bldg_def_const_set.name}")
2693
+ else
2694
+ runner.registerError("Could not create default construction set for the building type #{lookup_building_type} in climate zone #{climate_zone}.")
2695
+ log_messages_to_runner(runner, debug = true)
2696
+ return false
2697
+ end
2698
+
2699
+ # address any adiabatic surfaces that don't have hard assigned constructions
2700
+ model.getSurfaces.each do |surface|
2701
+ next if surface.outsideBoundaryCondition != 'Adiabatic'
2702
+ next if surface.construction.is_initialized
2703
+ surface.setAdjacentSurface(surface)
2704
+ surface.setConstruction(surface.construction.get)
2705
+ surface.setOutsideBoundaryCondition('Adiabatic')
2706
+ end
2707
+
2708
+ # modify the infiltration rates
2709
+ if args['remove_objects']
2710
+ model.getSpaceInfiltrationDesignFlowRates.each(&:remove)
2711
+ end
2712
+ standard.model_apply_infiltration_standard(model)
2713
+ standard.model_modify_infiltration_coefficients(model, primary_bldg_type, climate_zone)
2714
+
2715
+ # set ground temperatures from DOE prototype buildings
2716
+ standard.model_add_ground_temperatures(model, primary_bldg_type, climate_zone)
2717
+
2718
+ end
2719
+
2720
+ # add elevators (returns ElectricEquipment object)
2721
+ if args['add_elevators']
2722
+
2723
+ # remove elevators as spaceLoads or exteriorLights
2724
+ model.getSpaceLoads.each do |instance|
2725
+ next if !instance.name.to_s.include?('Elevator') # most prototype building types model exterior elevators with name Elevator
2726
+ instance.remove
2727
+ end
2728
+ model.getExteriorLightss.each do |ext_light|
2729
+ next if !ext_light.name.to_s.include?('Fuel equipment') # some prototype building types model exterior elevators by this name
2730
+ ext_light.remove
2731
+ end
2732
+
2733
+ elevators = standard.model_add_elevators(model)
2734
+ if elevators.nil?
2735
+ runner.registerInfo('No elevators added to the building.')
2736
+ else
2737
+ elevator_def = elevators.electricEquipmentDefinition
2738
+ design_level = elevator_def.designLevel.get
2739
+ runner.registerInfo("Adding #{elevators.multiplier.round(1)} elevators each with power of #{OpenStudio.toNeatString(design_level, 0, true)} (W), plus lights and fans.")
2740
+ elevator_def.setFractionLost(1.0)
2741
+ elevator_def.setFractionRadiant(0.0)
2742
+ end
2743
+ end
2744
+
2745
+ # add exterior lights (returns a hash where key is lighting type and value is exteriorLights object)
2746
+ if args['add_exterior_lights']
2747
+
2748
+ if args['remove_objects']
2749
+ model.getExteriorLightss.each do |ext_light|
2750
+ next if ext_light.name.to_s.include?('Fuel equipment') # some prototype building types model exterior elevators by this name
2751
+ ext_light.remove
2752
+ end
2753
+ end
2754
+
2755
+ exterior_lights = standard.model_add_typical_exterior_lights(model, args['exterior_lighting_zone'].chars[0].to_i, args['onsite_parking_fraction'])
2756
+ exterior_lights.each do |k, v|
2757
+ runner.registerInfo("Adding Exterior Lights named #{v.exteriorLightsDefinition.name} with design level of #{v.exteriorLightsDefinition.designLevel} * #{OpenStudio.toNeatString(v.multiplier, 0, true)}.")
2758
+ end
2759
+ end
2760
+
2761
+ # add_exhaust
2762
+ if args['add_exhaust']
2763
+
2764
+ # remove exhaust objects
2765
+ if args['remove_objects']
2766
+ model.getFanZoneExhausts.each(&:remove)
2767
+ end
2768
+
2769
+ zone_exhaust_fans = standard.model_add_exhaust(model, args['kitchen_makeup']) # second argument is strategy for finding makeup zones for exhaust zones
2770
+ zone_exhaust_fans.each do |k, v|
2771
+ max_flow_rate_ip = OpenStudio.convert(k.maximumFlowRate.get, 'm^3/s', 'cfm').get
2772
+ if v.key?(:zone_mixing)
2773
+ zone_mixing = v[:zone_mixing]
2774
+ mixing_source_zone_name = zone_mixing.sourceZone.get.name
2775
+ mixing_design_flow_rate_ip = OpenStudio.convert(zone_mixing.designFlowRate.get, 'm^3/s', 'cfm').get
2776
+ runner.registerInfo("Adding #{OpenStudio.toNeatString(max_flow_rate_ip, 0, true)} (cfm) of exhaust to #{k.thermalZone.get.name}, with #{OpenStudio.toNeatString(mixing_design_flow_rate_ip, 0, true)} (cfm) of makeup air from #{mixing_source_zone_name}")
2777
+ else
2778
+ runner.registerInfo("Adding #{OpenStudio.toNeatString(max_flow_rate_ip, 0, true)} (cfm) of exhaust to #{k.thermalZone.get.name}")
2779
+ end
2780
+ end
2781
+ end
2782
+
2783
+ # add service water heating demand and supply
2784
+ if args['add_swh']
2785
+
2786
+ # remove water use equipment and water use connections
2787
+ if args['remove_objects']
2788
+ # TODO: - remove plant loops used for service water heating
2789
+ model.getWaterUseEquipments.each(&:remove)
2790
+ model.getWaterUseConnectionss.each(&:remove)
2791
+ end
2792
+
2793
+ # Infer the SWH type
2794
+ if args['swh_src'] == 'Inferred'
2795
+ if args['htg_src'] == 'NaturalGas' || args['htg_src'] == 'DistrictHeating'
2796
+ args['swh_src'] = 'NaturalGas' # If building has gas service, probably uses natural gas for SWH
2797
+ elsif args['htg_src'] == 'Electricity'
2798
+ args['swh_src'] == 'Electricity' # If building is doing space heating with electricity, probably used for SWH
2799
+ elsif args['htg_src'] == 'DistrictAmbient'
2800
+ args['swh_src'] == 'HeatPump' # If building has district ambient loop, it is fancy and probably uses HPs for SWH
2801
+ else
2802
+ args['swh_src'] = nil # Use inferences built into OpenStudio Standards for each building and space type
2803
+ end
2804
+ end
2805
+
2806
+ typical_swh = standard.model_add_typical_swh(model, water_heater_fuel: args['swh_src'])
2807
+ midrise_swh_loops = []
2808
+ stripmall_swh_loops = []
2809
+ typical_swh.each do |loop|
2810
+ if loop.name.get.include?('MidriseApartment')
2811
+ midrise_swh_loops << loop
2812
+ elsif loop.name.get.include?('RetailStripmall')
2813
+ stripmall_swh_loops << loop
2814
+ else
2815
+ water_use_connections = []
2816
+ loop.demandComponents.each do |component|
2817
+ next if !component.to_WaterUseConnections.is_initialized
2818
+ water_use_connections << component
2819
+ end
2820
+ runner.registerInfo("Adding #{loop.name} to the building. It has #{water_use_connections.size} water use connections.")
2821
+ end
2822
+ end
2823
+ if !midrise_swh_loops.empty?
2824
+ runner.registerInfo("Adding #{midrise_swh_loops.size} MidriseApartment service water heating loops.")
2825
+ end
2826
+ if !stripmall_swh_loops.empty?
2827
+ runner.registerInfo("Adding #{stripmall_swh_loops.size} RetailStripmall service water heating loops.")
2828
+ end
2829
+ end
2830
+
2831
+ # add_daylighting_controls (since outdated measure don't have this default to true if arg not found)
2832
+ if !args.has_key?('add_daylighting_controls')
2833
+ args['add_daylighting_controls'] = true
2834
+ end
2835
+ if args['add_daylighting_controls']
2836
+ # remove add_daylighting_controls objects
2837
+ if args['remove_objects']
2838
+ model.getDaylightingControls.each(&:remove)
2839
+ end
2840
+
2841
+ # add daylight controls, need to perform a sizing run for 2010
2842
+ if args['template'] == '90.1-2010'
2843
+ if standard.model_run_sizing_run(model, "#{Dir.pwd}/SRvt") == false
2844
+ log_messages_to_runner(runner, debug = true)
2845
+ return false
2846
+ end
2847
+ end
2848
+ standard.model_add_daylighting_controls(model)
2849
+ end
2850
+
2851
+ # add refrigeration
2852
+ if args['add_refrigeration']
2853
+
2854
+ # remove refrigeration equipment
2855
+ if args['remove_objects']
2856
+ model.getRefrigerationSystems.each(&:remove)
2857
+ end
2858
+
2859
+ # Add refrigerated cases and walkins
2860
+ standard.model_add_typical_refrigeration(model, primary_bldg_type)
2861
+ end
2862
+
2863
+ # add internal mass
2864
+ if args['add_internal_mass']
2865
+
2866
+ if args['remove_objects']
2867
+ model.getSpaceLoads.each do |instance|
2868
+ next unless instance.to_InternalMass.is_initialized
2869
+ instance.remove
2870
+ end
2871
+ end
2872
+
2873
+ # add internal mass to conditioned spaces; needs to happen after thermostats are applied
2874
+ standard.model_add_internal_mass(model, primary_bldg_type)
2875
+ end
2876
+
2877
+ # TODO: - add slab modeling and slab insulation
2878
+
2879
+ # TODO: - fuel customization for cooking and laundry
2880
+ # works by switching some fraction of electric loads to gas if requested (assuming base load is electric)
2881
+
2882
+ # add thermostats
2883
+ if args['add_thermostat']
2884
+
2885
+ # remove thermostats
2886
+ if args['remove_objects']
2887
+ model.getThermostatSetpointDualSetpoints.each(&:remove)
2888
+ end
2889
+
2890
+ model.getSpaceTypes.each do |space_type|
2891
+ # create thermostat schedules
2892
+ # skip un-recognized space types
2893
+ next if standard.space_type_get_standards_data(space_type).empty?
2894
+ # the last bool test it to make thermostat schedules. They are added to the model but not assigned
2895
+ standard.space_type_apply_internal_load_schedules(space_type, false, false, false, false, false, false, true)
2896
+
2897
+ # identify thermal thermostat and apply to zones (apply_internal_load_schedules names )
2898
+ model.getThermostatSetpointDualSetpoints.each do |thermostat|
2899
+ next if thermostat.name.to_s != "#{space_type.name} Thermostat"
2900
+ next if !thermostat.coolingSetpointTemperatureSchedule.is_initialized
2901
+ next if !thermostat.heatingSetpointTemperatureSchedule.is_initialized
2902
+ runner.registerInfo("Assigning #{thermostat.name} to thermal zones with #{space_type.name} assigned.")
2903
+ space_type.spaces.each do |space|
2904
+ next if !space.thermalZone.is_initialized
2905
+ space.thermalZone.get.setThermostatSetpointDualSetpoint(thermostat)
2906
+ end
2907
+ end
2908
+ end
2909
+ end
2910
+
2911
+ # add hvac system
2912
+ if args['add_hvac']
2913
+
2914
+ # remove HVAC objects
2915
+ if args['remove_objects']
2916
+ standard.model_remove_prm_hvac(model)
2917
+ end
2918
+
2919
+ case args['system_type']
2920
+ when 'Inferred'
2921
+
2922
+ # Get the hvac delivery type enum
2923
+ hvac_delivery = case args['hvac_delivery_type']
2924
+ when 'Forced Air'
2925
+ 'air'
2926
+ when 'Hydronic'
2927
+ 'hydronic'
2928
+ end
2929
+
2930
+ # Group the zones by occupancy type. Only split out non-dominant groups if their total area exceeds the limit.
2931
+ sys_groups = standard.model_group_zones_by_type(model, OpenStudio.convert(20_000, 'ft^2', 'm^2').get)
2932
+
2933
+ # For each group, infer the HVAC system type.
2934
+ sys_groups.each do |sys_group|
2935
+ # Infer the primary system type
2936
+ # runner.registerInfo("template = #{args['template']}, climate_zone = #{climate_zone}, occ_type = #{sys_group['type']}, hvac_delivery = #{hvac_delivery}, htg_src = #{args['htg_src']}, clg_src = #{args['clg_src']}, area_ft2 = #{sys_group['area_ft2']}, num_stories = #{sys_group['stories']}")
2937
+ sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel = standard.model_typical_hvac_system_type(model,
2938
+ climate_zone,
2939
+ sys_group['type'],
2940
+ hvac_delivery,
2941
+ args['htg_src'],
2942
+ args['clg_src'],
2943
+ OpenStudio.convert(sys_group['area_ft2'], 'ft^2', 'm^2').get,
2944
+ sys_group['stories'])
2945
+
2946
+ # Infer the secondary system type for multizone systems
2947
+ sec_sys_type = case sys_type
2948
+ when 'PVAV Reheat', 'VAV Reheat'
2949
+ 'PSZ-AC'
2950
+ when 'PVAV PFP Boxes', 'VAV PFP Boxes'
2951
+ 'PSZ-HP'
2952
+ else
2953
+ sys_type # same as primary system type
2954
+ end
2955
+
2956
+ # Group zones by story
2957
+ story_zone_lists = standard.model_group_zones_by_story(model, sys_group['zones'])
2958
+
2959
+ # On each story, add the primary system to the primary zones
2960
+ # and add the secondary system to any zones that are different.
2961
+ story_zone_lists.each do |story_group|
2962
+ # Differentiate primary and secondary zones, based on
2963
+ # operating hours and internal loads (same as 90.1 PRM)
2964
+ pri_sec_zone_lists = standard.model_differentiate_primary_secondary_thermal_zones(model, story_group)
2965
+ system_zones = pri_sec_zone_lists['primary']
2966
+
2967
+ # if the primary system type is PTAC, filter to cooled zones to prevent sizing error if no cooling
2968
+ if sys_type == 'PTAC'
2969
+ heated_and_cooled_zones = system_zones.select { |zone| standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2970
+ cooled_only_zones = system_zones.select { |zone| !standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2971
+ system_zones = heated_and_cooled_zones + cooled_only_zones
2972
+ end
2973
+
2974
+ # Add the primary system to the primary zones
2975
+ standard.model_add_hvac_system(model, sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel, system_zones)
2976
+
2977
+ # Add the secondary system to the secondary zones (if any)
2978
+ if !pri_sec_zone_lists['secondary'].empty?
2979
+ system_zones = pri_sec_zone_lists['secondary']
2980
+ if (sec_sys_type == 'PTAC') || (sec_sys_type == 'PSZ-AC')
2981
+ heated_and_cooled_zones = system_zones.select { |zone| standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2982
+ cooled_only_zones = system_zones.select { |zone| !standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2983
+ system_zones = heated_and_cooled_zones + cooled_only_zones
2984
+ end
2985
+ standard.model_add_hvac_system(model, sec_sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel, system_zones)
2986
+ end
2987
+ end
2988
+ end
2989
+
2990
+ else # HVAC system_type specified
2991
+
2992
+ # Group the zones by occupancy type. Only split out non-dominant groups if their total area exceeds the limit.
2993
+ sys_groups = standard.model_group_zones_by_type(model, OpenStudio.convert(20_000, 'ft^2', 'm^2').get)
2994
+ sys_groups.each do |sys_group|
2995
+ # Group the zones by story
2996
+ story_groups = standard.model_group_zones_by_story(model, sys_group['zones'])
2997
+
2998
+ # Add the user specified HVAC system for each story.
2999
+ # Single-zone systems will get one per zone.
3000
+ story_groups.each do |zones|
3001
+ model.add_cbecs_hvac_system(standard, args['system_type'], zones)
3002
+ end
3003
+ end
3004
+ end
3005
+ end
3006
+
3007
+ # hours of operation
3008
+ if args['modify_wkdy_op_hrs'] || args['modify_wknd_op_hrs']
3009
+ # Infer the current hours of operation schedule for the building
3010
+ op_sch = standard.model_infer_hours_of_operation_building(model)
3011
+
3012
+ # Convert existing schedules in the model to parametric schedules based on current hours of operation
3013
+ standard.model_setup_parametric_schedules(model)
3014
+
3015
+ # Create start and end times from start time and duration supplied
3016
+ wkdy_start_time = nil
3017
+ wkdy_end_time = nil
3018
+ wknd_start_time = nil
3019
+ wknd_end_time = nil
3020
+ # weekdays
3021
+ if args['modify_wkdy_op_hrs']
3022
+ wkdy_start_time = OpenStudio::Time.new(0, wkdy_op_hrs_start_time_hr, wkdy_op_hrs_start_time_min, 0)
3023
+ wkdy_end_time = wkdy_start_time + OpenStudio::Time.new(0, wkdy_op_hrs_duration_hr, wkdy_op_hrs_duration_min, 0)
3024
+ end
3025
+ # weekends
3026
+ if args['modify_wknd_op_hrs']
3027
+ wknd_start_time = OpenStudio::Time.new(0, wknd_op_hrs_start_time_hr, wknd_op_hrs_start_time_min, 0)
3028
+ wknd_end_time = wknd_start_time + OpenStudio::Time.new(0, wknd_op_hrs_duration_hr, wknd_op_hrs_duration_min, 0)
3029
+ end
3030
+
3031
+ # Modify hours of operation, using weekdays values for all weekdays and weekend values for Saturday and Sunday
3032
+ standard.schedule_ruleset_set_hours_of_operation(op_sch,
3033
+ wkdy_start_time: wkdy_start_time,
3034
+ wkdy_end_time: wkdy_end_time,
3035
+ sat_start_time: wknd_start_time,
3036
+ sat_end_time: wknd_end_time,
3037
+ sun_start_time: wknd_start_time,
3038
+ sun_end_time: wknd_end_time)
3039
+
3040
+ # Apply new operating hours to parametric schedules to make schedules in model reflect modified hours of operation
3041
+ parametric_schedules = standard.model_apply_parametric_schedules(model, error_on_out_of_order: false)
3042
+ runner.registerInfo("Updated #{parametric_schedules.size} schedules with new hours of operation.")
3043
+ end
3044
+
3045
+ # set hvac controls and efficiencies (this should be last model articulation element)
3046
+ if args['add_hvac']
3047
+ # set additional properties for building
3048
+ props = model.getBuilding.additionalProperties
3049
+ props.setFeature('hvac_system_type',"#{args['system_type']}")
3050
+
3051
+ case args['system_type']
3052
+ when 'Ideal Air Loads'
3053
+
3054
+ else
3055
+ # Set the heating and cooling sizing parameters
3056
+ standard.model_apply_prm_sizing_parameters(model)
3057
+
3058
+ # Perform a sizing run
3059
+ if standard.model_run_sizing_run(model, "#{Dir.pwd}/SR1") == false
3060
+ log_messages_to_runner(runner, debug = true)
3061
+ return false
3062
+ end
3063
+
3064
+ # If there are any multizone systems, reset damper positions
3065
+ # to achieve a 60% ventilation effectiveness minimum for the system
3066
+ # following the ventilation rate procedure from 62.1
3067
+ standard.model_apply_multizone_vav_outdoor_air_sizing(model)
3068
+
3069
+ # Apply the prototype HVAC assumptions
3070
+ standard.model_apply_prototype_hvac_assumptions(model, primary_bldg_type, climate_zone)
3071
+
3072
+ # Apply the HVAC efficiency standard
3073
+ standard.model_apply_hvac_efficiency_standard(model, climate_zone)
3074
+ end
3075
+ end
3076
+
3077
+ # set unmet hours tolerance
3078
+ unmet_hrs_tol_r = args['unmet_hours_tolerance']
3079
+ unmet_hrs_tol_k = OpenStudio.convert(unmet_hrs_tol_r, 'R', 'K').get
3080
+ tolerances = model.getOutputControlReportingTolerances
3081
+ tolerances.setToleranceforTimeHeatingSetpointNotMet(unmet_hrs_tol_k)
3082
+ tolerances.setToleranceforTimeCoolingSetpointNotMet(unmet_hrs_tol_k)
3083
+
3084
+ # remove everything but spaces, zones, and stub space types (extend as needed for additional objects, may make bool arg for this)
3085
+ if args['remove_objects']
3086
+ model.purgeUnusedResourceObjects
3087
+ objects_after_cleanup = initial_objects - model.getModelObjects.size
3088
+ if objects_after_cleanup > 0
3089
+ runner.registerInfo("Removing #{objects_after_cleanup} objects from model")
3090
+ end
3091
+ end
3092
+
3093
+ # report final condition of model
3094
+ runner.registerFinalCondition("The building finished with #{model.getModelObjects.size} objects.")
3095
+
3096
+ # log messages to info messages
3097
+ log_messages_to_runner(runner, debug = false)
3098
+
3099
+ return true
3100
+ end
3101
+
3102
+ # wizard
3103
+ # used for varieties of measures that create space type and construction set wizard
3104
+ def wizard(model, runner, user_arguments)
3105
+
3106
+ # use the built-in error checking
3107
+ if !runner.validateUserArguments(arguments(model), user_arguments)
3108
+ return false
3109
+ end
3110
+
3111
+ # assign the user inputs to variables
3112
+ building_type = runner.getStringArgumentValue('building_type', user_arguments)
3113
+ template = runner.getStringArgumentValue('template', user_arguments)
3114
+ climate_zone = runner.getStringArgumentValue('climate_zone', user_arguments)
3115
+ create_space_types = runner.getBoolArgumentValue('create_space_types', user_arguments)
3116
+ create_construction_set = runner.getBoolArgumentValue('create_construction_set', user_arguments)
3117
+ set_building_defaults = runner.getBoolArgumentValue('set_building_defaults', user_arguments)
3118
+
3119
+ # reporting initial condition of model
3120
+ starting_spaceTypes = model.getSpaceTypes
3121
+ starting_constructionSets = model.getDefaultConstructionSets
3122
+ runner.registerInitialCondition("The building started with #{starting_spaceTypes.size} space types and #{starting_constructionSets.size} construction sets.")
3123
+
3124
+ # lookup space types for specified building type (false indicates not to use whole building type only)
3125
+ space_type_hash = get_space_types_from_building_type(building_type, template, false)
3126
+ if space_type_hash == false
3127
+ runner.registerError("#{building_type} is an unexpected building type.")
3128
+ return false
3129
+ end
3130
+
3131
+ # create space_type_map from array
3132
+ space_type_map = {}
3133
+ default_space_type_name = nil
3134
+ space_type_hash.each do |space_type_name, hash|
3135
+ next if hash[:space_type_gen] == false # space types like undeveloped and basement are skipped.
3136
+ space_type_map[space_type_name] = [] # no spaces to pass in
3137
+ if hash[:default]
3138
+ default_space_type_name = space_type_name
3139
+ end
3140
+ end
3141
+
3142
+ # Make the standard applier
3143
+ standard = Standard.build(template)
3144
+
3145
+ # mapping building_type name is needed for a few methods
3146
+ lookup_building_type = standard.model_get_lookup_name(building_type)
3147
+
3148
+ # remap small medium and large office to office
3149
+ if building_type.include?("Office") then building_type = "Office" end
3150
+
3151
+ # get array of new space types
3152
+ space_types_new = []
3153
+
3154
+ # create_space_types
3155
+ if create_space_types
3156
+
3157
+ # array of starting space types
3158
+ space_types_starting = model.getSpaceTypes
3159
+
3160
+ # create stub space types
3161
+ space_type_hash.each do |space_type_name, hash|
3162
+ next if hash[:space_type_gen] == false # space types like undeveloped and basement are skipped.
3163
+
3164
+ # create space type
3165
+ space_type = OpenStudio::Model::SpaceType.new(model)
3166
+ space_type.setStandardsBuildingType(building_type)
3167
+ space_type.setStandardsSpaceType(space_type_name)
3168
+ space_type.setName("#{building_type} #{space_type_name}")
3169
+
3170
+ # add to array of new space types
3171
+ space_types_new << space_type
3172
+
3173
+ # add internal loads (the nil check isn't ncessary, but I will keep it in as a warning instad of an error)
3174
+ test = standard.space_type_apply_internal_loads(space_type, true, true, true, true, true, true)
3175
+ if test.nil?
3176
+ runner.registerWarning("Could not add loads for #{space_type.name}. Not expected for #{template} #{lookup_building_type}")
3177
+ end
3178
+
3179
+ # the last bool test it to make thermostat schedules. They are added to the model but not assigned
3180
+ standard.space_type_apply_internal_load_schedules(space_type, true, true, true, true, true, true, true)
3181
+
3182
+ # assign colors
3183
+ standard.space_type_apply_rendering_color(space_type)
3184
+
3185
+ # exend space type name to include the template. Consider this as well for load defs
3186
+ space_type.setName("#{space_type.name} - #{template}")
3187
+ runner.registerInfo("Added space type named #{space_type.name}")
3188
+ end
3189
+
3190
+ end
3191
+
3192
+ # add construction sets
3193
+ bldg_def_const_set = nil
3194
+ if create_construction_set
3195
+
3196
+ # Make the default construction set for the building
3197
+ is_residential = 'No' # default is nonresidential for building level
3198
+ bldg_def_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, is_residential)
3199
+ if bldg_def_const_set.is_initialized
3200
+ bldg_def_const_set = bldg_def_const_set.get
3201
+ runner.registerInfo("Added default construction set named #{bldg_def_const_set.name}")
3202
+ else
3203
+ runner.registerError('Could not create default construction set for the building.')
3204
+ return false
3205
+ end
3206
+
3207
+ # make residential construction set as unused resource
3208
+ if ['SmallHotel', 'LargeHotel', 'MidriseApartment', 'HighriseApartment'].include?(building_type)
3209
+ res_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, 'Yes')
3210
+ if res_const_set.is_initialized
3211
+ res_const_set = res_const_set.get
3212
+ res_const_set.setName("#{bldg_def_const_set.name} - Residential ")
3213
+ runner.registerInfo("Added residential construction set named #{res_const_set.name}")
3214
+ else
3215
+ runner.registerError('Could not create residential construction set for the building.')
3216
+ return false
3217
+ end
3218
+ end
3219
+
3220
+ end
3221
+
3222
+ # set_building_defaults
3223
+ if set_building_defaults
3224
+
3225
+ # identify default space type
3226
+ space_type_standards_info_hash = OsLib_HelperMethods.getSpaceTypeStandardsInformation(space_types_new)
3227
+ default_space_type = nil
3228
+ space_type_standards_info_hash.each do |space_type, standards_array|
3229
+ standards_space_type = standards_array[1]
3230
+ if default_space_type_name == standards_space_type
3231
+ default_space_type = space_type
3232
+ end
3233
+ end
3234
+
3235
+ # set default space type
3236
+ building = model.getBuilding
3237
+ if !default_space_type.nil?
3238
+ building.setSpaceType(default_space_type)
3239
+ runner.registerInfo("Setting default Space Type for building to #{building.spaceType.get.name}")
3240
+ end
3241
+
3242
+ # default construction
3243
+ if !bldg_def_const_set.nil?
3244
+ building.setDefaultConstructionSet(bldg_def_const_set)
3245
+ runner.registerInfo("Setting default Construction Set for building to #{building.defaultConstructionSet.get.name}")
3246
+ end
3247
+
3248
+ # set climate zone
3249
+ os_climate_zone = climate_zone.gsub('ASHRAE 169-2013-', '')
3250
+ # trim off letter from climate zone 7 or 8
3251
+ if (os_climate_zone[0] == '7') || (os_climate_zone[0] == '8')
3252
+ os_climate_zone = os_climate_zone[0]
3253
+ end
3254
+ climate_zone = model.getClimateZones.setClimateZone('ASHRAE', os_climate_zone)
3255
+ runner.registerInfo("Setting #{climate_zone.institution} Climate Zone to #{climate_zone.value}")
3256
+
3257
+ # set building type
3258
+ # use lookup_building_type so spaces like MediumOffice will map to Office (Supports baseline automation)
3259
+ building.setStandardsBuildingType(lookup_building_type)
3260
+ runner.registerInfo("Setting Standards Building Type to #{building.standardsBuildingType}")
3261
+
3262
+ # rename building if it is named "Building 1"
3263
+ if model.getBuilding.name.to_s == 'Building 1'
3264
+ model.getBuilding.setName("#{building_type} #{template} #{os_climate_zone}")
3265
+ runner.registerInfo("Renaming building to #{model.getBuilding.name}")
3266
+ end
3267
+
3268
+ end
3269
+
3270
+ # reporting final condition of model
3271
+ finishing_spaceTypes = model.getSpaceTypes
3272
+ finishing_constructionSets = model.getDefaultConstructionSets
3273
+ runner.registerFinalCondition("The building finished with #{finishing_spaceTypes.size} space types and #{finishing_constructionSets.size} construction sets.")
3274
+
3275
+ return true
3276
+ end
3277
+
817
3278
  end