openstudio-extension 0.1.5 → 0.1.6

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,15 +591,15 @@ 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 }
@@ -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,1890 @@ 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
+ puts "asdf, doesn't have it I'm adding it"
1645
+ else
1646
+ puts "asdf, already had it"
1647
+ end
1648
+
1649
+ # lookup and replace argument values from upstream measures
1650
+ if args['use_upstream_args'] == true
1651
+ args.each do |arg, value|
1652
+ next if arg == 'use_upstream_args' # this argument should not be changed
1653
+ value_from_osw = OsLib_HelperMethods.check_upstream_measure_for_arg(runner, arg)
1654
+ if !value_from_osw.empty?
1655
+ runner.registerInfo("Replacing argument named #{arg} from current measure with a value of #{value_from_osw[:value]} from #{value_from_osw[:measure_name]}.")
1656
+ new_val = value_from_osw[:value]
1657
+ # TODO: - make code to handle non strings more robust. check_upstream_measure_for_arg could pass back the argument type
1658
+ if arg == 'total_bldg_floor_area'
1659
+ args[arg] = new_val.to_f
1660
+ elsif arg == 'num_stories_above_grade'
1661
+ args[arg] = new_val.to_f
1662
+ elsif arg == 'zipcode'
1663
+ args[arg] = new_val.to_i
1664
+ else
1665
+ args[arg] = new_val
1666
+ end
1667
+ end
1668
+ end
1669
+ end
1670
+
1671
+ # check expected values of double arguments
1672
+ fraction_args = ['bldg_type_b_fract_bldg_area',
1673
+ 'bldg_type_c_fract_bldg_area',
1674
+ 'bldg_type_d_fract_bldg_area',
1675
+ 'wwr', 'party_wall_fraction']
1676
+ 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)
1677
+
1678
+ positive_args = ['total_bldg_floor_area']
1679
+ positive = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 0.0, 'max' => nil, 'min_eq_bool' => false, 'max_eq_bool' => false, 'arg_array' => positive_args)
1680
+
1681
+ one_or_greater_args = ['num_stories_above_grade']
1682
+ 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)
1683
+
1684
+ non_neg_args = ['num_stories_below_grade',
1685
+ 'floor_height',
1686
+ 'ns_to_ew_ratio',
1687
+ 'party_wall_stories_north',
1688
+ 'party_wall_stories_south',
1689
+ 'party_wall_stories_east',
1690
+ 'party_wall_stories_west',
1691
+ 'single_floor_area',
1692
+ 'bar_width',]
1693
+ 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)
1694
+
1695
+ # return false if any errors fail
1696
+ if !fraction then return false end
1697
+ if !positive then return false end
1698
+ if !one_or_greater then return false end
1699
+ if !non_neg then return false end
1700
+
1701
+ # if aspect ratio, story height or wwr have argument value of 0 then use smart building type defaults
1702
+ building_form_defaults = building_form_defaults(args['bldg_type_a'])
1703
+
1704
+ # store list of defaulted items
1705
+ defaulted_args = []
1706
+
1707
+ if args['ns_to_ew_ratio'] == 0.0
1708
+ args['ns_to_ew_ratio'] = building_form_defaults[:aspect_ratio]
1709
+ 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]}.")
1710
+ end
1711
+
1712
+ if args['perim_mult'] == 0.0
1713
+ # if this is not defined then use default of 1.0
1714
+ if !building_form_defaults.has_key?(:perim_mult)
1715
+ args['perim_mult'] = 1.0
1716
+ else
1717
+ args['perim_mult'] = building_form_defaults[:perim_mult]
1718
+ end
1719
+ 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]}.")
1720
+ elsif args['perim_mult'] < 1.0
1721
+ runner.registerError("Other than the smart default value of 0, the minimum perimeter multiplier should be equal to 1.0 or greater.")
1722
+ return false
1723
+ end
1724
+
1725
+ if args['floor_height'] == 0.0
1726
+ args['floor_height'] = building_form_defaults[:typical_story]
1727
+ 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]}.")
1728
+ defaulted_args << 'floor_height'
1729
+ end
1730
+ # 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
1731
+ if args['wwr'] == 0.0
1732
+ args['wwr'] = building_form_defaults[:wwr]
1733
+ 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]}.")
1734
+ end
1735
+
1736
+ # check that sum of fractions for b,c, and d is less than 1.0 (so something is left for primary building type)
1737
+ 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']
1738
+ if bldg_type_a_fract_bldg_area <= 0.0
1739
+ 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.')
1740
+ return false
1741
+ end
1742
+
1743
+ # Make the standard applier
1744
+ standard = Standard.build("#{args['template']}")
1745
+
1746
+ # report initial condition of model
1747
+ runner.registerInitialCondition("The building started with #{model.getSpaces.size} spaces.")
1748
+
1749
+ # determine of ns_ew needs to be mirrored
1750
+ mirror_ns_ew = false
1751
+ rotation = model.getBuilding.northAxis
1752
+ if rotation > 45.0 && rotation < 135.0
1753
+ mirror_ns_ew = true
1754
+ elsif rotation > 45.0 && rotation < 135.0
1755
+ mirror_ns_ew = true
1756
+ end
1757
+
1758
+ # remove non-resource objects not removed by removing the building
1759
+ remove_non_resource_objects(runner, model)
1760
+
1761
+ # rename building to infer template in downstream measure
1762
+ name_array = [args['template'], args['bldg_type_a']]
1763
+ if args['bldg_type_b_fract_bldg_area'] > 0 then name_array << args['bldg_type_b'] end
1764
+ if args['bldg_type_c_fract_bldg_area'] > 0 then name_array << args['bldg_type_c'] end
1765
+ if args['bldg_type_d_fract_bldg_area'] > 0 then name_array << args['bldg_type_d'] end
1766
+ model.getBuilding.setName(name_array.join('|').to_s)
1767
+
1768
+ # hash to whole building type data
1769
+ building_type_hash = {}
1770
+
1771
+ # gather data for bldg_type_a
1772
+ building_type_hash[args['bldg_type_a']] = {}
1773
+ building_type_hash[args['bldg_type_a']][:frac_bldg_area] = bldg_type_a_fract_bldg_area
1774
+ #building_type_hash[args['bldg_type_a']][:num_units] = args['bldg_type_a_num_units']
1775
+ building_type_hash[args['bldg_type_a']][:space_types] = get_space_types_from_building_type(args['bldg_type_a'], args['template'], true)
1776
+
1777
+ # gather data for bldg_type_b
1778
+ if args['bldg_type_b_fract_bldg_area'] > 0
1779
+ building_type_hash[args['bldg_type_b']] = {}
1780
+ building_type_hash[args['bldg_type_b']][:frac_bldg_area] = args['bldg_type_b_fract_bldg_area']
1781
+ #building_type_hash[args['bldg_type_b']][:num_units] = args['bldg_type_b_num_units']
1782
+ building_type_hash[args['bldg_type_b']][:space_types] = get_space_types_from_building_type(args['bldg_type_b'], args['template'], true)
1783
+ end
1784
+
1785
+ # gather data for bldg_type_c
1786
+ if args['bldg_type_c_fract_bldg_area'] > 0
1787
+ building_type_hash[args['bldg_type_c']] = {}
1788
+ building_type_hash[args['bldg_type_c']][:frac_bldg_area] = args['bldg_type_c_fract_bldg_area']
1789
+ #building_type_hash[args['bldg_type_c']][:num_units] = args['bldg_type_c_num_units']
1790
+ building_type_hash[args['bldg_type_c']][:space_types] = get_space_types_from_building_type(args['bldg_type_c'], args['template'], true)
1791
+ end
1792
+
1793
+ # gather data for bldg_type_d
1794
+ if args['bldg_type_d_fract_bldg_area'] > 0
1795
+ building_type_hash[args['bldg_type_d']] = {}
1796
+ building_type_hash[args['bldg_type_d']][:frac_bldg_area] = args['bldg_type_d_fract_bldg_area']
1797
+ #building_type_hash[args['bldg_type_d']][:num_units] = args['bldg_type_d_num_units']
1798
+ building_type_hash[args['bldg_type_d']][:space_types] = get_space_types_from_building_type(args['bldg_type_d'], args['template'], true)
1799
+ end
1800
+
1801
+ # creating space types for requested building types
1802
+ building_type_hash.each do |building_type, building_type_hash|
1803
+ runner.registerInfo("Creating Space Types for #{building_type}.")
1804
+
1805
+ # mapping building_type name is needed for a few methods
1806
+ building_type = standard.model_get_lookup_name(building_type)
1807
+
1808
+ # create space_type_map from array
1809
+ sum_of_ratios = 0.0
1810
+ building_type_hash[:space_types] = building_type_hash[:space_types].sort_by { |k, v| v[:ratio] }.to_h
1811
+ building_type_hash[:space_types].each do |space_type_name, hash|
1812
+ next if hash[:space_type_gen] == false # space types like undeveloped and basement are skipped.
1813
+
1814
+ # create space type
1815
+ space_type = OpenStudio::Model::SpaceType.new(model)
1816
+ space_type.setStandardsBuildingType(building_type)
1817
+ space_type.setStandardsSpaceType(space_type_name)
1818
+ space_type.setName("#{building_type} #{space_type_name}")
1819
+
1820
+ # set color
1821
+ test = standard.space_type_apply_rendering_color(space_type) # this uses openstudio-standards
1822
+ if !test
1823
+ # todo - once fixed in standards un-comment this
1824
+ #runner.registerWarning("Could not find color for #{args['template']} #{space_type.name}")
1825
+ end
1826
+
1827
+ # extend hash to hold new space type object
1828
+ hash[:space_type] = space_type
1829
+
1830
+ # add to sum_of_ratios counter for adjustment multiplier
1831
+ sum_of_ratios += hash[:ratio]
1832
+ end
1833
+
1834
+ # store multiplier needed to adjust sum of ratios to equal 1.0
1835
+ building_type_hash[:ratio_adjustment_multiplier] = 1.0 / sum_of_ratios
1836
+ end
1837
+
1838
+ # calculate length and with of bar
1839
+ total_bldg_floor_area_si = OpenStudio.convert(args['total_bldg_floor_area'], 'ft^2', 'm^2').get
1840
+ single_floor_area_si = OpenStudio.convert(args['single_floor_area'], 'ft^2', 'm^2').get
1841
+
1842
+ # store number of stories
1843
+ num_stories = args['num_stories_below_grade'] + args['num_stories_above_grade']
1844
+
1845
+ # handle user-assigned single floor plate size condition
1846
+ if args['single_floor_area'] > 0.0
1847
+ footprint_si = single_floor_area_si
1848
+ total_bldg_floor_area_si = footprint_si * num_stories.to_f
1849
+ runner.registerWarning('User-defined single floor area was used for calculation of total building floor area')
1850
+ # add warning if custom_height_bar is true and applicable building type is selected
1851
+ if args['custom_height_bar']
1852
+ runner.registerWarning("Cannot use custom height bar with single floor area method, will not create custom height bar.")
1853
+ args['custom_height_bar'] = false
1854
+ end
1855
+ else
1856
+ footprint_si = nil
1857
+ end
1858
+
1859
+ # populate space_types_hash
1860
+ space_types_hash = {}
1861
+ multi_height_space_types_hash = {}
1862
+ custom_story_heights = []
1863
+ if args['space_type_sort_logic'] == 'Building Type > Size'
1864
+ building_type_hash = building_type_hash.sort_by { |k, v| v[:frac_bldg_area] }
1865
+ end
1866
+ building_type_hash.each do |building_type, building_type_hash|
1867
+
1868
+ if args["double_loaded_corridor"] == "Primary Space Type"
1869
+
1870
+ # 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
1871
+ default_st = nil
1872
+ circ_st = nil
1873
+ building_type_hash[:space_types].each do |space_type_name, hash|
1874
+ if hash[:default] then default_st = space_type_name end
1875
+ if hash[:circ] then circ_st = space_type_name end
1876
+ end
1877
+
1878
+ # update building hash
1879
+ if !default_st.nil? && !circ_st.nil?
1880
+ runner.registerInfo("Combining #{default_st} and #{circ_st} into a group representing a double loaded corridor")
1881
+
1882
+ # add new item
1883
+ building_type_hash[:space_types]["Double Loaded Corridor"] = {}
1884
+ double_loaded_st = building_type_hash[:space_types]["Double Loaded Corridor"]
1885
+ double_loaded_st[:ratio] = building_type_hash[:space_types][default_st][:ratio] + building_type_hash[:space_types][circ_st][:ratio]
1886
+ double_loaded_st[:double_loaded_corridor] = true
1887
+ double_loaded_st[:space_type] = model.getBuilding
1888
+ double_loaded_st[:children] = {}
1889
+ 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]
1890
+ 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]
1891
+ building_type_hash[:space_types][default_st][:name] = default_st
1892
+ building_type_hash[:space_types][circ_st][:name] = circ_st
1893
+ double_loaded_st[:children][:default] = building_type_hash[:space_types][default_st]
1894
+ double_loaded_st[:children][:circ] = building_type_hash[:space_types][circ_st]
1895
+ double_loaded_st[:orig_ratio] = 0.0
1896
+
1897
+ # zero out ratios from old item (don't delete because I still want the space types made)
1898
+ building_type_hash[:space_types][default_st][:ratio] = 0.0
1899
+ building_type_hash[:space_types][circ_st][:ratio] = 0.0
1900
+ end
1901
+ end
1902
+
1903
+ building_type_hash[:space_types].each do |space_type_name, hash|
1904
+ next if hash[:space_type_gen] == false
1905
+
1906
+ space_type = hash[:space_type]
1907
+ ratio_of_bldg_total = hash[:ratio] * building_type_hash[:ratio_adjustment_multiplier] * building_type_hash[:frac_bldg_area]
1908
+ 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
1909
+
1910
+ # only add custom height space if 0 is used for floor_height
1911
+ if defaulted_args.include?('floor_height') && hash.key?(:story_height) && args['custom_height_bar']
1912
+ multi_height_space_types_hash[space_type] = { floor_area: final_floor_area, space_type: space_type, story_height: hash[:story_height] }
1913
+ if hash.key?(:orig_ratio) then multi_height_space_types_hash[space_type][:orig_ratio] = hash[:orig_ratio] end
1914
+ custom_story_heights << hash[:story_height]
1915
+ if args['wwr'] == 0 && hash.key?(:wwr)
1916
+ multi_height_space_types_hash[space_type][:wwr] = hash[:wwr]
1917
+ end
1918
+ else
1919
+ # only add wwr if 0 used for wwr arg and if space type has wwr as key
1920
+ space_types_hash[space_type] = { floor_area: final_floor_area, space_type: space_type }
1921
+ if hash.key?(:orig_ratio) then space_types_hash[space_type][:orig_ratio] = hash[:orig_ratio] end
1922
+ if args['wwr'] == 0 && hash.key?(:wwr)
1923
+ space_types_hash[space_type][:wwr] = hash[:wwr]
1924
+ end
1925
+ if hash[:double_loaded_corridor]
1926
+ space_types_hash[space_type][:children] = hash[:children]
1927
+ end
1928
+ end
1929
+ end
1930
+ end
1931
+
1932
+ # resort if not sorted by building type
1933
+ if args['space_type_sort_logic'] == "Size"
1934
+ # added code to convert to hash. I use sort_by 3 other times, but those seem to be working fine as is now.
1935
+ space_types_hash = Hash[space_types_hash.sort_by { |k, v| v[:floor_area] }]
1936
+ end
1937
+
1938
+ # calculate targets for testing
1939
+ target_areas = {} # used for checks
1940
+ target_areas_cust_height = 0.0
1941
+ space_types_hash.each do |k,v|
1942
+ if v.key?(:orig_ratio)
1943
+ target_areas[k] = v[:orig_ratio] * total_bldg_floor_area_si
1944
+ else
1945
+ target_areas[k] = v[:floor_area]
1946
+ end
1947
+ end
1948
+ multi_height_space_types_hash.each do |k,v|
1949
+ if v.key?(:orig_ratio)
1950
+ target_areas[k] = v[:orig_ratio] * total_bldg_floor_area_si
1951
+ target_areas_cust_height += v[:orig_ratio] * total_bldg_floor_area_si
1952
+ else
1953
+ target_areas[k] = v[:floor_area]
1954
+ target_areas_cust_height += v[:floor_area]
1955
+ end
1956
+ end
1957
+
1958
+ # gather inputs
1959
+ if footprint_si.nil?
1960
+ footprint_si = (total_bldg_floor_area_si - target_areas_cust_height) / num_stories.to_f
1961
+ end
1962
+ floor_height_si = OpenStudio.convert(args['floor_height'], 'ft', 'm').get
1963
+ min_allow_size = OpenStudio.convert(15.0,'ft','m').get
1964
+ specified_bar_width_si = OpenStudio.convert(args['bar_width'],'ft','m').get
1965
+
1966
+ # set custom width
1967
+ if specified_bar_width_si > 0
1968
+ runner.registerInfo("Ignoring perimeter multiplier argument when non zero width argument is used")
1969
+ if footprint_si / specified_bar_width_si >= min_allow_size
1970
+ width = specified_bar_width_si
1971
+ length = footprint_si / width
1972
+ else
1973
+ length = min_allow_size
1974
+ width = footprint_si / length
1975
+ runner.registerWarning("User specified width results in a length that is too short, adjusting width to be narrower than specified.")
1976
+ end
1977
+ width_cust_height = specified_bar_width_si
1978
+ else
1979
+ width = Math.sqrt(footprint_si / args['ns_to_ew_ratio'])
1980
+ length = footprint_si / width
1981
+ width_cust_height = Math.sqrt(target_areas_cust_height / args['ns_to_ew_ratio'])
1982
+ end
1983
+ length_cust_height = target_areas_cust_height / width_cust_height
1984
+ if args['perim_mult'] > 1.0 && target_areas_cust_height > 0.0
1985
+ # todo - update tests that hit this warning
1986
+ runner.registerWarning("Ignoring perimeter multiplier for bar that represents custom height spaces.")
1987
+ end
1988
+
1989
+ # check if dual bar is needed
1990
+ dual_bar = false
1991
+ if specified_bar_width_si > 0.0 && args['bar_division_method'] == 'Multiple Space Types - Individual Stories Sliced'
1992
+ if length/width != args['ns_to_ew_ratio']
1993
+
1994
+ if 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']}, Lowering it to #{length/width} ")
1996
+ args['ns_to_ew_ratio'] = length/width
1997
+ elsif args['ns_to_ew_ratio'] < 1.0 && args['ns_to_ew_ratio'] > length/width
1998
+ runner.registerWarning("Can't meet target aspect ratio of #{args['ns_to_ew_ratio']}, Increasing it to #{length/width} ")
1999
+ args['ns_to_ew_ratio'] = length/width
2000
+ else
2001
+ # check if each bar would be longer then 15 feet, then set as dual bar and override perimeter multiplier
2002
+ 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'])
2003
+ length_alt2 = length - length_alt1
2004
+ if [length_alt1,length_alt2].min >= min_allow_size
2005
+ dual_bar = true
2006
+ else
2007
+ runner.registerInfo("Second bar would be below minimum length, will model as single bar")
2008
+ # swap length and width if single bar and aspect ratio less than 1
2009
+ if args['ns_to_ew_ratio'] < 1.0
2010
+ width = length
2011
+ length = specified_bar_width_si
2012
+ end
2013
+ end
2014
+ end
2015
+ end
2016
+ elsif args['perim_mult'] > 1.0 && args['bar_division_method'] == 'Multiple Space Types - Individual Stories Sliced'
2017
+ 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.")
2018
+ dual_bar = true
2019
+ elsif args['perim_mult'] > 1.0
2020
+ 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")
2021
+ end
2022
+
2023
+ # calculations for dual bar, which later will be setup to run create_bar twice
2024
+ if dual_bar
2025
+ min_perim = 2 * width + 2 * length
2026
+ target_area = footprint_si
2027
+ target_perim = min_perim * args['perim_mult']
2028
+ tol_testing = 0.00001
2029
+ dual_bar_calc_approach = nil # stretched, adiabatic_ends_bar_b, dual_bar
2030
+ 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.")
2031
+ runner.registerInfo("Target dual bar perimeter is #{OpenStudio.toNeatString(OpenStudio.convert(target_perim, 'm', 'ft').get, 0, true)} ft.")
2032
+
2033
+ # determine which of the three paths to hit target perimeter multiplier are possible
2034
+ # A use dual bar non adiabatic
2035
+ # B use dual bar adiabatic
2036
+ # C use stretched bar (requires model to miss ns/ew ratio)
2037
+
2038
+ # custom quadratic equation to solve two bars with common width 2l^2 - p*l + 4a = 0
2039
+ if target_perim**2 - 32 * footprint_si > 0
2040
+ if specified_bar_width_si > 0
2041
+ runner.registerInfo("Ignoring perimeter multiplier argument and using use specified bar width.")
2042
+ dual_double_end_width = specified_bar_width_si
2043
+ dual_double_end_length = footprint_si / dual_double_end_width
2044
+ else
2045
+ dual_double_end_length = 0.25 * (target_perim + Math.sqrt(target_perim**2 - 32 * footprint_si))
2046
+ dual_double_end_width = footprint_si / dual_double_end_length
2047
+ end
2048
+
2049
+ # now that stretched bar is made, determine where to split it and rotate
2050
+ 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'])
2051
+ bar_b_length = dual_double_end_length - bar_a_length
2052
+ area_a = bar_a_length * dual_double_end_width
2053
+ area_b = bar_b_length * dual_double_end_width
2054
+ else
2055
+ # this will throw it to adiabatic ends test
2056
+ bar_a_length = 0
2057
+ bar_b_length = 0
2058
+ end
2059
+
2060
+ if bar_a_length >= min_allow_size && bar_b_length >= min_allow_size
2061
+ dual_bar_calc_approach = 'dual_bar'
2062
+ else
2063
+ # adiabatic bar input calcs
2064
+ if target_perim**2 - 16 * footprint_si > 0
2065
+ adiabatic_dual_double_end_length = 0.25 * (target_perim + Math.sqrt(target_perim**2 - 16 * footprint_si))
2066
+ adiabatic_dual_double_end_width = footprint_si / adiabatic_dual_double_end_length
2067
+ # test for unexpected
2068
+ unexpected = false
2069
+ if (target_area - adiabatic_dual_double_end_length*adiabatic_dual_double_end_width).abs > tol_testing then unexpected = true end
2070
+ if specified_bar_width_si == 0
2071
+ if (target_perim - (adiabatic_dual_double_end_length * 2 + adiabatic_dual_double_end_width * 2)).abs > tol_testing then unexpected = true end
2072
+ end
2073
+ if unexpected
2074
+ runner.registerWarning("Unexpected values for dual rectangle adiabatic ends bar b.")
2075
+ end
2076
+ # now that stretched bar is made, determine where to split it and rotate
2077
+ 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'])
2078
+ adiabatic_bar_b_length = adiabatic_dual_double_end_length - adiabatic_bar_a_length
2079
+ adiabatic_area_a = adiabatic_bar_a_length * adiabatic_dual_double_end_width
2080
+ adiabatic_area_b = adiabatic_bar_b_length * adiabatic_dual_double_end_width
2081
+ else
2082
+ # this will throw it stretched single bar
2083
+ adiabatic_bar_a_length = 0
2084
+ adiabatic_bar_b_length = 0
2085
+ end
2086
+ if adiabatic_bar_a_length >= min_allow_size && adiabatic_bar_b_length >= min_allow_size
2087
+ dual_bar_calc_approach = 'adiabatic_ends_bar_b'
2088
+ else
2089
+ dual_bar_calc_approach = 'stretched'
2090
+ end
2091
+ end
2092
+
2093
+ # apply prescribed approach for stretched or dual bar
2094
+ if dual_bar_calc_approach == 'dual_bar'
2095
+ 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")
2096
+ 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
2097
+ runner.registerWarning("Unexpected values for dual rectangle.")
2098
+ end
2099
+
2100
+ 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")
2101
+ 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
2102
+ runner.registerWarning("Unexpected values for rotated dual rectangle")
2103
+ end
2104
+ elsif dual_bar_calc_approach == 'adiabatic_ends_bar_b'
2105
+ runner.registerInfo("Can't hit target perimeter with two rectangles, need to make two ends adiabatic")
2106
+
2107
+ 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")
2108
+ 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
2109
+ runner.registerWarning("Unexpected values for rotated dual rectangle adiabatic ends bar b")
2110
+ end
2111
+ else # stretched bar
2112
+ dual_bar = false
2113
+
2114
+ stretched_length = 0.25 * (target_perim + Math.sqrt(target_perim**2 - 16 * footprint_si))
2115
+ stretched_width = footprint_si / stretched_length
2116
+ if (target_area - stretched_length*stretched_width).abs > tol_testing || (target_perim - (stretched_length + stretched_width)*2) > tol_testing
2117
+ runner.registerWarning("Unexpected values for single stretched")
2118
+ end
2119
+
2120
+ width = stretched_width
2121
+ length = stretched_length
2122
+ 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.")
2123
+ end
2124
+ end
2125
+
2126
+ bars = {}
2127
+ bars['primary'] = {}
2128
+ if dual_bar
2129
+ if mirror_ns_ew && dual_bar_calc_approach == 'dual_bar'
2130
+ bars['primary'][:length] = dual_double_end_width
2131
+ bars['primary'][:width] = bar_a_length
2132
+ elsif dual_bar_calc_approach == 'dual_bar'
2133
+ bars['primary'][:length] = bar_a_length
2134
+ bars['primary'][:width] = dual_double_end_width
2135
+ elsif mirror_ns_ew
2136
+ bars['primary'][:length] = adiabatic_dual_double_end_width
2137
+ bars['primary'][:width] = adiabatic_bar_a_length
2138
+ else
2139
+ bars['primary'][:length] = adiabatic_bar_a_length
2140
+ bars['primary'][:width] = adiabatic_dual_double_end_width
2141
+ end
2142
+ else
2143
+ if mirror_ns_ew
2144
+ bars['primary'][:length] = width
2145
+ bars['primary'][:width] = length
2146
+ else
2147
+ bars['primary'][:length] = length
2148
+ bars['primary'][:width] = width
2149
+ end
2150
+ end
2151
+ bars['primary'][:floor_height_si] = floor_height_si # can make use of this when breaking out multi-height spaces
2152
+ bars['primary'][:num_stories] = num_stories
2153
+ bars['primary'][:center_of_footprint] = OpenStudio::Point3d.new(0.0,0.0,0.0)
2154
+ space_types_hash_secondary = {}
2155
+ if dual_bar
2156
+ # loop through each story and move portion for other bar to its own hash
2157
+ primary_footprint = bars['primary'][:length] * bars['primary'][:width]
2158
+ secondary_footprint = target_area - primary_footprint
2159
+ footprint_counter = primary_footprint
2160
+ secondary_footprint_counter = secondary_footprint
2161
+ story_counter = 0
2162
+ pri_sec_tol = 0.0001 #m^2
2163
+ pri_sec_min_area = 0.0001 #m^2
2164
+ space_types_hash.each do |k,v|
2165
+ space_type_left = v[:floor_area]
2166
+
2167
+ # do not go to next space type until this one is evaulate, which may span stories
2168
+ until space_type_left == 0.0 || story_counter >= num_stories
2169
+
2170
+ # use secondary footprint if any left
2171
+ if secondary_footprint_counter > 0.0
2172
+ hash_area = [space_type_left,secondary_footprint_counter].min
2173
+
2174
+ # confirm that the part of space type use or what is left is greater than min allowed value
2175
+ projected_space_type_left = space_type_left - hash_area
2176
+ test_a = if hash_area >= pri_sec_min_area then true else false end
2177
+ test_b = if projected_space_type_left >= pri_sec_min_area || projected_space_type_left == 0.0 then true else false end
2178
+ 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
2179
+ if (test_a && test_b) || test_c
2180
+ if space_types_hash_secondary.has_key?(k)
2181
+ # add to what was added for previous story
2182
+ space_types_hash_secondary[k][:floor_area] += hash_area
2183
+ else
2184
+ # add new space type to hash
2185
+ if v.has_key?(:children)
2186
+ space_types_hash_secondary[k] = {:floor_area => hash_area, :space_type => v[:space_type], :children => v[:children],}
2187
+ else
2188
+ space_types_hash_secondary[k] = {:floor_area => hash_area, :space_type => v[:space_type]}
2189
+ end
2190
+ end
2191
+ space_types_hash[k][:floor_area] -= hash_area
2192
+ secondary_footprint_counter -= hash_area
2193
+ space_type_left -= hash_area
2194
+ else
2195
+ runner.registerInfo("Shifting space types between bars to avoid sliver of #{k.name}.")
2196
+ end
2197
+ end
2198
+
2199
+ # remove space if entirely used up by secondary bar
2200
+ if space_types_hash[k][:floor_area] <= pri_sec_tol
2201
+ space_types_hash.delete(k)
2202
+ space_type_left = 0.0
2203
+ else
2204
+ # then look at primary bar
2205
+ hash_area_pri = [space_type_left,footprint_counter].min
2206
+ footprint_counter -= hash_area_pri
2207
+ space_type_left -= hash_area_pri
2208
+ end
2209
+
2210
+ # reset counter when full
2211
+ if footprint_counter <= pri_sec_tol && secondary_footprint_counter <= pri_sec_tol
2212
+ # check if this is partial top floor
2213
+ story_counter += 1
2214
+ if num_stories < story_counter + 1
2215
+ footprint_counter = primary_footprint * (num_stories - story_counter)
2216
+ secondary_footprint_counter = secondary_footprint * (num_stories - story_counter)
2217
+ else
2218
+ footprint_counter = primary_footprint
2219
+ secondary_footprint_counter = secondary_footprint
2220
+ end
2221
+ end
2222
+ end
2223
+ end
2224
+ end
2225
+
2226
+ # setup bar_hash and run create_bar
2227
+ bars['primary'][:space_types_hash] = space_types_hash
2228
+ bars['primary'][:args] = args
2229
+ v = bars['primary']
2230
+ 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])
2231
+
2232
+ # store offset value for multiple bars
2233
+ if args.has_key?('bar_sep_dist_mult') && args['bar_sep_dist_mult'] > 0.0
2234
+ offset_val = num_stories.ceil * floor_height_si * args['bar_sep_dist_mult']
2235
+ elsif args.has_key?('bar_sep_dist_mult')
2236
+ runner.registerWarning("Positive valu eis required for bar_sep_dist_mult, ignoring input and using value of 0.1")
2237
+ offset_val = num_stories.ceil * floor_height_si * 0.1
2238
+ else
2239
+ offset_val = num_stories.ceil * floor_height_si * 10.0
2240
+ end
2241
+
2242
+ if dual_bar
2243
+ args2 = args.clone
2244
+ bars['secondary'] = {}
2245
+ if mirror_ns_ew && dual_bar_calc_approach == 'dual_bar'
2246
+ bars['secondary'][:length] = bar_b_length
2247
+ bars['secondary'][:width] = dual_double_end_width
2248
+ elsif dual_bar_calc_approach == 'dual_bar'
2249
+ bars['secondary'][:length] = dual_double_end_width
2250
+ bars['secondary'][:width] = bar_b_length
2251
+ elsif mirror_ns_ew
2252
+ bars['secondary'][:length] = adiabatic_bar_b_length
2253
+ bars['secondary'][:width] = adiabatic_dual_double_end_width
2254
+ args2['party_wall_stories_east'] = num_stories.ceil
2255
+ args2['party_wall_stories_west'] = num_stories.ceil
2256
+ else
2257
+ bars['secondary'][:length] = adiabatic_dual_double_end_width
2258
+ bars['secondary'][:width] = adiabatic_bar_b_length
2259
+ args2['party_wall_stories_south'] = num_stories.ceil
2260
+ args2['party_wall_stories_north'] = num_stories.ceil
2261
+ end
2262
+ bars['secondary'][:floor_height_si] = floor_height_si # can make use of this when breaking out multi-height spaces
2263
+ bars['secondary'][:num_stories] = num_stories
2264
+ bars['secondary'][:space_types_hash] = space_types_hash_secondary
2265
+ if dual_bar_calc_approach == 'adiabatic_ends_bar_b'
2266
+ # 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
2267
+ 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
2268
+ 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.')
2269
+ else
2270
+ runner.registerInfo('Adiabatic ends added to secondary bar because target perimeter multiplier could not be met with two full rectangular footprints.')
2271
+ end
2272
+ 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)
2273
+ else
2274
+ 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)
2275
+ end
2276
+ bars['secondary'][:args] = args2
2277
+
2278
+ # setup bar_hash and run create_bar
2279
+ v = bars['secondary']
2280
+ 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])
2281
+
2282
+ end
2283
+
2284
+ # future development (up against primary bar run intersection and surface matching after add all bars, avoid interior windows)
2285
+ # 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
2286
+ if multi_height_space_types_hash.size > 0
2287
+ args3 = args.clone
2288
+ bars['custom_height'] = {}
2289
+ if mirror_ns_ew
2290
+ bars['custom_height'][:length] = width_cust_height
2291
+ bars['custom_height'][:width] = length_cust_height
2292
+ else
2293
+ bars['custom_height'][:length] = length_cust_height
2294
+ bars['custom_height'][:width] = width_cust_height
2295
+ end
2296
+ if args['party_wall_stories_east'] + args['party_wall_stories_west'] + args['party_wall_stories_south'] + args['party_wall_stories_north'] > 0.0
2297
+ runner.registerWarning("Ignorning party wall inputs for custom height bar")
2298
+ end
2299
+
2300
+ # disable party walls
2301
+ args3['party_wall_stories_east'] = 0
2302
+ args3['party_wall_stories_west'] = 0
2303
+ args3['party_wall_stories_south'] = 0
2304
+ args3['party_wall_stories_north'] = 0
2305
+
2306
+ # setup stories
2307
+ args3['num_stories_below_grade'] = 0
2308
+ args3['num_stories_above_grade'] = 1
2309
+
2310
+ bars['custom_height'][:floor_height_si] = floor_height_si # can make use of this when breaking out multi-height spaces
2311
+ bars['custom_height'][:num_stories] = num_stories
2312
+ 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)
2313
+ bars['custom_height'][:floor_height_si] = OpenStudio.convert(custom_story_heights.max,'ft','m').get
2314
+ bars['custom_height'][:num_stories] = 1
2315
+ bars['custom_height'][:space_types_hash] = multi_height_space_types_hash
2316
+ bars['custom_height'][:args] = args3
2317
+
2318
+ v = bars['custom_height']
2319
+ 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])
2320
+ end
2321
+
2322
+ # diagnostic log
2323
+ sum_actual = 0.0
2324
+ sum_target = 0.0
2325
+ throw_error = false
2326
+
2327
+ # check expected floor areas against actual
2328
+ model.getSpaceTypes.sort.each do |space_type|
2329
+ next if !target_areas.key? space_type # space type in model not part of building type(s), maybe issue warning
2330
+
2331
+ # convert to IP
2332
+ actual_ip = OpenStudio.convert(space_type.floorArea, 'm^2', 'ft^2').get
2333
+ target_ip = OpenStudio.convert(target_areas[space_type], 'm^2', 'ft^2').get
2334
+ sum_actual += actual_ip
2335
+ sum_target += target_ip
2336
+
2337
+ if (space_type.floorArea - target_areas[space_type]).abs >= 1.0
2338
+
2339
+ if !args['bar_division_method'].include? 'Single Space Type'
2340
+ 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)")
2341
+ throw_error = true
2342
+ else
2343
+ # will see this if use Single Space type division method on multi-use building or single building type without whole building space type
2344
+ 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)")
2345
+ end
2346
+
2347
+ end
2348
+ end
2349
+
2350
+ # report summary then throw error
2351
+ if throw_error
2352
+ runner.registerError("Sum of actual floor area is #{sum_actual} ft^2, sum of target floor area is #{sum_target}.")
2353
+ return false
2354
+ end
2355
+
2356
+ # check party wall fraction by looping through surfaces
2357
+ if args['party_wall_fraction'] > 0
2358
+ actual_ext_wall_area = model.getBuilding.exteriorWallArea
2359
+ actual_party_wall_area = 0.0
2360
+ model.getSurfaces.each do |surface|
2361
+ next if surface.outsideBoundaryCondition != 'Adiabatic'
2362
+ next if surface.surfaceType != 'Wall'
2363
+ actual_party_wall_area += surface.grossArea * surface.space.get.multiplier
2364
+ end
2365
+ actual_party_wall_fraction = actual_party_wall_area / (actual_party_wall_area + actual_ext_wall_area)
2366
+ runner.registerInfo("Target party wall fraction is #{args['party_wall_fraction']}. Realized fraction is #{actual_party_wall_fraction.round(2)}")
2367
+ runner.registerValue('party_wall_fraction_actual', actual_party_wall_fraction)
2368
+ end
2369
+
2370
+ # check ns/ew aspect ratio (harder to check when party walls are added)
2371
+ wall_and_window_by_orientation = OsLib_Geometry.getExteriorWindowAndWllAreaByOrientation(model,model.getSpaces)
2372
+ wall_ns = (wall_and_window_by_orientation['northWall'] + wall_and_window_by_orientation['southWall'])
2373
+ wall_ew = wall_and_window_by_orientation['eastWall'] + wall_and_window_by_orientation['westWall']
2374
+ wall_ns_ip = OpenStudio.convert(wall_ns,'m^2','ft^2').get
2375
+ wall_ew_ip = OpenStudio.convert(wall_ew,'m^2','ft^2').get
2376
+ runner.registerValue('wall_area_ip',wall_ns_ip + wall_ew_ip,'ft^2')
2377
+ runner.registerValue('ns_wall_area_ip',wall_ns_ip,'ft^2')
2378
+ runner.registerValue('ew_wall_area_ip',wall_ew_ip,'ft^2')
2379
+ # for now using perimeter of ground floor and average story area (building area / num_stories)
2380
+ runner.registerValue('floor_area_to_perim_ratio',model.getBuilding.floorArea / (OsLib_Geometry.calculate_perimeter(model) * num_stories))
2381
+ runner.registerValue('bar_width',OpenStudio.convert(bars['primary'][:width],'m','ft').get,'ft')
2382
+
2383
+ 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
2384
+ runner.registerInfo("Target facade area by orientation not validated when party walls are applied")
2385
+ elsif args['num_stories_above_grade'] != args['num_stories_above_grade'].ceil
2386
+ runner.registerInfo("Target facade area by orientation not validated when partial top story is used")
2387
+ elsif dual_bar_calc_approach == 'stretched'
2388
+ runner.registerInfo("Target facade area by orientation not validated when single stretched bar has to be used to meet target minimum perimeter multiplier")
2389
+ elsif defaulted_args.include?('floor_height') && args['custom_height_bar'] && multi_height_space_types_hash.size > 0
2390
+ runner.registerInfo("Target facade area by orientation not validated when a dedicated bar is added for space types with custom heights")
2391
+ elsif args['bar_width'] > 0
2392
+ runner.registerInfo("Target facade area by orientation not validated when a dedicated custom bar width is defined")
2393
+ else
2394
+
2395
+ # adjust length versus width based on building rotation
2396
+ if mirror_ns_ew
2397
+ wall_target_ns_ip = 2 * OpenStudio.convert(width,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2398
+ wall_target_ew_ip = 2 * OpenStudio.convert(length,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2399
+ else
2400
+ wall_target_ns_ip = 2 * OpenStudio.convert(length,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2401
+ wall_target_ew_ip = 2 * OpenStudio.convert(width,'m','ft').get * args['perim_mult'] * args['num_stories_above_grade'] * args['floor_height']
2402
+ end
2403
+ flag_error = false
2404
+ if (wall_target_ns_ip - wall_ns_ip).abs > 0.1
2405
+ 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)")
2406
+ flag_error = true
2407
+ end
2408
+ if (wall_target_ew_ip - wall_ew_ip).abs > 0.1
2409
+ 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)")
2410
+ flag_error = true
2411
+ end
2412
+ if flag_error
2413
+ return false
2414
+ end
2415
+ end
2416
+
2417
+ # test for excessive exterior roof area (indication of problem with intersection and or surface matching)
2418
+ ext_roof_area = model.getBuilding.exteriorSurfaceArea - model.getBuilding.exteriorWallArea
2419
+ expected_roof_area = args['total_bldg_floor_area'] / (args['num_stories_above_grade'] + args['num_stories_below_grade']).to_f
2420
+ if ext_roof_area > expected_roof_area && single_floor_area_si == 0.0 # only test if using whole-building area input
2421
+ runner.registerError('Roof area larger than expected, may indicate problem with inter-floor surface intersection or matching.')
2422
+ return false
2423
+ end
2424
+
2425
+ # set building rotation
2426
+ initial_rotation = model.getBuilding.northAxis
2427
+ if args['building_rotation'] != initial_rotation
2428
+ model.getBuilding.setNorthAxis(args['building_rotation'])
2429
+ 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.")
2430
+ end
2431
+
2432
+ # report final condition of model
2433
+ runner.registerFinalCondition("The building finished with #{model.getSpaces.size} spaces.")
2434
+
2435
+ return true
2436
+ end
2437
+
2438
+ # typical
2439
+ # used for varieties of measures that create typical building from model
2440
+ def typical_building_from_model(model, runner, user_arguments)
2441
+
2442
+ # assign the user inputs to variables
2443
+ args = OsLib_HelperMethods.createRunVariables(runner, model, user_arguments, arguments(model))
2444
+ if !args then return false end
2445
+
2446
+ # lookup and replace argument values from upstream measures
2447
+ if args['use_upstream_args'] == true
2448
+ args.each do |arg, value|
2449
+ next if arg == 'use_upstream_args' # this argument should not be changed
2450
+ value_from_osw = OsLib_HelperMethods.check_upstream_measure_for_arg(runner, arg)
2451
+ if !value_from_osw.empty?
2452
+ runner.registerInfo("Replacing argument named #{arg} from current measure with a value of #{value_from_osw[:value]} from #{value_from_osw[:measure_name]}.")
2453
+ new_val = value_from_osw[:value]
2454
+ # TODO: - make code to handle non strings more robust. check_upstream_measure_for_arg coudl pass bakc the argument type
2455
+ if arg == 'total_bldg_floor_area'
2456
+ args[arg] = new_val.to_f
2457
+ elsif arg == 'num_stories_above_grade'
2458
+ args[arg] = new_val.to_f
2459
+ elsif arg == 'zipcode'
2460
+ args[arg] = new_val.to_i
2461
+ else
2462
+ args[arg] = new_val
2463
+ end
2464
+ end
2465
+ end
2466
+ end
2467
+
2468
+ # validate fraction parking
2469
+ 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'])
2470
+ if !fraction then return false end
2471
+
2472
+ # validate unmet hours tolerance
2473
+ 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'])
2474
+ if !unmet_hours_tolerance_valid then return false end
2475
+
2476
+ # validate weekday hours of operation
2477
+ wkdy_op_hrs_start_time_hr = nil
2478
+ wkdy_op_hrs_start_time_min = nil
2479
+ wkdy_op_hrs_duration_hr = nil
2480
+ wkdy_op_hrs_duration_min = nil
2481
+ if args['modify_wkdy_op_hrs']
2482
+ # weekday start time hr
2483
+ wkdy_op_hrs_start_time_hr = args['wkdy_op_hrs_start_time'].floor
2484
+ if wkdy_op_hrs_start_time_hr < 0 || wkdy_op_hrs_start_time_hr > 24
2485
+ runner.registerError("Weekday operating hours start time hrs must be between 0 and 24. #{args['wkdy_op_hrs_start_time']} was entered.")
2486
+ return false
2487
+ end
2488
+
2489
+ # weekday start time min
2490
+ wkdy_op_hrs_start_time_min = (60.0 * (args['wkdy_op_hrs_start_time'] - args['wkdy_op_hrs_start_time'].floor)).floor
2491
+ if wkdy_op_hrs_start_time_min < 0 || wkdy_op_hrs_start_time_min > 59
2492
+ runner.registerError("Weekday operating hours start time mins must be between 0 and 59. #{args['wkdy_op_hrs_start_time']} was entered.")
2493
+ return false
2494
+ end
2495
+
2496
+ # weekday duration hr
2497
+ wkdy_op_hrs_duration_hr = args['wkdy_op_hrs_duration'].floor
2498
+ if wkdy_op_hrs_duration_hr < 0 || wkdy_op_hrs_duration_hr > 24
2499
+ runner.registerError("Weekday operating hours duration hrs must be between 0 and 24. #{args['wkdy_op_hrs_duration']} was entered.")
2500
+ return false
2501
+ end
2502
+
2503
+ # weekday duration min
2504
+ wkdy_op_hrs_duration_min = (60.0 * (args['wkdy_op_hrs_duration'] - args['wkdy_op_hrs_duration'].floor)).floor
2505
+ if wkdy_op_hrs_duration_min < 0 || wkdy_op_hrs_duration_min > 59
2506
+ runner.registerError("Weekday operating hours duration mins must be between 0 and 59. #{args['wkdy_op_hrs_duration']} was entered.")
2507
+ return false
2508
+ end
2509
+
2510
+ # check that weekday start time plus duration does not exceed 24 hrs
2511
+ 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
2512
+ 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.")
2513
+ end
2514
+ end
2515
+
2516
+ # validate weekend hours of operation
2517
+ wknd_op_hrs_start_time_hr = nil
2518
+ wknd_op_hrs_start_time_min = nil
2519
+ wknd_op_hrs_duration_hr = nil
2520
+ wknd_op_hrs_duration_min = nil
2521
+ if args['modify_wknd_op_hrs']
2522
+ # weekend start time hr
2523
+ wknd_op_hrs_start_time_hr = args['wknd_op_hrs_start_time'].floor
2524
+ if wknd_op_hrs_start_time_hr < 0 || wknd_op_hrs_start_time_hr > 24
2525
+ runner.registerError("Weekend operating hours start time hrs must be between 0 and 24. #{args['wknd_op_hrs_start_time_change']} was entered.")
2526
+ return false
2527
+ end
2528
+
2529
+ # weekend start time min
2530
+ wknd_op_hrs_start_time_min = (60.0 * (args['wknd_op_hrs_start_time'] - args['wknd_op_hrs_start_time'].floor)).floor
2531
+ if wknd_op_hrs_start_time_min < 0 || wknd_op_hrs_start_time_min > 59
2532
+ runner.registerError("Weekend operating hours start time mins must be between 0 and 59. #{args['wknd_op_hrs_start_time_change']} was entered.")
2533
+ return false
2534
+ end
2535
+
2536
+ # weekend duration hr
2537
+ wknd_op_hrs_duration_hr = args['wknd_op_hrs_duration'].floor
2538
+ if wknd_op_hrs_duration_hr < 0 || wknd_op_hrs_duration_hr > 24
2539
+ runner.registerError("Weekend operating hours duration hrs must be between 0 and 24. #{args['wknd_op_hrs_duration']} was entered.")
2540
+ return false
2541
+ end
2542
+
2543
+ # weekend duration min
2544
+ wknd_op_hrs_duration_min = (60.0 * (args['wknd_op_hrs_duration'] - args['wknd_op_hrs_duration'].floor)).floor
2545
+ if wknd_op_hrs_duration_min < 0 || wknd_op_hrs_duration_min > 59
2546
+ runner.registerError("Weekend operating hours duration min smust be between 0 and 59. #{args['wknd_op_hrs_duration']} was entered.")
2547
+ return false
2548
+ end
2549
+
2550
+ # check that weekend start time plus duration does not exceed 24 hrs
2551
+ 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
2552
+ 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.")
2553
+ end
2554
+ end
2555
+
2556
+ # report initial condition of model
2557
+ initial_objects = model.getModelObjects.size
2558
+ runner.registerInitialCondition("The building started with #{initial_objects} objects.")
2559
+
2560
+ # open channel to log messages
2561
+ reset_log
2562
+
2563
+ # Make the standard applier
2564
+ standard = Standard.build((args['template']).to_s)
2565
+
2566
+ # make sure daylight savings is turned on up prior to any sizing runs being done.
2567
+ if args['enable_dst']
2568
+ start_date = '2nd Sunday in March'
2569
+ end_date = '1st Sunday in November'
2570
+
2571
+ runperiodctrl_daylgtsaving = model.getRunPeriodControlDaylightSavingTime
2572
+ runperiodctrl_daylgtsaving.setStartDate(start_date)
2573
+ runperiodctrl_daylgtsaving.setEndDate(end_date)
2574
+ end
2575
+
2576
+ # add internal loads to space types
2577
+ if args['add_space_type_loads']
2578
+
2579
+ # remove internal loads
2580
+ if args['remove_objects']
2581
+ model.getSpaceLoads.each do |instance|
2582
+ next if instance.name.to_s.include?('Elevator') # most prototype building types model exterior elevators with name Elevator
2583
+ next if instance.to_InternalMass.is_initialized
2584
+ next if instance.to_WaterUseEquipment.is_initialized
2585
+ instance.remove
2586
+ end
2587
+ model.getDesignSpecificationOutdoorAirs.each(&:remove)
2588
+ model.getDefaultScheduleSets.each(&:remove)
2589
+ end
2590
+
2591
+ model.getSpaceTypes.each do |space_type|
2592
+ # Don't add infiltration here; will be added later in the script
2593
+ test = standard.space_type_apply_internal_loads(space_type, true, true, true, true, true, false)
2594
+ if test == false
2595
+ runner.registerWarning("Could not add loads for #{space_type.name}. Not expected for #{args['template']}")
2596
+ next
2597
+ end
2598
+
2599
+ # apply internal load schedules
2600
+ # the last bool test it to make thermostat schedules. They are now added in HVAC section instead of here
2601
+ standard.space_type_apply_internal_load_schedules(space_type, true, true, true, true, true, true, false)
2602
+
2603
+ # extend space type name to include the args['template']. Consider this as well for load defs
2604
+ space_type.setName("#{space_type.name} - #{args['template']}")
2605
+ runner.registerInfo("Adding loads to space type named #{space_type.name}")
2606
+ end
2607
+
2608
+ # warn if spaces in model without space type
2609
+ spaces_without_space_types = []
2610
+ model.getSpaces.each do |space|
2611
+ next if space.spaceType.is_initialized
2612
+ spaces_without_space_types << space
2613
+ end
2614
+ if !spaces_without_space_types.empty?
2615
+ runner.registerWarning("#{spaces_without_space_types.size} spaces do not have space types assigned, and wont' receive internal loads from standards space type lookups.")
2616
+ end
2617
+ end
2618
+
2619
+ # identify primary building type (used for construction, and ideally HVAC as well)
2620
+ building_types = {}
2621
+ model.getSpaceTypes.each do |space_type|
2622
+ # populate hash of building types
2623
+ if space_type.standardsBuildingType.is_initialized
2624
+ bldg_type = space_type.standardsBuildingType.get
2625
+ if !building_types.key?(bldg_type)
2626
+ building_types[bldg_type] = space_type.floorArea
2627
+ else
2628
+ building_types[bldg_type] += space_type.floorArea
2629
+ end
2630
+ else
2631
+ runner.registerWarning("Can't identify building type for #{space_type.name}")
2632
+ end
2633
+ end
2634
+ 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
2635
+ lookup_building_type = standard.model_get_lookup_name(primary_bldg_type) # Used for some lookups in the standards gem
2636
+ model.getBuilding.setStandardsBuildingType(primary_bldg_type)
2637
+
2638
+ # make construction set and apply to building
2639
+ if args['add_constructions']
2640
+
2641
+ # remove default construction sets
2642
+ if args['remove_objects']
2643
+ model.getDefaultConstructionSets.each(&:remove)
2644
+ end
2645
+
2646
+ # TODO: - allow building type and space type specific constructions set selection.
2647
+ if ['SmallHotel', 'LargeHotel', 'MidriseApartment', 'HighriseApartment'].include?(primary_bldg_type)
2648
+ is_residential = 'Yes'
2649
+ else
2650
+ is_residential = 'No'
2651
+ end
2652
+ if !args.has_key?('climate_zone') || args['climate_zone'] == 'Lookup From Model'
2653
+ climate_zone = standard.model_get_building_climate_zone_and_building_type(model)['climate_zone']
2654
+ runner.registerInfo("Using climate zone #{climate_zone} from model")
2655
+ else
2656
+ climate_zone = args['climate_zone']
2657
+ runner.registerInfo("Using climate zone #{climate_zone} from user arguments")
2658
+ end
2659
+ bldg_def_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, is_residential)
2660
+ if bldg_def_const_set.is_initialized
2661
+ bldg_def_const_set = bldg_def_const_set.get
2662
+ if is_residential then bldg_def_const_set.setName("Res #{bldg_def_const_set.name}") end
2663
+ model.getBuilding.setDefaultConstructionSet(bldg_def_const_set)
2664
+ runner.registerInfo("Adding default construction set named #{bldg_def_const_set.name}")
2665
+ else
2666
+ runner.registerError("Could not create default construction set for the building type #{lookup_building_type} in climate zone #{climate_zone}.")
2667
+ log_messages_to_runner(runner, debug = true)
2668
+ return false
2669
+ end
2670
+
2671
+ # address any adiabatic surfaces that don't have hard assigned constructions
2672
+ model.getSurfaces.each do |surface|
2673
+ next if surface.outsideBoundaryCondition != 'Adiabatic'
2674
+ next if surface.construction.is_initialized
2675
+ surface.setAdjacentSurface(surface)
2676
+ surface.setConstruction(surface.construction.get)
2677
+ surface.setOutsideBoundaryCondition('Adiabatic')
2678
+ end
2679
+
2680
+ # modify the infiltration rates
2681
+ if args['remove_objects']
2682
+ model.getSpaceInfiltrationDesignFlowRates.each(&:remove)
2683
+ end
2684
+ standard.model_apply_infiltration_standard(model)
2685
+ standard.model_modify_infiltration_coefficients(model, primary_bldg_type, climate_zone)
2686
+
2687
+ # set ground temperatures from DOE prototype buildings
2688
+ standard.model_add_ground_temperatures(model, primary_bldg_type, climate_zone)
2689
+
2690
+ end
2691
+
2692
+ # add elevators (returns ElectricEquipment object)
2693
+ if args['add_elevators']
2694
+
2695
+ # remove elevators as spaceLoads or exteriorLights
2696
+ model.getSpaceLoads.each do |instance|
2697
+ next if !instance.name.to_s.include?('Elevator') # most prototype building types model exterior elevators with name Elevator
2698
+ instance.remove
2699
+ end
2700
+ model.getExteriorLightss.each do |ext_light|
2701
+ next if !ext_light.name.to_s.include?('Fuel equipment') # some prototype building types model exterior elevators by this name
2702
+ ext_light.remove
2703
+ end
2704
+
2705
+ elevators = standard.model_add_elevators(model)
2706
+ if elevators.nil?
2707
+ runner.registerInfo('No elevators added to the building.')
2708
+ else
2709
+ elevator_def = elevators.electricEquipmentDefinition
2710
+ design_level = elevator_def.designLevel.get
2711
+ runner.registerInfo("Adding #{elevators.multiplier.round(1)} elevators each with power of #{OpenStudio.toNeatString(design_level, 0, true)} (W), plus lights and fans.")
2712
+ elevator_def.setFractionLost(1.0)
2713
+ elevator_def.setFractionRadiant(0.0)
2714
+ end
2715
+ end
2716
+
2717
+ # add exterior lights (returns a hash where key is lighting type and value is exteriorLights object)
2718
+ if args['add_exterior_lights']
2719
+
2720
+ if args['remove_objects']
2721
+ model.getExteriorLightss.each do |ext_light|
2722
+ next if ext_light.name.to_s.include?('Fuel equipment') # some prototype building types model exterior elevators by this name
2723
+ ext_light.remove
2724
+ end
2725
+ end
2726
+
2727
+ exterior_lights = standard.model_add_typical_exterior_lights(model, args['exterior_lighting_zone'].chars[0].to_i, args['onsite_parking_fraction'])
2728
+ exterior_lights.each do |k, v|
2729
+ runner.registerInfo("Adding Exterior Lights named #{v.exteriorLightsDefinition.name} with design level of #{v.exteriorLightsDefinition.designLevel} * #{OpenStudio.toNeatString(v.multiplier, 0, true)}.")
2730
+ end
2731
+ end
2732
+
2733
+ # add_exhaust
2734
+ if args['add_exhaust']
2735
+
2736
+ # remove exhaust objects
2737
+ if args['remove_objects']
2738
+ model.getFanZoneExhausts.each(&:remove)
2739
+ end
2740
+
2741
+ zone_exhaust_fans = standard.model_add_exhaust(model, args['kitchen_makeup']) # second argument is strategy for finding makeup zones for exhaust zones
2742
+ zone_exhaust_fans.each do |k, v|
2743
+ max_flow_rate_ip = OpenStudio.convert(k.maximumFlowRate.get, 'm^3/s', 'cfm').get
2744
+ if v.key?(:zone_mixing)
2745
+ zone_mixing = v[:zone_mixing]
2746
+ mixing_source_zone_name = zone_mixing.sourceZone.get.name
2747
+ mixing_design_flow_rate_ip = OpenStudio.convert(zone_mixing.designFlowRate.get, 'm^3/s', 'cfm').get
2748
+ 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}")
2749
+ else
2750
+ runner.registerInfo("Adding #{OpenStudio.toNeatString(max_flow_rate_ip, 0, true)} (cfm) of exhaust to #{k.thermalZone.get.name}")
2751
+ end
2752
+ end
2753
+ end
2754
+
2755
+ # add service water heating demand and supply
2756
+ if args['add_swh']
2757
+
2758
+ # remove water use equipment and water use connections
2759
+ if args['remove_objects']
2760
+ # TODO: - remove plant loops used for service water heating
2761
+ model.getWaterUseEquipments.each(&:remove)
2762
+ model.getWaterUseConnectionss.each(&:remove)
2763
+ end
2764
+
2765
+ # Infer the SWH type
2766
+ if args['swh_src'] == 'Inferred'
2767
+ if args['htg_src'] == 'NaturalGas' || args['htg_src'] == 'DistrictHeating'
2768
+ args['swh_src'] = 'NaturalGas' # If building has gas service, probably uses natural gas for SWH
2769
+ elsif args['htg_src'] == 'Electricity'
2770
+ args['swh_src'] == 'Electricity' # If building is doing space heating with electricity, probably used for SWH
2771
+ elsif args['htg_src'] == 'DistrictAmbient'
2772
+ args['swh_src'] == 'HeatPump' # If building has district ambient loop, it is fancy and probably uses HPs for SWH
2773
+ else
2774
+ args['swh_src'] = nil # Use inferences built into OpenStudio Standards for each building and space type
2775
+ end
2776
+ end
2777
+
2778
+ typical_swh = standard.model_add_typical_swh(model, water_heater_fuel: args['swh_src'])
2779
+ midrise_swh_loops = []
2780
+ stripmall_swh_loops = []
2781
+ typical_swh.each do |loop|
2782
+ if loop.name.get.include?('MidriseApartment')
2783
+ midrise_swh_loops << loop
2784
+ elsif loop.name.get.include?('RetailStripmall')
2785
+ stripmall_swh_loops << loop
2786
+ else
2787
+ water_use_connections = []
2788
+ loop.demandComponents.each do |component|
2789
+ next if !component.to_WaterUseConnections.is_initialized
2790
+ water_use_connections << component
2791
+ end
2792
+ runner.registerInfo("Adding #{loop.name} to the building. It has #{water_use_connections.size} water use connections.")
2793
+ end
2794
+ end
2795
+ if !midrise_swh_loops.empty?
2796
+ runner.registerInfo("Adding #{midrise_swh_loops.size} MidriseApartment service water heating loops.")
2797
+ end
2798
+ if !stripmall_swh_loops.empty?
2799
+ runner.registerInfo("Adding #{stripmall_swh_loops.size} RetailStripmall service water heating loops.")
2800
+ end
2801
+ end
2802
+
2803
+ # TODO: - when add methods below add bool to enable/disable them with default value to true
2804
+
2805
+ # add daylight controls, need to perform a sizing run for 2010
2806
+ if args['template'] == '90.1-2010'
2807
+ if standard.model_run_sizing_run(model, "#{Dir.pwd}/SRvt") == false
2808
+ log_messages_to_runner(runner, debug = true)
2809
+ return false
2810
+ end
2811
+ end
2812
+ standard.model_add_daylighting_controls(model)
2813
+
2814
+ # add refrigeration
2815
+ if args['add_refrigeration']
2816
+
2817
+ # remove refrigeration equipment
2818
+ if args['remove_objects']
2819
+ model.getRefrigerationSystems.each(&:remove)
2820
+ end
2821
+
2822
+ # Add refrigerated cases and walkins
2823
+ standard.model_add_typical_refrigeration(model, primary_bldg_type)
2824
+ end
2825
+
2826
+ # add internal mass
2827
+ if args['add_internal_mass']
2828
+
2829
+ if args['remove_objects']
2830
+ model.getSpaceLoads.each do |instance|
2831
+ next unless instance.to_InternalMass.is_initialized
2832
+ instance.remove
2833
+ end
2834
+ end
2835
+
2836
+ # add internal mass to conditioned spaces; needs to happen after thermostats are applied
2837
+ standard.model_add_internal_mass(model, primary_bldg_type)
2838
+ end
2839
+
2840
+ # TODO: - add slab modeling and slab insulation
2841
+
2842
+ # TODO: - fuel customization for cooking and laundry
2843
+ # works by switching some fraction of electric loads to gas if requested (assuming base load is electric)
2844
+
2845
+ # add thermostats
2846
+ if args['add_thermostat']
2847
+
2848
+ # remove thermostats
2849
+ if args['remove_objects']
2850
+ model.getThermostatSetpointDualSetpoints.each(&:remove)
2851
+ end
2852
+
2853
+ model.getSpaceTypes.each do |space_type|
2854
+ # create thermostat schedules
2855
+ # skip un-recognized space types
2856
+ next if standard.space_type_get_standards_data(space_type).empty?
2857
+ # the last bool test it to make thermostat schedules. They are added to the model but not assigned
2858
+ standard.space_type_apply_internal_load_schedules(space_type, false, false, false, false, false, false, true)
2859
+
2860
+ # identify thermal thermostat and apply to zones (apply_internal_load_schedules names )
2861
+ model.getThermostatSetpointDualSetpoints.each do |thermostat|
2862
+ next if thermostat.name.to_s != "#{space_type.name} Thermostat"
2863
+ next if !thermostat.coolingSetpointTemperatureSchedule.is_initialized
2864
+ next if !thermostat.heatingSetpointTemperatureSchedule.is_initialized
2865
+ runner.registerInfo("Assigning #{thermostat.name} to thermal zones with #{space_type.name} assigned.")
2866
+ space_type.spaces.each do |space|
2867
+ next if !space.thermalZone.is_initialized
2868
+ space.thermalZone.get.setThermostatSetpointDualSetpoint(thermostat)
2869
+ end
2870
+ end
2871
+ end
2872
+ end
2873
+
2874
+ # add hvac system
2875
+ if args['add_hvac']
2876
+
2877
+ # remove HVAC objects
2878
+ if args['remove_objects']
2879
+ standard.model_remove_prm_hvac(model)
2880
+ end
2881
+
2882
+ case args['system_type']
2883
+ when 'Inferred'
2884
+
2885
+ # Get the hvac delivery type enum
2886
+ hvac_delivery = case args['hvac_delivery_type']
2887
+ when 'Forced Air'
2888
+ 'air'
2889
+ when 'Hydronic'
2890
+ 'hydronic'
2891
+ end
2892
+
2893
+ # Group the zones by occupancy type. Only split out non-dominant groups if their total area exceeds the limit.
2894
+ sys_groups = standard.model_group_zones_by_type(model, OpenStudio.convert(20_000, 'ft^2', 'm^2').get)
2895
+
2896
+ # For each group, infer the HVAC system type.
2897
+ sys_groups.each do |sys_group|
2898
+ # Infer the primary system type
2899
+ # 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']}")
2900
+ sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel = standard.model_typical_hvac_system_type(model,
2901
+ climate_zone,
2902
+ sys_group['type'],
2903
+ hvac_delivery,
2904
+ args['htg_src'],
2905
+ args['clg_src'],
2906
+ OpenStudio.convert(sys_group['area_ft2'], 'ft^2', 'm^2').get,
2907
+ sys_group['stories'])
2908
+
2909
+ # Infer the secondary system type for multizone systems
2910
+ sec_sys_type = case sys_type
2911
+ when 'PVAV Reheat', 'VAV Reheat'
2912
+ 'PSZ-AC'
2913
+ when 'PVAV PFP Boxes', 'VAV PFP Boxes'
2914
+ 'PSZ-HP'
2915
+ else
2916
+ sys_type # same as primary system type
2917
+ end
2918
+
2919
+ # Group zones by story
2920
+ story_zone_lists = standard.model_group_zones_by_story(model, sys_group['zones'])
2921
+
2922
+ # On each story, add the primary system to the primary zones
2923
+ # and add the secondary system to any zones that are different.
2924
+ story_zone_lists.each do |story_group|
2925
+ # Differentiate primary and secondary zones, based on
2926
+ # operating hours and internal loads (same as 90.1 PRM)
2927
+ pri_sec_zone_lists = standard.model_differentiate_primary_secondary_thermal_zones(model, story_group)
2928
+ system_zones = pri_sec_zone_lists['primary']
2929
+
2930
+ # if the primary system type is PTAC, filter to cooled zones to prevent sizing error if no cooling
2931
+ if sys_type == 'PTAC'
2932
+ heated_and_cooled_zones = system_zones.select { |zone| standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2933
+ cooled_only_zones = system_zones.select { |zone| !standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2934
+ system_zones = heated_and_cooled_zones + cooled_only_zones
2935
+ end
2936
+
2937
+ # Add the primary system to the primary zones
2938
+ standard.model_add_hvac_system(model, sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel, system_zones)
2939
+
2940
+ # Add the secondary system to the secondary zones (if any)
2941
+ if !pri_sec_zone_lists['secondary'].empty?
2942
+ system_zones = pri_sec_zone_lists['secondary']
2943
+ if (sec_sys_type == 'PTAC') || (sec_sys_type == 'PSZ-AC')
2944
+ heated_and_cooled_zones = system_zones.select { |zone| standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2945
+ cooled_only_zones = system_zones.select { |zone| !standard.thermal_zone_heated?(zone) && standard.thermal_zone_cooled?(zone) }
2946
+ system_zones = heated_and_cooled_zones + cooled_only_zones
2947
+ end
2948
+ standard.model_add_hvac_system(model, sec_sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel, system_zones)
2949
+ end
2950
+ end
2951
+ end
2952
+
2953
+ else # HVAC system_type specified
2954
+
2955
+ # Group the zones by occupancy type. Only split out non-dominant groups if their total area exceeds the limit.
2956
+ sys_groups = standard.model_group_zones_by_type(model, OpenStudio.convert(20_000, 'ft^2', 'm^2').get)
2957
+ sys_groups.each do |sys_group|
2958
+ # Group the zones by story
2959
+ story_groups = standard.model_group_zones_by_story(model, sys_group['zones'])
2960
+
2961
+ # Add the user specified HVAC system for each story.
2962
+ # Single-zone systems will get one per zone.
2963
+ story_groups.each do |zones|
2964
+ model.add_cbecs_hvac_system(standard, args['system_type'], zones)
2965
+ end
2966
+ end
2967
+ end
2968
+ end
2969
+
2970
+ # hours of operation
2971
+ if args['modify_wkdy_op_hrs'] || args['modify_wknd_op_hrs']
2972
+ # Infer the current hours of operation schedule for the building
2973
+ op_sch = standard.model_infer_hours_of_operation_building(model)
2974
+
2975
+ # Convert existing schedules in the model to parametric schedules based on current hours of operation
2976
+ standard.model_setup_parametric_schedules(model)
2977
+
2978
+ # Create start and end times from start time and duration supplied
2979
+ wkdy_start_time = nil
2980
+ wkdy_end_time = nil
2981
+ wknd_start_time = nil
2982
+ wknd_end_time = nil
2983
+ # weekdays
2984
+ if args['modify_wkdy_op_hrs']
2985
+ wkdy_start_time = OpenStudio::Time.new(0, wkdy_op_hrs_start_time_hr, wkdy_op_hrs_start_time_min, 0)
2986
+ wkdy_end_time = wkdy_start_time + OpenStudio::Time.new(0, wkdy_op_hrs_duration_hr, wkdy_op_hrs_duration_min, 0)
2987
+ end
2988
+ # weekends
2989
+ if args['modify_wknd_op_hrs']
2990
+ wknd_start_time = OpenStudio::Time.new(0, wknd_op_hrs_start_time_hr, wknd_op_hrs_start_time_min, 0)
2991
+ wknd_end_time = wknd_start_time + OpenStudio::Time.new(0, wknd_op_hrs_duration_hr, wknd_op_hrs_duration_min, 0)
2992
+ end
2993
+
2994
+ # Modify hours of operation, using weekdays values for all weekdays and weekend values for Saturday and Sunday
2995
+ standard.schedule_ruleset_set_hours_of_operation(op_sch,
2996
+ wkdy_start_time: wkdy_start_time,
2997
+ wkdy_end_time: wkdy_end_time,
2998
+ sat_start_time: wknd_start_time,
2999
+ sat_end_time: wknd_end_time,
3000
+ sun_start_time: wknd_start_time,
3001
+ sun_end_time: wknd_end_time)
3002
+
3003
+ # Apply new operating hours to parametric schedules to make schedules in model reflect modified hours of operation
3004
+ parametric_schedules = standard.model_apply_parametric_schedules(model, error_on_out_of_order: false)
3005
+ runner.registerInfo("Updated #{parametric_schedules.size} schedules with new hours of operation.")
3006
+ end
3007
+
3008
+ # set hvac controls and efficiencies (this should be last model articulation element)
3009
+ if args['add_hvac']
3010
+ # set additional properties for building
3011
+ props = model.getBuilding.additionalProperties
3012
+ props.setFeature('hvac_system_type',"#{args['system_type']}")
3013
+
3014
+ case args['system_type']
3015
+ when 'Ideal Air Loads'
3016
+
3017
+ else
3018
+ # Set the heating and cooling sizing parameters
3019
+ standard.model_apply_prm_sizing_parameters(model)
3020
+
3021
+ # Perform a sizing run
3022
+ if standard.model_run_sizing_run(model, "#{Dir.pwd}/SR1") == false
3023
+ log_messages_to_runner(runner, debug = true)
3024
+ return false
3025
+ end
3026
+
3027
+ # If there are any multizone systems, reset damper positions
3028
+ # to achieve a 60% ventilation effectiveness minimum for the system
3029
+ # following the ventilation rate procedure from 62.1
3030
+ standard.model_apply_multizone_vav_outdoor_air_sizing(model)
3031
+
3032
+ # Apply the prototype HVAC assumptions
3033
+ standard.model_apply_prototype_hvac_assumptions(model, primary_bldg_type, climate_zone)
3034
+
3035
+ # Apply the HVAC efficiency standard
3036
+ standard.model_apply_hvac_efficiency_standard(model, climate_zone)
3037
+ end
3038
+ end
3039
+
3040
+ # add internal mass
3041
+ if args['add_internal_mass']
3042
+
3043
+ if args['remove_objects']
3044
+ model.getSpaceLoads.each do |instance|
3045
+ next unless instance.to_InternalMass.is_initialized
3046
+ instance.remove
3047
+ end
3048
+ end
3049
+
3050
+ # add internal mass to conditioned spaces; needs to happen after thermostats are applied
3051
+ standard.model_add_internal_mass(model, primary_bldg_type)
3052
+ end
3053
+
3054
+ # set unmet hours tolerance
3055
+ unmet_hrs_tol_r = args['unmet_hours_tolerance']
3056
+ unmet_hrs_tol_k = OpenStudio.convert(unmet_hrs_tol_r, 'R', 'K').get
3057
+ tolerances = model.getOutputControlReportingTolerances
3058
+ tolerances.setToleranceforTimeHeatingSetpointNotMet(unmet_hrs_tol_k)
3059
+ tolerances.setToleranceforTimeCoolingSetpointNotMet(unmet_hrs_tol_k)
3060
+
3061
+ # remove everything but spaces, zones, and stub space types (extend as needed for additional objects, may make bool arg for this)
3062
+ if args['remove_objects']
3063
+ model.purgeUnusedResourceObjects
3064
+ objects_after_cleanup = initial_objects - model.getModelObjects.size
3065
+ if objects_after_cleanup > 0
3066
+ runner.registerInfo("Removing #{objects_after_cleanup} objects from model")
3067
+ end
3068
+ end
3069
+
3070
+ # report final condition of model
3071
+ runner.registerFinalCondition("The building finished with #{model.getModelObjects.size} objects.")
3072
+
3073
+ # log messages to info messages
3074
+ log_messages_to_runner(runner, debug = false)
3075
+
3076
+ return true
3077
+ end
3078
+
3079
+ # wizard
3080
+ # used for varieties of measures that create space type and construction set wizard
3081
+ def wizard(model, runner, user_arguments)
3082
+
3083
+ # use the built-in error checking
3084
+ if !runner.validateUserArguments(arguments(model), user_arguments)
3085
+ return false
3086
+ end
3087
+
3088
+ # assign the user inputs to variables
3089
+ building_type = runner.getStringArgumentValue('building_type', user_arguments)
3090
+ template = runner.getStringArgumentValue('template', user_arguments)
3091
+ climate_zone = runner.getStringArgumentValue('climate_zone', user_arguments)
3092
+ create_space_types = runner.getBoolArgumentValue('create_space_types', user_arguments)
3093
+ create_construction_set = runner.getBoolArgumentValue('create_construction_set', user_arguments)
3094
+ set_building_defaults = runner.getBoolArgumentValue('set_building_defaults', user_arguments)
3095
+
3096
+ # reporting initial condition of model
3097
+ starting_spaceTypes = model.getSpaceTypes
3098
+ starting_constructionSets = model.getDefaultConstructionSets
3099
+ runner.registerInitialCondition("The building started with #{starting_spaceTypes.size} space types and #{starting_constructionSets.size} construction sets.")
3100
+
3101
+ # lookup space types for specified building type (false indicates not to use whole building type only)
3102
+ space_type_hash = get_space_types_from_building_type(building_type, template, false)
3103
+ if space_type_hash == false
3104
+ runner.registerError("#{building_type} is an unexpected building type.")
3105
+ return false
3106
+ end
3107
+
3108
+ # create space_type_map from array
3109
+ space_type_map = {}
3110
+ default_space_type_name = nil
3111
+ space_type_hash.each do |space_type_name, hash|
3112
+ next if hash[:space_type_gen] == false # space types like undeveloped and basement are skipped.
3113
+ space_type_map[space_type_name] = [] # no spaces to pass in
3114
+ if hash[:default]
3115
+ default_space_type_name = space_type_name
3116
+ end
3117
+ end
3118
+
3119
+ # Make the standard applier
3120
+ standard = Standard.build(template)
3121
+
3122
+ # mapping building_type name is needed for a few methods
3123
+ lookup_building_type = standard.model_get_lookup_name(building_type)
3124
+
3125
+ # remap small medium and large office to office
3126
+ if building_type.include?("Office") then building_type = "Office" end
3127
+
3128
+ # get array of new space types
3129
+ space_types_new = []
3130
+
3131
+ # create_space_types
3132
+ if create_space_types
3133
+
3134
+ # array of starting space types
3135
+ space_types_starting = model.getSpaceTypes
3136
+
3137
+ # create stub space types
3138
+ space_type_hash.each do |space_type_name, hash|
3139
+ next if hash[:space_type_gen] == false # space types like undeveloped and basement are skipped.
3140
+
3141
+ # create space type
3142
+ space_type = OpenStudio::Model::SpaceType.new(model)
3143
+ space_type.setStandardsBuildingType(building_type)
3144
+ space_type.setStandardsSpaceType(space_type_name)
3145
+ space_type.setName("#{building_type} #{space_type_name}")
3146
+
3147
+ # add to array of new space types
3148
+ space_types_new << space_type
3149
+
3150
+ # add internal loads (the nil check isn't ncessary, but I will keep it in as a warning instad of an error)
3151
+ test = standard.space_type_apply_internal_loads(space_type, true, true, true, true, true, true)
3152
+ if test.nil?
3153
+ runner.registerWarning("Could not add loads for #{space_type.name}. Not expected for #{template} #{lookup_building_type}")
3154
+ end
3155
+
3156
+ # the last bool test it to make thermostat schedules. They are added to the model but not assigned
3157
+ standard.space_type_apply_internal_load_schedules(space_type, true, true, true, true, true, true, true)
3158
+
3159
+ # assign colors
3160
+ standard.space_type_apply_rendering_color(space_type)
3161
+
3162
+ # exend space type name to include the template. Consider this as well for load defs
3163
+ space_type.setName("#{space_type.name} - #{template}")
3164
+ runner.registerInfo("Added space type named #{space_type.name}")
3165
+ end
3166
+
3167
+ end
3168
+
3169
+ # add construction sets
3170
+ bldg_def_const_set = nil
3171
+ if create_construction_set
3172
+
3173
+ # Make the default construction set for the building
3174
+ is_residential = 'No' # default is nonresidential for building level
3175
+ bldg_def_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, is_residential)
3176
+ if bldg_def_const_set.is_initialized
3177
+ bldg_def_const_set = bldg_def_const_set.get
3178
+ runner.registerInfo("Added default construction set named #{bldg_def_const_set.name}")
3179
+ else
3180
+ runner.registerError('Could not create default construction set for the building.')
3181
+ return false
3182
+ end
3183
+
3184
+ # make residential construction set as unused resource
3185
+ if ['SmallHotel', 'LargeHotel', 'MidriseApartment', 'HighriseApartment'].include?(building_type)
3186
+ res_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, 'Yes')
3187
+ if res_const_set.is_initialized
3188
+ res_const_set = res_const_set.get
3189
+ res_const_set.setName("#{bldg_def_const_set.name} - Residential ")
3190
+ runner.registerInfo("Added residential construction set named #{res_const_set.name}")
3191
+ else
3192
+ runner.registerError('Could not create residential construction set for the building.')
3193
+ return false
3194
+ end
3195
+ end
3196
+
3197
+ end
3198
+
3199
+ # set_building_defaults
3200
+ if set_building_defaults
3201
+
3202
+ # identify default space type
3203
+ space_type_standards_info_hash = OsLib_HelperMethods.getSpaceTypeStandardsInformation(space_types_new)
3204
+ default_space_type = nil
3205
+ space_type_standards_info_hash.each do |space_type, standards_array|
3206
+ standards_space_type = standards_array[1]
3207
+ if default_space_type_name == standards_space_type
3208
+ default_space_type = space_type
3209
+ end
3210
+ end
3211
+
3212
+ # set default space type
3213
+ building = model.getBuilding
3214
+ if !default_space_type.nil?
3215
+ building.setSpaceType(default_space_type)
3216
+ runner.registerInfo("Setting default Space Type for building to #{building.spaceType.get.name}")
3217
+ end
3218
+
3219
+ # default construction
3220
+ if !bldg_def_const_set.nil?
3221
+ building.setDefaultConstructionSet(bldg_def_const_set)
3222
+ runner.registerInfo("Setting default Construction Set for building to #{building.defaultConstructionSet.get.name}")
3223
+ end
3224
+
3225
+ # set climate zone
3226
+ os_climate_zone = climate_zone.gsub('ASHRAE 169-2013-', '')
3227
+ # trim off letter from climate zone 7 or 8
3228
+ if (os_climate_zone[0] == '7') || (os_climate_zone[0] == '8')
3229
+ os_climate_zone = os_climate_zone[0]
3230
+ end
3231
+ climate_zone = model.getClimateZones.setClimateZone('ASHRAE', os_climate_zone)
3232
+ runner.registerInfo("Setting #{climate_zone.institution} Climate Zone to #{climate_zone.value}")
3233
+
3234
+ # set building type
3235
+ # use lookup_building_type so spaces like MediumOffice will map to Office (Supports baseline automation)
3236
+ building.setStandardsBuildingType(lookup_building_type)
3237
+ runner.registerInfo("Setting Standards Building Type to #{building.standardsBuildingType}")
3238
+
3239
+ # rename building if it is named "Building 1"
3240
+ if model.getBuilding.name.to_s == 'Building 1'
3241
+ model.getBuilding.setName("#{building_type} #{template} #{os_climate_zone}")
3242
+ runner.registerInfo("Renaming building to #{model.getBuilding.name}")
3243
+ end
3244
+
3245
+ end
3246
+
3247
+ # reporting final condition of model
3248
+ finishing_spaceTypes = model.getSpaceTypes
3249
+ finishing_constructionSets = model.getDefaultConstructionSets
3250
+ runner.registerFinalCondition("The building finished with #{finishing_spaceTypes.size} space types and #{finishing_constructionSets.size} construction sets.")
3251
+
3252
+ return true
3253
+ end
3254
+
817
3255
  end