openstudio-extension 0.2.0 → 0.2.1

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