Module: OpenstudioStandards::Geometry

Defined in:
lib/openstudio-standards/geometry/group.rb,
lib/openstudio-standards/geometry/create.rb,
lib/openstudio-standards/geometry/modify.rb,
lib/openstudio-standards/geometry/create_bar.rb,
lib/openstudio-standards/geometry/information.rb,
lib/openstudio-standards/geometry/create_shape.rb

Overview

This Module provides methods to create, modify, and get information about model geometry

Group collapse

Create collapse

Modify:SubSurface collapse

Modify:Model collapse

CreateBar collapse

Information:Calculations collapse

Information:Surface collapse

Information:Surfaces collapse

Information:SubSurface collapse

Information:Space collapse

Information:Spaces collapse

Information:ThermalZone collapse

Information:ThermalZones collapse

Information:Story collapse

Information:Model collapse

Create:Shape collapse

Class Method Details

.aspect_ratio(area, perimeter) ⇒ Double

calculate aspect ratio from area and perimeter

Parameters:

  • area (Double)

    area

  • perimeter (Double)

    perimeter

Returns:

  • (Double)

    aspect ratio



13
14
15
16
17
18
19
# File 'lib/openstudio-standards/geometry/information.rb', line 13

def self.aspect_ratio(area, perimeter)
  length = 0.25 * (perimeter + Math.sqrt((perimeter**2) - (16 * area)))
  width = 0.25 * (perimeter - Math.sqrt((perimeter**2) - (16 * area)))
  aspect_ratio = length / width

  return aspect_ratio
end

.bar_hash_setup_run(model, args, length, width, floor_height, center_of_footprint, space_types_hash, num_stories) ⇒ Boolean

give info messages bar hash for create_bar method

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • args (Hash)

    user arguments

  • length (Double)

    length of building in meters

  • width (Double)

    width of building in meters

  • floor_height (Double)

    floor height in meters

  • center_of_footprint (OpenStudio::Point3d)

    center of footprint

  • space_types_hash (Hash)

    space type hash

  • num_stories (Double)

    number of stories

Returns:

  • (Boolean)

    returns true if successful, false if not



969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 969

def self.bar_hash_setup_run(model, args, length, width, floor_height, center_of_footprint, space_types_hash, num_stories)
  # create envelope
  # populate bar_hash and create envelope with data from envelope_data_hash and user arguments
  bar_hash = {}
  bar_hash[:length] = length
  bar_hash[:width] = width
  bar_hash[:num_stories_below_grade] = args[:num_stories_below_grade]
  bar_hash[:num_stories_above_grade] = args[:num_stories_above_grade]
  bar_hash[:floor_height] = floor_height
  bar_hash[:center_of_footprint] = center_of_footprint
  bar_hash[:bar_division_method] = args[:bar_division_method]
  bar_hash[:story_multiplier_method] = args[:story_multiplier_method]
  bar_hash[:make_mid_story_surfaces_adiabatic] = args[:make_mid_story_surfaces_adiabatic]
  bar_hash[:space_types] = space_types_hash
  bar_hash[:building_wwr_n] = args[:wwr]
  bar_hash[:building_wwr_s] = args[:wwr]
  bar_hash[:building_wwr_e] = args[:wwr]
  bar_hash[:building_wwr_w] = args[:wwr]

  # round up non integer stoires to next integer
  num_stories_round_up = num_stories.ceil
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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")

  # party_walls_array to be used by orientation specific or fractional party wall values
  party_walls_array = [] # this is an array of arrays, where each entry is effective building story with array of directions

  if args[:party_wall_stories_north] + args[:party_wall_stories_south] + args[:party_wall_stories_east] + args[:party_wall_stories_west] > 0

    # loop through effective number of stories add orientation specific party walls per user arguments
    num_stories_round_up.times do |i|
      test_value = i + 1 - bar_hash[:num_stories_below_grade]

      array = []
      if args[:party_wall_stories_north] >= test_value
        array << 'north'
      end
      if args[:party_wall_stories_south] >= test_value
        array << 'south'
      end
      if args[:party_wall_stories_east] >= test_value
        array << 'east'
      end
      if args[:party_wall_stories_west] >= test_value
        array << 'west'
      end

      # populate party_wall_array for this story
      party_walls_array << array
    end
  end

  # calculate party walls if using party_wall_fraction method
  if args[:party_wall_fraction] > 0 && !party_walls_array.empty?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Both orientation and fractional party wall values arguments were populated, will ignore fractional party wall input')
  elsif args[:party_wall_fraction] > 0
    # orientation of long and short side of building will vary based on building rotation

    # full story ext wall area
    typical_length_facade_area = length * floor_height
    typical_width_facade_area = width * floor_height

    # top story ext wall area, may be partial story
    partial_story_multiplier = (1.0 - args[:num_stories_above_grade].ceil + args[:num_stories_above_grade])
    area_multiplier = partial_story_multiplier
    edge_multiplier = Math.sqrt(area_multiplier)
    top_story_length = length * edge_multiplier
    top_story_width = width * edge_multiplier
    top_story_length_facade_area = top_story_length * floor_height
    top_story_width_facade_area = top_story_width * floor_height

    total_exterior_wall_area = (2 * (length + width) * (args[:num_stories_above_grade].ceil - 1.0) * floor_height) + (2 * (top_story_length + top_story_width) * floor_height)
    target_party_wall_area = total_exterior_wall_area * args[:party_wall_fraction]

    width_counter = 0
    width_area = 0.0
    facade_area = typical_width_facade_area
    until (width_area + facade_area >= target_party_wall_area) || (width_counter == (args[:num_stories_above_grade].ceil * 2))
      # update facade area for top story
      if width_counter == args[:num_stories_above_grade].ceil - 1 || ((width_counter == (args[:num_stories_above_grade].ceil * 2)) - 1)
        facade_area = top_story_width_facade_area
      else
        facade_area = typical_width_facade_area
      end

      width_counter += 1
      width_area += facade_area

    end
    width_area_remainder = target_party_wall_area - width_area

    length_counter = 0
    length_area = 0.0
    facade_area = typical_length_facade_area
    until (length_area + facade_area >= target_party_wall_area) || (length_counter == args[:num_stories_above_grade].ceil * 2)
      # update facade area for top story
      if length_counter == args[:num_stories_above_grade].ceil - 1 || ((length_counter == args[:num_stories_above_grade].ceil * 2) - 1)
        facade_area = top_story_length_facade_area
      else
        facade_area = typical_length_facade_area
      end

      length_counter += 1
      length_area += facade_area
    end
    length_area_remainder = target_party_wall_area - length_area

    # get rotation and best fit to adjust orientation for fraction party wall
    rotation = args[:building_rotation] % 360.0 # should result in value between 0 and 360
    card_dir_array = [0.0, 90.0, 180.0, 270.0, 360.0]
    # reverse array to properly handle 45, 135, 225, and 315
    best_fit = card_dir_array.reverse.min_by { |x| (x.to_f - rotation).abs }

    if ![90.0, 270.0].include? best_fit
      width_card_dir = ['east', 'west']
      length_card_dir = ['north', 'south']
    else # if rotation is closest to 90 or 270 then reverse which orientation is used for length and width
      width_card_dir = ['north', 'south']
      length_card_dir = ['east', 'west']
    end

    # if dont' find enough on short sides
    if width_area_remainder <= typical_length_facade_area

      num_stories_round_up.times do |i|
        if i + 1 <= args[:num_stories_below_grade]
          party_walls_array << []
          next
        end
        if i + 1 - args[:num_stories_below_grade] <= width_counter
          if i + 1 - args[:num_stories_below_grade] <= width_counter - args[:num_stories_above_grade]
            party_walls_array << width_card_dir
          else
            party_walls_array << [width_card_dir.first]
          end
        else
          party_walls_array << []
        end
      end

    else
      # use long sides instead
      num_stories_round_up.times do |i|
        if i + 1 <= args[:num_stories_below_grade]
          party_walls_array << []
          next
        end
        if i + 1 - args[:num_stories_below_grade] <= length_counter
          if i + 1 - args[:num_stories_below_grade] <= length_counter - args[:num_stories_above_grade]
            party_walls_array << length_card_dir
          else
            party_walls_array << [length_card_dir.first]
          end
        else
          party_walls_array << []
        end
      end

    end
    # @todo currently won't go past making two opposing sets of walls party walls. Info and registerValue are after create_bar in measure.rb
  end

  # populate bar hash with story information
  bar_hash[:stories] = {}
  num_stories_round_up.times do |i|
    if party_walls_array.empty?
      party_walls = []
    else
      party_walls = party_walls_array[i]
    end

    # add below_partial_story
    if num_stories.ceil > num_stories && i == num_stories_round_up - 2
      below_partial_story = true
    else
      below_partial_story = false
    end

    # bottom_story_ground_exposed_floor and top_story_exterior_exposed_roof already setup as bool
    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] }
  end

  # create bar
  new_spaces = create_bar(model, bar_hash)

  # check expect roof and wall area
  target_footprint = bar_hash[:length] * bar_hash[:width]
  ground_floor_area = 0.0
  roof_area = 0.0
  new_spaces.each do |space|
    space.surfaces.each do |surface|
      if surface.surfaceType == 'Floor' && surface.outsideBoundaryCondition == 'Ground'
        ground_floor_area += surface.netArea
      elsif surface.surfaceType == 'RoofCeiling' && surface.outsideBoundaryCondition == 'Outdoors'
        roof_area += surface.netArea
      end
    end
  end
  # @todo extend to address when top and or bottom story are not exposed via argument
  if ground_floor_area > target_footprint + 0.001 || roof_area > target_footprint + 0.001
    # OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "Ground exposed floor or Roof area is larger than footprint, likely inter-floor surface matching and intersection error.")
    # return false

    # not providing adiabatic work around when top story is partial story.
    if args[:num_stories_above_grade].to_i != args[:num_stories_above_grade].ceil
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', 'Ground exposed floor or Roof area is larger than footprint, likely inter-floor surface matching and intersection error.')
      return false
    else
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', '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.')
      match_error = true
    end
  else
    match_error = false
  end

  # @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
  return true unless match_error

  # identify z value of top and bottom story
  bottom_story = nil
  top_story = nil
  new_spaces.each do |space|
    story = space.buildingStory.get
    nom_z = story.nominalZCoordinate.get
    if bottom_story.nil?
      bottom_story = nom_z
    elsif bottom_story > nom_z
      bottom_story = nom_z
    end
    if top_story.nil?
      top_story = nom_z
    elsif top_story < nom_z
      top_story = nom_z
    end
  end

  # change boundary condition and intersection as needed.
  new_spaces.each do |space|
    if space.buildingStory.get.nominalZCoordinate.get > bottom_story
      # change floors
      space.surfaces.each do |surface|
        next if !(surface.surfaceType == 'Floor' && surface.outsideBoundaryCondition == 'Ground')

        surface.setOutsideBoundaryCondition('Adiabatic')
      end
    end
    if space.buildingStory.get.nominalZCoordinate.get < top_story
      # change ceilings
      space.surfaces.each do |surface|
        next if !(surface.surfaceType == 'RoofCeiling' && surface.outsideBoundaryCondition == 'Outdoors')

        surface.setOutsideBoundaryCondition('Adiabatic')
      end
    end
  end
end

.bar_reduced_bounding_box(envelope_data_hash) ⇒ Hash

get length and width of rectangle matching bounding box aspect ratio will maintaining proper floor area

Parameters:

  • envelope_data_hash (Hash)

    Hash of envelope data

Returns:

  • (Hash)

    hash of bar length and width



387
388
389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 387

def self.bar_reduced_bounding_box(envelope_data_hash)
  bar = {}

  bounding_length = envelope_data_hash[:building_max_xyz][0] - envelope_data_hash[:building_min_xyz][0]
  bounding_width = envelope_data_hash[:building_max_xyz][1] - envelope_data_hash[:building_min_xyz][1]
  bounding_area = bounding_length * bounding_width
  footprint_area = envelope_data_hash[:building_floor_area] / (envelope_data_hash[:effective_num_stories_above_grade] + envelope_data_hash[:effective_num_stories_below_grade].to_f)
  area_multiplier = footprint_area / bounding_area
  edge_multiplier = Math.sqrt(area_multiplier)
  bar[:length] = bounding_length * edge_multiplier
  bar[:width] = bounding_width * edge_multiplier

  return bar
end

.bar_reduced_width(envelope_data_hash) ⇒ Hash

get length and width of rectangle matching longer of two edges, and reducing the other way until floor area matches

Parameters:

  • envelope_data_hash (Hash)

    Hash of envelope data

Returns:

  • (Hash)

    hash of bar length and width



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 406

def self.bar_reduced_width(envelope_data_hash)
  bar = {}

  bounding_length = envelope_data_hash[:building_max_xyz][0] - envelope_data_hash[:building_min_xyz][0]
  bounding_width = envelope_data_hash[:building_max_xyz][1] - envelope_data_hash[:building_min_xyz][1]
  footprint_area = envelope_data_hash[:building_floor_area] / (envelope_data_hash[:effective_num_stories_above_grade] + envelope_data_hash[:effective_num_stories_below_grade].to_f)

  if bounding_length >= bounding_width
    bar[:length] = bounding_length
    bar[:width] = footprint_area / bounding_length
  else
    bar[:width] = bounding_width
    bar[:length] = footprint_area / bounding_width
  end

  return bar
end

.bar_stretched(envelope_data_hash) ⇒ Hash

get length and width of rectangle by stretching it until both floor area and exterior wall area or perimeter match

Parameters:

  • envelope_data_hash (Hash)

    Hash of envelope data including

Returns:

  • (Hash)

    hash of bar length and width



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 428

def self.bar_stretched(envelope_data_hash)
  bar = {}

  bounding_length = envelope_data_hash[:building_max_xyz][0] - envelope_data_hash[:building_min_xyz][0]
  bounding_width = envelope_data_hash[:building_max_xyz][1] - envelope_data_hash[:building_min_xyz][1]
  a = envelope_data_hash[:building_floor_area] / (envelope_data_hash[:effective_num_stories_above_grade] + envelope_data_hash[:effective_num_stories_below_grade].to_f)
  p = envelope_data_hash[:building_perimeter]

  if bounding_length >= bounding_width
    bar[:length] = 0.25 * (p + Math.sqrt((p**2) - (16 * a)))
    bar[:width] = 0.25 * (p - Math.sqrt((p**2) - (16 * a)))
  else
    bar[:length] = 0.25 * (p - Math.sqrt((p**2) - (16 * a)))
    bar[:width] = 0.25 * (p + Math.sqrt((p**2) - (16 * a)))
  end

  return bar
end

.building_form_defaults(building_type) ⇒ Hash

Building Form Defaults from Table 4.2 in Achieving the 30% Goal: Energy and Cost Savings Analysis of ASHRAE Standard 90.1-2010 aspect ratio for NA replaced with floor area to perimeter ratio from prototype model currently no reason to split apart doe and deer inputs here

Parameters:

  • building_type (String)

    standard building type

Returns:

  • (Hash)

    Hash of aspect_ratio, wwr, typical_story, and perim_mult



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 13

def self.building_form_defaults(building_type)
  hash = {}

  # DOE Prototypes

  # calculate aspect ratios not represented on Table 4.2
  primary_footprint = 73958.0
  primary_p = 619.0 # wrote measure using calculate_perimeter method in os_lib_geometry
  primary_ns_ew_ratio = 2.829268293 # estimated from ratio of ns/ew total wall area
  primary_width = Math.sqrt(primary_footprint / primary_ns_ew_ratio)
  primary_p_min = 2 * (primary_width + (primary_width / primary_footprint))
  primary_p_mult = primary_p / primary_p_min

  secondary_footprint = 210887.0 / 2.0 # floor area divided by area instead of true footprint 128112.0)
  secondary_p = 708.0 # wrote measure using calculate_perimeter method in os_lib_geometry
  secondary_ns_ew_ratio = 2.069230769 # estimated from ratio of ns/ew total wall area
  secondary_width = Math.sqrt(secondary_footprint / secondary_ns_ew_ratio)
  secondary_p_min = 2 * (secondary_width + (secondary_width / secondary_footprint))
  secondary_p_mult = secondary_p / secondary_p_min

  outpatient_footprint = 40946.0 / 3.0 # floor area divided by area instead of true footprint 17872.0)
  outpatient_p = 537.0 # wrote measure using calculate_perimeter method in os_lib_geometry
  outpatient_ns_ew_ratio = 1.56448737 # estimated from ratio of ns/ew total wall area
  outpatient_width = Math.sqrt(outpatient_footprint / outpatient_ns_ew_ratio)
  outpatient_p_min = 2 * (outpatient_width + (outpatient_footprint / outpatient_width))
  outpatient_p_mult = outpatient_p / outpatient_p_min

  # primary_aspet_ratio = OpenstudioStandards::Geometry.aspect_ratio(73958.0, 2060.0)
  # secondary_aspet_ratio = OpenstudioStandards::Geometry.aspect_ratio(128112.0, 2447.0)
  # outpatient_aspet_ratio = OpenstudioStandards::Geometry.aspect_ratio(14782.0, 588.0)
  supermarket_a = 45001.0
  supermarket_p = 866.0
  supermarket_wwr = 1880.0 / (supermarket_p * 20.0)
  supermarket_aspect_ratio = OpenstudioStandards::Geometry.aspect_ratio(supermarket_a, supermarket_p)

  hash['SmallOffice'] = { aspect_ratio: 1.5, wwr: 0.15, typical_story: 10.0, perim_mult: 1.0 }
  hash['MediumOffice'] = { aspect_ratio: 1.5, wwr: 0.33, typical_story: 13.0, perim_mult: 1.0 }
  hash['LargeOffice'] = { aspect_ratio: 1.5, wwr: 0.15, typical_story: 13.0, perim_mult: 1.0 }
  hash['RetailStandalone'] = { aspect_ratio: 1.28, wwr: 0.07, typical_story: 20.0, perim_mult: 1.0 }
  hash['RetailStripmall'] = { aspect_ratio: 4.0, wwr: 0.11, typical_story: 17.0, perim_mult: 1.0 }
  hash['PrimarySchool'] = { aspect_ratio: primary_ns_ew_ratio.round(1), wwr: 0.35, typical_story: 13.0, perim_mult: primary_p_mult.round(3) }
  hash['SecondarySchool'] = { aspect_ratio: secondary_ns_ew_ratio.round(1), wwr: 0.33, typical_story: 13.0, perim_mult: secondary_p_mult.round(3) }
  hash['Outpatient'] = { aspect_ratio: outpatient_ns_ew_ratio.round(1), wwr: 0.20, typical_story: 10.0, perim_mult: outpatient_p_mult.round(3) }
  hash['Hospital'] = { aspect_ratio: 1.33, wwr: 0.16, typical_story: 14.0, perim_mult: 1.0 }
  hash['SmallHotel'] = { aspect_ratio: 3.0, wwr: 0.11, typical_story: 9.0, first_story: 11.0, perim_mult: 1.0 }
  hash['LargeHotel'] = { aspect_ratio: 5.1, wwr: 0.27, typical_story: 10.0, first_story: 13.0, perim_mult: 1.0 }

  # code in get_space_types_from_building_type is used to override building wwr with space type specific wwr
  hash['Warehouse'] = { aspect_ratio: 2.2, wwr: 0.0, typical_story: 28.0, perim_mult: 1.0 }

  hash['FullServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.18, typical_story: 10.0, perim_mult: 1.0 }
  hash['QuickServiceRestaurant'] = { aspect_ratio: 1.0, wwr: 0.18, typical_story: 10.0, perim_mult: 1.0 }
  hash['MidriseApartment'] = { aspect_ratio: 2.75, wwr: 0.15, typical_story: 10.0, perim_mult: 1.0 }
  hash['HighriseApartment'] = { aspect_ratio: 2.75, wwr: 0.15, typical_story: 10.0, perim_mult: 1.0 }
  # SuperMarket inputs come from prototype model
  hash['SuperMarket'] = { aspect_ratio: supermarket_aspect_ratio.round(1), wwr: supermarket_wwr.round(2), typical_story: 20.0, perim_mult: 1.0 }

  # Add Laboratory and Data Centers
  hash['Laboratory'] = { aspect_ratio: 1.33, wwr: 0.12, typical_story: 10.0, perim_mult: 1.0 }
  hash['LargeDataCenterLowITE'] = { aspect_ratio: 1.67, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }
  hash['LargeDataCenterHighITE'] = { aspect_ratio: 1.67, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }
  hash['SmallDataCenterLowITE'] = { aspect_ratio: 1.5, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }
  hash['SmallDataCenterHighITE'] = { aspect_ratio: 1.5, wwr: 0.0, typical_story: 14.0, perim_mult: 1.0 }

  # Add Courthouse and Education
  hash['Courthouse'] = { aspect_ratio: 2.06, wwr: 0.18, typical_story: 16.0, perim_mult: 1.0 }
  hash['College'] = { aspect_ratio: 2.5, wwr: 0.037, typical_story: 13.0, perim_mult: 1.0 }

  # DEER Prototypes
  hash['Asm'] = { aspect_ratio: 1.0, wwr: 0.19, typical_story: 15.0 }
  hash['ECC'] = { aspect_ratio: 4.0, wwr: 0.25, typical_story: 13.0 }
  hash['EPr'] = { aspect_ratio: 2.0, wwr: 0.16, typical_story: 12.0 }
  hash['ERC'] = { aspect_ratio: 1.7, wwr: 0.03, typical_story: 12.0 }
  hash['ESe'] = { aspect_ratio: 1.0, wwr: 0.15, typical_story: 13.0 }
  hash['EUn'] = { aspect_ratio: 2.5, wwr: 0.3, typical_story: 14.0 }
  hash['Gro'] = { aspect_ratio: 1.0, wwr: 0.07, typical_story: 25.0 }
  hash['Hsp'] = { aspect_ratio: 1.5, wwr: 0.11, typical_story: 13.0 }
  hash['Htl'] = { aspect_ratio: 3.0, wwr: 0.23, typical_story: 9.5, first_story: 12.0 }
  hash['MBT'] = { aspect_ratio: 10.7, wwr: 0.12, typical_story: 15.0 }
  hash['MFm'] = { aspect_ratio: 1.4, wwr: 0.24, typical_story: 9.5 }
  hash['MLI'] = { aspect_ratio: 1.0, wwr: 0.01, typical_story: 35.0 }
  hash['Mtl'] = { aspect_ratio: 5.1, wwr: 0.41, typical_story: 9.0 }
  hash['Nrs'] = { aspect_ratio: 10.3, wwr: 0.2, typical_story: 13.0 }
  hash['OfL'] = { aspect_ratio: 1.5, wwr: 0.33, typical_story: 12.0 }
  hash['OfS'] = { aspect_ratio: 1.5, wwr: 0.33, typical_story: 12.0 }
  hash['RFF'] = { aspect_ratio: 1.0, wwr: 0.25, typical_story: 13.0 }
  hash['RSD'] = { aspect_ratio: 1.0, wwr: 0.13, typical_story: 13.0 }
  hash['Rt3'] = { aspect_ratio: 1.0, wwr: 0.02, typical_story: 20.8 }
  hash['RtL'] = { aspect_ratio: 1.0, wwr: 0.03, typical_story: 20.5 }
  hash['RtS'] = { aspect_ratio: 1.0, wwr: 0.13, typical_story: 12.0 }
  hash['SCn'] = { aspect_ratio: 1.0, wwr: 0.01, typical_story: 48.0 }
  hash['SUn'] = { aspect_ratio: 1.0, wwr: 0.01, typical_story: 48.0 }
  hash['WRf'] = { aspect_ratio: 1.6, wwr: 0.0, typical_story: 32.0 }

  return hash[building_type]
end

.building_story_get_exterior_wall_perimeter(story, multiplier_adjustment: nil, exterior_boundary_conditions: ['Outdoors', 'Ground'], bounding_box: nil) ⇒ Object

TODO:

this doesn’t catch walls that are split that sit above floor surfaces that are not (e.g. main corridoor in secondary school model)

TODO:

also odd with multi-height spaces

Calculate the story exterior wall perimeter. Selected story should have above grade walls. If not perimeter may return zero.

Parameters:

  • story (OpenStudio::Model::BuildingStory)
  • multiplier_adjustment (Double) (defaults to: nil)

    Adjust the calculated perimeter to account for zone multipliers. The value represents the story_multiplier which reduces the adjustment by that factor over the full zone multiplier.

  • exterior_boundary_conditions (Array<String>) (defaults to: ['Outdoors', 'Ground'])

    Array of strings of exterior boundary conditions.

  • bounding_box (OpenStudio::BoundingBox) (defaults to: nil)

    bounding box to determine which spaces are included



695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
# File 'lib/openstudio-standards/geometry/information.rb', line 695

def self.building_story_get_exterior_wall_perimeter(story,
                                                    multiplier_adjustment: nil,
                                                    exterior_boundary_conditions: ['Outdoors', 'Ground'],
                                                    bounding_box: nil)
  perimeter = 0
  party_walls = []
  story.spaces.each do |space|
    # counter to use later
    edge_hash = {}
    edge_counter = 0
    space.surfaces.each do |surface|
      # get vertices
      vertex_hash = {}
      vertex_counter = 0
      surface.vertices.each do |vertex|
        vertex_counter += 1
        vertex_hash[vertex_counter] = [vertex.x, vertex.y, vertex.z]
      end
      # make edges
      counter = 0
      vertex_hash.each do |k, v|
        edge_counter += 1
        counter += 1
        if vertex_hash.size == counter
          # different code for wrap around vertex
          edge_hash[edge_counter] = [v, vertex_hash[1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
        else
          edge_hash[edge_counter] = [v, vertex_hash[counter + 1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
        end
      end
    end

    # check edges for matches (need opposite vertices and proper boundary conditions)
    edge_hash.each do |k1, v1|
      # apply to any floor boundary condition. This supports used in floors above basements
      next if v1[4] != 'Floor'

      edge_hash.each do |k2, v2|
        test_boundary_cond = false
        next if !exterior_boundary_conditions.include?(v2[3]) # method arg takes multiple conditions
        next if v2[4] != 'Wall'

        # see if edges have same geometry

        # found cases where the two lines below removed edges and resulted in lower than actual perimeter. Added new code with tolerance.
        # next if not v1[0] == v2[1] # next if not same geometry reversed
        # next if not v1[1] == v2[0]

        # these are three item array's add in tolerance for each array entry
        tolerance = 0.0001
        test_a = true
        test_b = true
        3.times.each do |i|
          if (v1[0][i] - v2[1][i]).abs > tolerance
            test_a = false
          end
          if (v1[1][i] - v2[0][i]).abs > tolerance
            test_b = false
          end
        end

        next if test_a != true
        next if test_b != true

        # edge_bounding_box = OpenStudio::BoundingBox.new
        # edge_bounding_box.addPoints(space.transformation() * v2[2].vertices)
        # if not edge_bounding_box.intersects(bounding_box) doesn't seem to work reliably, writing custom code to check

        point_one = OpenStudio::Point3d.new(v2[0][0], v2[0][1], v2[0][2])
        point_one = (space.transformation * point_one)
        point_two = OpenStudio::Point3d.new(v2[1][0], v2[1][1], v2[1][2])
        point_two = (space.transformation * point_two)

        if !bounding_box.nil? && (v2[3] == 'Adiabatic')

          on_bounding_box = false
          if ((bounding_box.minX.to_f - point_one.x).abs < tolerance) && ((bounding_box.minX.to_f - point_two.x).abs < tolerance)
            on_bounding_box = true
          elsif ((bounding_box.maxX.to_f - point_one.x).abs < tolerance) && ((bounding_box.maxX.to_f - point_two.x).abs < tolerance)
            on_bounding_box = true
          elsif ((bounding_box.minY.to_f - point_one.y).abs < tolerance) && ((bounding_box.minY.to_f - point_two.y).abs < tolerance)
            on_bounding_box = true
          elsif ((bounding_box.maxY.to_f - point_one.y).abs < tolerance) && ((bounding_box.maxY.to_f - point_two.y).abs < tolerance)
            on_bounding_box = true
          end

          # if not edge_bounding_box.intersects(bounding_box) doesn't seem to work reliably, writing custom code to check
          # todo - this is basic check for adiabatic party walls and won't catch all situations. Can be made more robust in the future
          if on_bounding_box == true
            length = OpenStudio::Vector3d.new(point_one - point_two).length
            party_walls << v2[2]
            length_ip_display = OpenStudio.convert(length, 'm', 'ft').get.round(2)
            OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Information', " * #{v2[2].name} has an adiabatic boundary condition and sits in plane with the building bounding box. Adding #{length_ip_display} (ft) to perimeter length of #{story.name} for this surface, assuming it is a party wall.")
          elsif space.multiplier == 1
            length = OpenStudio::Vector3d.new(point_one - point_two).length
            party_walls << v2[2]
            length_ip_display = OpenStudio.convert(length, 'm', 'ft').get.round(2)
            OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Information', " * #{v2[2].name} has an adiabatic boundary condition and is in a zone with a multiplier of 1. Adding #{length_ip_display} (ft) to perimeter length of #{story.name} for this surface, assuming it is a party wall.")
          else
            length = 0
          end

        else
          length = OpenStudio::Vector3d.new(point_one - point_two).length
        end

        if multiplier_adjustment.nil?
          perimeter += length
        else
          # adjust for multiplier
          non_story_multiplier = space.multiplier / multiplier_adjustment.to_f
          perimeter += length * non_story_multiplier
        end
      end
    end
  end

  return { perimeter: perimeter, party_walls: party_walls }
end

.building_story_get_floor_multiplier(building_story) ⇒ Integer

Checks all spaces on this story that are part of the total floor area to see if they have the same multiplier. If they do, assume that the multipliers are being used as a floor multiplier.

Parameters:

  • building_story (OpenStudio::Model::BuildingStory)

    OpenStudio BuildingStory object

Returns:

  • (Integer)

    return the floor multiplier for this story, returning 1 if no floor multiplier.



820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
# File 'lib/openstudio-standards/geometry/information.rb', line 820

def self.building_story_get_floor_multiplier(building_story)
  floor_multiplier = 1

  # Determine the multipliers for all spaces
  multipliers = []
  building_story.spaces.each do |space|
    # Ignore spaces that aren't part of the total floor area
    next unless space.partofTotalFloorArea

    multipliers << space.multiplier
  end

  # If there are no spaces on this story, assume
  # a multiplier of 1
  if multipliers.empty?
    return floor_multiplier
  end

  # Calculate the average multiplier and
  # then convert to integer.
  avg_multiplier = (multipliers.inject { |a, e| a + e }.to_f / multipliers.size).to_i

  # If the multiplier is greater than 1, report this
  if avg_multiplier > 1
    floor_multiplier = avg_multiplier
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Information', "Story #{building_story.name} has a multiplier of #{floor_multiplier}.")
  end

  return floor_multiplier
end

.building_story_get_minimum_height(building_story) ⇒ Double

Gets the minimum height of the building story. This is considered to be the minimum z value of any vertex of any surface of any space on the story, with the exception of plenum spaces.

Parameters:

  • building_story (OpenStudio::Model::BuildingStory)

    OpenStudio BuildingStory object

Returns:

  • (Double)

    the minimum height in meters



856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
# File 'lib/openstudio-standards/geometry/information.rb', line 856

def self.building_story_get_minimum_height(building_story)
  z_heights = []
  building_story.spaces.each do |space|
    # Skip plenum spaces
    next if OpenstudioStandards::Space.space_plenum?(space)

    # Get the z value of the space, which
    # vertices in space surfaces are relative to.
    z_origin = space.zOrigin

    # loop through space surfaces to find min z value
    space.surfaces.each do |surface|
      surface.vertices.each do |vertex|
        z_heights << (vertex.z + z_origin)
      end
    end
  end

  # Error if no z heights were found
  z = 999.9
  if z_heights.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Information', "For #{building_story.name} could not find the minimum_z_value, which means the story has no spaces assigned or the spaces have no surfaces.")
  else
    z = z_heights.min
  end

  return z
end

.building_story_get_thermal_zones(building_story) ⇒ Array<OpenStudio::Model::ThermalZone>

Get an array of OpenStudio ThermalZone objects for an OpenStudio BuildingStory

Parameters:

  • building_story (OpenStudio::Model::BuildingStory)

    OpenStudio BuildingStory object

Returns:

  • (Array<OpenStudio::Model::ThermalZone>)

    Array of OpenStudio ThermalZone objects, empty array if none



889
890
891
892
893
894
895
896
897
# File 'lib/openstudio-standards/geometry/information.rb', line 889

def self.building_story_get_thermal_zones(building_story)
  zones = []
  building_story.spaces.sort.each do |space|
    zones << space.thermalZone.get if space.thermalZone.is_initialized
  end
  zones = zones.uniq

  return zones
end

.create_bar(model, bar_hash) ⇒ Array<OpenStudio::Model::Space>

create_bar creates spaces based on a set of geometric characteristics

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • bar_hash (Hash)

    A hash object of bar characteristics

Returns:

  • (Array<OpenStudio::Model::Space>)

    An array of OpenStudio Space objects



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 452

def self.create_bar(model, bar_hash)
  # make custom story hash when number of stories below grade > 0
  # @todo update this so have option basements are not below 0? (useful for simplifying existing model and maintaining z position relative to site shading)
  story_hash = {}
  eff_below = bar_hash[:num_stories_below_grade]
  eff_above = bar_hash[:num_stories_above_grade]
  footprint_origin_point = bar_hash[:center_of_footprint]
  typical_story_height = bar_hash[:floor_height]

  # warn about site shading
  if !model.getSite.shadingSurfaceGroups.empty?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'The model has one or more site shading surfaces. New geometry may not be positioned where expected, it will be centered over the center of the original geometry.')
  end

  # flatten story_hash out to individual stories included in building area
  stories_flat = []
  stories_flat_counter = 0
  bar_hash[:stories].each_with_index do |(k, v), i|
    # k is invalid in some cases, old story object that has been removed, should be from low to high including basement
    # skip if source story insn't included in building area
    if v[:story_included_in_building_area].nil? || (v[:story_included_in_building_area] == true)

      # add to counter
      stories_flat_counter += v[:story_min_multiplier]

      flat_hash = {}
      flat_hash[:story_party_walls] = v[:story_party_walls]
      flat_hash[:below_partial_story] = v[:below_partial_story]
      flat_hash[:bottom_story_ground_exposed_floor] = v[:bottom_story_ground_exposed_floor]
      flat_hash[:top_story_exterior_exposed_roof] = v[:top_story_exterior_exposed_roof]
      if i < eff_below
        flat_hash[:story_type] = 'b'
        flat_hash[:multiplier] = 1
      elsif i == eff_below
        flat_hash[:story_type] = 'ground'
        flat_hash[:multiplier] = 1
      elsif stories_flat_counter == eff_below + eff_above.ceil
        flat_hash[:story_type] = 'top'
        flat_hash[:multiplier] = 1
      else
        flat_hash[:story_type] = 'mid'
        flat_hash[:multiplier] = v[:story_min_multiplier]
      end

      compare_hash = {}
      if !stories_flat.empty?
        stories_flat.last.each { |s, m| compare_hash[s] = flat_hash[s] if flat_hash[s] != m }
      end
      if (bar_hash[:story_multiplier_method] != 'None' && stories_flat.last == flat_hash) || (bar_hash[:story_multiplier_method] != 'None' && compare_hash.size == 1 && compare_hash.include?(:multiplier))
        stories_flat.last[:multiplier] += v[:story_min_multiplier]
      else
        stories_flat << flat_hash
      end
    end
  end

  if bar_hash[:num_stories_below_grade] > 0

    # add in below grade levels (may want to add below grade multipliers at some point if we start running deep basements)
    eff_below.times do |i|
      story_hash["B#{i + 1}"] = { space_origin_z: footprint_origin_point.z - (typical_story_height * (i + 1)), space_height: typical_story_height, multiplier: 1 }
    end
  end

  # add in above grade levels
  if eff_above > 2
    story_hash['ground'] = { space_origin_z: footprint_origin_point.z, space_height: typical_story_height, multiplier: 1 }

    footprint_counter = 0
    effective_stories_counter = 1
    stories_flat.each do |hash|
      next if hash[:story_type] != 'mid'

      if footprint_counter == 0
        string = 'mid'
      else
        string = "mid#{footprint_counter + 1}"
      end
      story_hash[string] = { space_origin_z: footprint_origin_point.z + (typical_story_height * effective_stories_counter) + (typical_story_height * (hash[:multiplier] - 1) / 2.0), space_height: typical_story_height, multiplier: hash[:multiplier] }
      footprint_counter += 1
      effective_stories_counter += hash[:multiplier]
    end

    story_hash['top'] = { space_origin_z: footprint_origin_point.z + (typical_story_height * (eff_above.ceil - 1)), space_height: typical_story_height, multiplier: 1 }
  elsif eff_above > 1
    story_hash['ground'] = { space_origin_z: footprint_origin_point.z, space_height: typical_story_height, multiplier: 1 }
    story_hash['top'] = { space_origin_z: footprint_origin_point.z + (typical_story_height * (eff_above.ceil - 1)), space_height: typical_story_height, multiplier: 1 }
  else # one story only
    story_hash['ground'] = { space_origin_z: footprint_origin_point.z, space_height: typical_story_height, multiplier: 1 }
  end

  # create footprints
  if bar_hash[:bar_division_method] == 'Multiple Space Types - Simple Sliced'
    footprints = []
    story_hash.size.times do |i|
      # adjust size of bar of top story is not a full story
      if i + 1 == story_hash.size
        area_multiplier = (1.0 - bar_hash[:num_stories_above_grade].ceil + bar_hash[:num_stories_above_grade])
        edge_multiplier = Math.sqrt(area_multiplier)
        length = bar_hash[:length] * edge_multiplier
        width = bar_hash[:width] * edge_multiplier
      else
        length = bar_hash[:length]
        width = bar_hash[:width]
      end
      footprints << OpenstudioStandards::Geometry.create_sliced_bar_simple_polygons(bar_hash[:space_types], length, width, bar_hash[:center_of_footprint])
    end

  elsif bar_hash[:bar_division_method] == 'Multiple Space Types - Individual Stories Sliced'

    # update story_hash for partial_story_above
    story_hash.each_with_index do |(k, v), i|
      # adjust size of bar of top story is not a full story
      if i + 1 == story_hash.size
        story_hash[k][:partial_story_multiplier] = (1.0 - bar_hash[:num_stories_above_grade].ceil + bar_hash[:num_stories_above_grade])
      end
    end

    footprints = OpenstudioStandards::Geometry.create_sliced_bar_multi_polygons(bar_hash[:space_types], bar_hash[:length], bar_hash[:width], bar_hash[:center_of_footprint], story_hash)

  else
    footprints = []
    story_hash.size.times do |i|
      # adjust size of bar of top story is not a full story
      if i + 1 == story_hash.size
        area_multiplier = (1.0 - bar_hash[:num_stories_above_grade].ceil + bar_hash[:num_stories_above_grade])
        edge_multiplier = Math.sqrt(area_multiplier)
        length = bar_hash[:length] * edge_multiplier
        width = bar_hash[:width] * edge_multiplier
      else
        length = bar_hash[:length]
        width = bar_hash[:width]
      end
      # perimeter defaults to 15 ft
      footprints << OpenstudioStandards::Geometry.create_core_and_perimeter_polygons(length, width, bar_hash[:center_of_footprint])
    end

    # set primary space type to building default space type
    space_types = bar_hash[:space_types].sort_by { |k, v| v[:floor_area] }
    if space_types.last.first.class.to_s == 'OpenStudio::Model::SpaceType'
      model.getBuilding.setSpaceType(space_types.last.first)
    end

  end

  # make spaces from polygons
  new_spaces = OpenstudioStandards::Geometry.create_spaces_from_polygons(model, footprints, bar_hash[:floor_height], bar_hash[:num_stories], bar_hash[:center_of_footprint], story_hash)

  # put all of the spaces in the model into a vector for intersection and surface matching
  spaces = OpenStudio::Model::SpaceVector.new
  model.getSpaces.sort.each do |space|
    spaces << space
  end

  # flag for intersection and matching type
  diagnostic_intersect = true

  # only intersect if make_mid_story_surfaces_adiabatic false
  if diagnostic_intersect

    model.getPlanarSurfaces.sort.each do |surface|
      array = []
      vertices = surface.vertices
      fixed = false
      vertices.each do |vertex|
        next if fixed

        if array.include?(vertex)
          # create a new set of vertices
          new_vertices = OpenStudio::Point3dVector.new
          array_b = []
          surface.vertices.each do |vertex_b|
            next if array_b.include?(vertex_b)

            new_vertices << vertex_b
            array_b << vertex_b
          end
          surface.setVertices(new_vertices)
          num_removed = vertices.size - surface.vertices.size
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{surface.name} has duplicate vertices. Started with #{vertices.size} vertices, removed #{num_removed}.")
          fixed = true
        else
          array << vertex
        end
      end
    end

    # remove collinear points in a surface
    model.getPlanarSurfaces.sort.each do |surface|
      new_vertices = OpenStudio.removeCollinear(surface.vertices)
      starting_count = surface.vertices.size
      final_count = new_vertices.size
      if final_count < starting_count
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "Removing #{starting_count - final_count} collinear vertices from #{surface.name}.")
        surface.setVertices(new_vertices)
      end
    end

    # remove duplicate surfaces in a space (should be done after remove duplicate and collinear points)
    model.getSpaces.sort.each do |space|
      # secondary array to compare against
      surfaces_b = space.surfaces.sort

      space.surfaces.sort.each do |surface_a|
        # delete from secondary array
        surfaces_b.delete(surface_a)

        surfaces_b.each do |surface_b|
          next if surface_a == surface_b # dont' test against same surface

          if surface_a.equalVertices(surface_b)
            OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{surface_a.name} and #{surface_b.name} in #{space.name} have duplicate geometry, removing #{surface_b.name}.")
            surface_b.remove
          elsif surface_a.reverseEqualVertices(surface_b)
            # @todo add logic to determine which face naormal is reversed and which is correct
            OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{surface_a.name} and #{surface_b.name} in #{space.name} have reversed geometry, removing #{surface_b.name}.")
            surface_b.remove
          end
        end
      end
    end

    if (bar_hash[:make_mid_story_surfaces_adiabatic])
      # elsif bar_hash[:double_loaded_corridor] # only intersect spaces in each story, not between wtory
      model.getBuilding.buildingStories.sort.each do |story|
        # intersect and surface match two pair by pair
        spaces_b = story.spaces.sort
        # looping through vector of each space
        story.spaces.sort.each do |space_a|
          spaces_b.delete(space_a)
          spaces_b.each do |space_b|
            spaces_temp = OpenStudio::Model::SpaceVector.new
            spaces_temp << space_a
            spaces_temp << space_b

            # intersect and sort
            OpenStudio::Model.intersectSurfaces(spaces_temp)
            OpenStudio::Model.matchSurfaces(spaces_temp)
          end
        end
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Intersecting and matching surfaces in story #{story.name}, this will create additional geometry.")
      end
    else
      # intersect and surface match two pair by pair
      spaces_b = model.getSpaces.sort
      # looping through vector of each space
      model.getSpaces.sort.each do |space_a|
        spaces_b.delete(space_a)
        spaces_b.each do |space_b|
          # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Intersecting and matching surfaces between #{space_a.name} and #{space.name}")
          spaces_temp = OpenStudio::Model::SpaceVector.new
          spaces_temp << space_a
          spaces_temp << space_b
          # intersect and sort
          OpenStudio::Model.intersectSurfaces(spaces_temp)
          OpenStudio::Model.matchSurfaces(spaces_temp)
        end
      end
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Intersecting and matching surfaces in model, this will create additional geometry.')
    end
  else
    if (bar_hash[:make_mid_story_surfaces_adiabatic])
      # elsif bar_hash[:double_loaded_corridor] # only intersect spaces in each story, not between wtory
      model.getBuilding.buildingStories.sort.each do |story|
        story_spaces = OpenStudio::Model::SpaceVector.new
        story.spaces.sort.each do |space|
          story_spaces << space
        end

        # intersect and sort
        OpenStudio::Model.intersectSurfaces(story_spaces)
        OpenStudio::Model.matchSurfaces(story_spaces)
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Intersecting and matching surfaces in story #{story.name}, this will create additional geometry.")
      end
    else
      # intersect surfaces
      # (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)
      intersect_surfaces = true
      if intersect_surfaces
        OpenStudio::Model.intersectSurfaces(spaces)
        OpenStudio::Model.matchSurfaces(spaces)
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Intersecting and matching surfaces in model, this will create additional geometry.')
      end
    end
  end

  # set boundary conditions if not already set when geometry was created
  # @todo update this to use space original z value vs. story name
  if bar_hash[:num_stories_below_grade] > 0
    model.getBuildingStorys.sort.each do |story|
      next if !story.name.to_s.include?('Story B')

      story.spaces.sort.each do |space|
        next if !new_spaces.include?(space)

        space.surfaces.sort.each do |surface|
          next if surface.surfaceType != 'Wall'
          next if surface.outsideBoundaryCondition != 'Outdoors'

          surface.setOutsideBoundaryCondition('Ground')
        end
      end
    end
  end

  # set wall boundary condtions to adiabatic if using make_mid_story_surfaces_adiabatic prior to windows being made
  if bar_hash[:make_mid_story_surfaces_adiabatic]

    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Finding non-exterior walls and setting boundary condition to adiabatic')

    # need to organize by story incase top story is partial story
    # should also be only for a single bar
    story_bounding = {}
    missed_match_count = 0

    # gather new spaces by story
    new_spaces.each do |space|
      story = space.buildingStory.get
      if story_bounding.key?(story)
        story_bounding[story][:spaces] << space
      else
        story_bounding[story] = { spaces: [space] }
      end
    end

    # get bounding box for each story
    story_bounding.each do |story, v|
      # get bounding_box
      bounding_box = OpenStudio::BoundingBox.new
      v[:spaces].each do |space|
        space.surfaces.each do |space_surface|
          bounding_box.addPoints(space.transformation * space_surface.vertices)
        end
      end
      min_x = bounding_box.minX.get
      min_y = bounding_box.minY.get
      max_x = bounding_box.maxX.get
      max_y = bounding_box.maxY.get
      ext_wall_toll = 0.01

      # check surfaces again against min/max and change to adiabatic if not fully on one min or max x or y
      # todo - may need to look at aidiabiatc constructions in downstream measure. Some may be exterior party wall others may be interior walls
      v[:spaces].each do |space|
        space.surfaces.each do |space_surface|
          next if space_surface.surfaceType != 'Wall'
          next if space_surface.outsideBoundaryCondition == 'Surface' # if if found a match leave it alone, don't change to adiabiatc

          surface_bounding_box = OpenStudio::BoundingBox.new
          surface_bounding_box.addPoints(space.transformation * space_surface.vertices)
          surface_on_outside = false
          # check xmin
          if (surface_bounding_box.minX.get - min_x).abs < ext_wall_toll && (surface_bounding_box.maxX.get - min_x).abs < ext_wall_toll then surface_on_outside = true end
          # check xmax
          if (surface_bounding_box.minX.get - max_x).abs < ext_wall_toll && (surface_bounding_box.maxX.get - max_x).abs < ext_wall_toll then surface_on_outside = true end
          # check ymin
          if (surface_bounding_box.minY.get - min_y).abs < ext_wall_toll && (surface_bounding_box.maxY.get - min_y).abs < ext_wall_toll then surface_on_outside = true end
          # check ymax
          if (surface_bounding_box.minY.get - max_y).abs < ext_wall_toll && (surface_bounding_box.maxY.get - max_y).abs < ext_wall_toll then surface_on_outside = true end

          # change if not exterior
          if !surface_on_outside
            space_surface.setOutsideBoundaryCondition('Adiabatic')
            missed_match_count += 1
          end
        end
      end
    end

    if missed_match_count > 0
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "#{missed_match_count} surfaces that were exterior appear to be interior walls and had boundary condition chagned to adiabiatic.")
    end
  end

  # sort stories (by name for now but need better way)
  sorted_stories = {}
  new_spaces.each do |space|
    next if !space.buildingStory.is_initialized

    story = space.buildingStory.get
    if !sorted_stories.key?(name.to_s)
      sorted_stories[story.name.to_s] = story
    end
  end

  # flag space types that have wwr overrides
  space_type_wwr_overrides = {}

  # loop through building stories, spaces, and surfaces
  sorted_stories.sort.each_with_index do |(key, story), i|
    # flag for adiabatic floor if building doesn't have ground exposed floor
    if stories_flat[i][:bottom_story_ground_exposed_floor] == false
      adiabatic_floor = true
    end
    # flag for adiabatic roof if building doesn't have exterior exposed roof
    if stories_flat[i][:top_story_exterior_exposed_roof] == false
      adiabatic_ceiling = true
    end

    # make all mid story floor and ceilings adiabatic if requested
    if bar_hash[:make_mid_story_surfaces_adiabatic]
      if i > 0
        adiabatic_floor = true
      end
      if i < sorted_stories.size - 1
        adiabatic_ceiling = true
      end
    end

    # flag orientations for this story to recieve party walls
    party_wall_facades = stories_flat[i][:story_party_walls]

    story.spaces.each do |space|
      next if !new_spaces.include?(space)

      space.surfaces.each do |surface|
        # set floor to adiabatic if requited
        if (adiabatic_floor && surface.surfaceType == 'Floor') || (adiabatic_ceiling && surface.surfaceType == 'RoofCeiling')
          surface.setOutsideBoundaryCondition('Adiabatic')
        end

        # skip of not exterior wall
        next if surface.surfaceType != 'Wall'
        next if surface.outsideBoundaryCondition != 'Outdoors'

        # get the absolute azimuth for the surface so we can categorize it
        absolute_azimuth = OpenStudio.convert(surface.azimuth, 'rad', 'deg').get + surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis
        absolute_azimuth = absolute_azimuth % 360.0 # should result in value between 0 and 360
        absolute_azimuth = absolute_azimuth.round(5) # this was creating issues at 45 deg angles with opposing facades

        # target wwr values that may be changed for specific space types
        wwr_n = bar_hash[:building_wwr_n]
        wwr_e = bar_hash[:building_wwr_e]
        wwr_s = bar_hash[:building_wwr_s]
        wwr_w = bar_hash[:building_wwr_w]

        # look for space type specific wwr values
        if surface.space.is_initialized && surface.space.get.spaceType.is_initialized
          space_type = surface.space.get.spaceType.get

          # see if space type has wwr value
          bar_hash[:space_types].each do |k, v|
            if v.key?(:space_type) && space_type == v[:space_type] && v.key?(:wwr)
              # if matching space type specifies a wwr then override the orientation specific recommendations for this surface.
              wwr_n = v[:wwr]
              wwr_e = v[:wwr]
              wwr_s = v[:wwr]
              wwr_w = v[:wwr]
              space_type_wwr_overrides[space_type] = v[:wwr]
            end
          end
        end

        # add fenestration (wwr for now, maybe overhang and overhead doors later)
        if (absolute_azimuth >= 315.0) || (absolute_azimuth < 45.0)
          if party_wall_facades.include?('north')
            surface.setOutsideBoundaryCondition('Adiabatic')
          else
            surface.setWindowToWallRatio(wwr_n)
          end
        elsif (absolute_azimuth >= 45.0) && (absolute_azimuth < 135.0)
          if party_wall_facades.include?('east')
            surface.setOutsideBoundaryCondition('Adiabatic')
          else
            surface.setWindowToWallRatio(wwr_e)
          end
        elsif (absolute_azimuth >= 135.0) && (absolute_azimuth < 225.0)
          if party_wall_facades.include?('south')
            surface.setOutsideBoundaryCondition('Adiabatic')
          else
            surface.setWindowToWallRatio(wwr_s)
          end
        elsif (absolute_azimuth >= 225.0) && (absolute_azimuth < 315.0)
          if party_wall_facades.include?('west')
            surface.setOutsideBoundaryCondition('Adiabatic')
          else
            surface.setWindowToWallRatio(wwr_w)
          end
        else
          OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "Unexpected value of facade: #{absolute_azimuth}.")
          return false
        end
      end
    end
  end

  # report space types with custom wwr values
  space_type_wwr_overrides.each do |space_type, wwr|
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "For #{space_type.name} the default building wwr was replaced with a space type specfic value of #{wwr}")
  end

  new_floor_area_si = 0.0
  new_spaces.each do |space|
    new_floor_area_si += space.floorArea * space.multiplier
  end
  new_floor_area_ip = OpenStudio.convert(new_floor_area_si, 'm^2', 'ft^2').get

  final_floor_area_ip = OpenStudio.convert(model.getBuilding.floorArea, 'm^2', 'ft^2').get
  if new_floor_area_ip == final_floor_area_ip
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Created bar envelope with floor area of #{OpenStudio.toNeatString(new_floor_area_ip, 0, true)} ft^2.")
  else
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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.")
  end

  return new_spaces
end

.create_bar_from_args_and_building_type_hash(model, args, building_type_hash) ⇒ Boolean

create bar from arguments and building type hash

Parameters:

  • args (Hash)

    user arguments

  • building_type_hash (Array<Hash>)

    array of building type hashes

Options Hash (args):

  • :single_floor_area (Double) — default: 0.0

    Single floor area in ft^2. Non-zero value will fix the single floor area, overriding a user entry for total_bldg_floor_area

  • :total_bldg_floor_area (Double) — default: 10000.0

    Total building floor area in ft^2

  • :floor_height (Double) — default: 0.0

    Typical floor to floor height. Selecting a typical floor height of 0 will trigger a smart building type default.

  • :custom_height_bar (Boolean) — default: true

    This is argument value is only relevant when smart default floor to floor height is used for a building type that has spaces with custom heights.

  • :num_stories_above_grade (Integer) — default: 1

    Number of stories above grade

  • :num_stories_below_grade (Integer) — default: 0

    Number of stories below grade

  • :building_rotation (Double) — default: 0.0

    Building rotation. Set Building Rotation off of North (positive value is clockwise). Rotation applied after geometry generation. Values greater than +/- 45 will result in aspect ratio and party wall orientations that do not match cardinal directions of the inputs.

  • :ns_to_ew_ratio (Double) — default: 0.0

    Ratio of North/South facade length relative to east/west facade length. Selecting an aspect ratio of 0 will trigger a smart building type default. Aspect ratios less than one are not recommended for sliced bar geometry, instead rotate building and use a greater than 1 aspect ratio.

  • :perim_mult (Double) — default: 0.0

    Perimeter multiplier. Selecting a value of 0 will trigger a smart building type default. This represents a multiplier for the building perimeter relative to the perimeter of a rectangular building that meets the area and aspect ratio inputs. Other than the smart default of 0.0 this argument should have a value of 1.0 or higher and is only applicable Multiple Space Types - Individual Stories Sliced division method.

  • :bar_width (Double) — default: 0.0

    Bar Width. Non-zero value will fix the building width, overriding user entry for Perimeter Multiplier. NS/EW Aspect Ratio may be limited based on target width.

  • :bar_sep_dist_mult (Double) — default: 10.0

    Bar separation distance multiplier. Multiplier of separation between bar elements relative to building height.

  • :wwr (Double) — default: 0.0

    Window to wall ratio. Selecting a window to wall ratio of 0 will trigger a smart building type default.

  • :party_wall_fraction (Double) — default: 0.0

    fraction of exterior wall area with an adjacent structure

  • :party_wall_stories_north (Integer) — default: 0

    Number of North facing stories with party wall

  • :party_wall_stories_south (Integer) — default: 0

    Number of South facing stories with party wall

  • :party_wall_stories_east (Integer) — default: 0

    Number of East facing stories with party wall

  • :party_wall_stories_west (Integer) — default: 0

    Number of West facing stories with party wall

  • :bottom_story_ground_exposed_floor (Boolean) — default: true

    Is the bottom story exposed to the ground

  • :top_story_exterior_exposed_roof (Boolean) — default: true

    Is the top story an exterior roof

  • :story_multiplier_method (String) — default: 'Basements Ground Mid Top'

    Calculation method for story multiplier. Options are ‘None’ and ‘Basements Ground Mid Top’

  • :make_mid_story_surfaces_adiabatic (Boolean) — default: true

    Make mid story floor surfaces adiabatic. If set to true, this will skip surface intersection and make mid story floors and celings adiabatic, not just at multiplied gaps.

  • :bar_division_method (String) — default: 'Multiple Space Types - Individual Stories Sliced'

    Division method for bar space types. Options are ‘Multiple Space Types - Simple Sliced’, ‘Multiple Space Types - Individual Stories Sliced’, ‘Single Space Type - Core and Perimeter’

  • :double_loaded_corridor (String) — default: 'Primary Space Type'

    Method for double loaded corridor. Add double loaded corridor for building types that have a defined circulation space type, to the selected space types. Options are ‘None’ and ‘Primary Space Type’

  • :space_type_sort_logic (String) — default: 'Building Type > Size'

    Space type sorting method. Options are ‘Size’ and ‘Building Type > Size’

  • :template (String) — default: '90.1-2013'

    target standard

Options Hash (building_type_hash):

  • :frac_bldg_area (Double)

    fraction of building area

  • :space_types (Hash)

    hash of space types data

Returns:

  • (Boolean)

    returns true if successful, false if not



1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 1257

def self.create_bar_from_args_and_building_type_hash(model, args, building_type_hash)
  # set argument defaults if not present
  args[:single_floor_area] = args.fetch(:single_floor_area, 0.0)
  args[:total_bldg_floor_area] = args.fetch(:total_bldg_floor_area, 10000.0)
  args[:floor_height] = args.fetch(:floor_height, 0.0)
  args[:custom_height_bar] = args.fetch(:custom_height_bar, true)
  args[:num_stories_above_grade] = args.fetch(:num_stories_above_grade, 1)
  args[:num_stories_below_grade] = args.fetch(:num_stories_below_grade, 0)
  args[:building_rotation] = args.fetch(:building_rotation, 0.0)
  args[:ns_to_ew_ratio] = args.fetch(:ns_to_ew_ratio, 0.0)
  args[:perim_mult] = args.fetch(:perim_mult, 0.0)
  args[:bar_width] = args.fetch(:bar_width, 0.0)
  args[:bar_sep_dist_mult] = args.fetch(:bar_sep_dist_mult, 10.0)
  args[:wwr] = args.fetch(:wwr, 0.0)
  args[:party_wall_fraction] = args.fetch(:party_wall_fraction, 0.0)
  args[:party_wall_stories_north] = args.fetch(:party_wall_stories_north, 0)
  args[:party_wall_stories_south] = args.fetch(:party_wall_stories_south, 0)
  args[:party_wall_stories_east] = args.fetch(:party_wall_stories_east, 0)
  args[:party_wall_stories_west] = args.fetch(:party_wall_stories_west, 0)
  args[:bottom_story_ground_exposed_floor] = args.fetch(:bottom_story_ground_exposed_floor, true)
  args[:top_story_exterior_exposed_roof] = args.fetch(:top_story_exterior_exposed_roof, true)
  args[:story_multiplier_method] = args.fetch(:story_multiplier_method, 'Basements Ground Mid Top')
  args[:make_mid_story_surfaces_adiabatic] = args.fetch(:make_mid_story_surfaces_adiabatic, true)
  args[:bar_division_method] = args.fetch(:bar_division_method, 'Multiple Space Types - Individual Stories Sliced')
  args[:double_loaded_corridor] = args.fetch(:double_loaded_corridor, 'Primary Space Type')
  args[:space_type_sort_logic] = args.fetch(:space_type_sort_logic, 'Building Type > Size')
  args[:template] = args.fetch(:template, '90.1-2013')

  # get defaults for the primary building type
  primary_building_type = args[:primary_building_type]
  building_form_defaults = OpenstudioStandards::Geometry.building_form_defaults(primary_building_type)

  # if aspect ratio, story height or wwr have argument value of 0 then use smart building type defaults
  # store list of defaulted items
  defaulted_args = []

  if args[:ns_to_ew_ratio].abs < 0.01
    args[:ns_to_ew_ratio] = building_form_defaults[:aspect_ratio]
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "0.0 value for aspect ratio will be replaced with smart default for #{primary_building_type} of #{building_form_defaults[:aspect_ratio]}.")
  end

  if args[:perim_mult].abs < 0.01
    # if this is not defined then use default of 1.0
    if !building_form_defaults.key?(:perim_mult)
      args[:perim_mult] = 1.0
    else
      args[:perim_mult] = building_form_defaults[:perim_mult]
    end
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "0.0 value for minimum perimeter multiplier will be replaced with smart default for #{primary_building_type} of #{building_form_defaults[:perim_mult]}.")
  elsif args[:perim_mult] < 1.0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', 'Other than the smart default value of 0, the minimum perimeter multiplier should be equal to 1.0 or greater.')
    return false
  end

  if args[:floor_height].abs < 0.01
    args[:floor_height] = building_form_defaults[:typical_story]
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "0.0 value for floor height will be replaced with smart default for #{primary_building_type} of #{building_form_defaults[:typical_story]}.")
    defaulted_args << 'floor_height'
  end
  # 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
  if args[:wwr].abs < 0.01
    args[:wwr] = building_form_defaults[:wwr]
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "0.0 value for window to wall ratio will be replaced with smart default for #{primary_building_type} of #{building_form_defaults[:wwr]}.")
  end

  # report initial condition of model
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "The building started with #{model.getSpaces.size} spaces.")

  # determine of ns_ew needs to be mirrored
  mirror_ns_ew = false
  rotation = model.getBuilding.northAxis
  if rotation > 45.0 && rotation < 135.0
    mirror_ns_ew = true
  elsif rotation > 45.0 && rotation < 135.0
    mirror_ns_ew = true
  end

  # remove non-resource objects not removed by removing the building
  # remove_non_resource_objects(model)

  # creating space types for requested building types
  building_type_hash.each do |building_type, building_type_hash|
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Creating Space Types for #{building_type}.")

    # mapping building_type name is needed for a few methods
    temp_standard = Standard.build(args[:template])
    building_type = temp_standard.model_get_lookup_name(building_type)

    # create space_type_map from array
    sum_of_ratios = 0.0
    building_type_hash[:space_types] = building_type_hash[:space_types].sort_by { |k, v| v[:ratio] }.to_h
    building_type_hash[:space_types].each do |space_type_name, hash|
      next if hash[:space_type_gen] == false # space types like undeveloped and basement are skipped.

      # create space type
      space_type = OpenStudio::Model::SpaceType.new(model)
      space_type.setStandardsBuildingType(building_type)
      space_type.setStandardsSpaceType(space_type_name)
      space_type.setName("#{building_type} #{space_type_name}")

      # set color
      test = temp_standard.space_type_apply_rendering_color(space_type)
      if !test
        # @todo once fixed in standards un-comment this
        # OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "Could not find color for #{space_type.name}")
      end

      # extend hash to hold new space type object
      hash[:space_type] = space_type

      # add to sum_of_ratios counter for adjustment multiplier
      sum_of_ratios += hash[:ratio]
    end

    # store multiplier needed to adjust sum of ratios to equal 1.0
    building_type_hash[:ratio_adjustment_multiplier] = 1.0 / sum_of_ratios
  end

  # calculate length and with of bar
  total_bldg_floor_area_si = OpenStudio.convert(args[:total_bldg_floor_area], 'ft^2', 'm^2').get
  single_floor_area_si = OpenStudio.convert(args[:single_floor_area], 'ft^2', 'm^2').get

  # store number of stories
  num_stories = args[:num_stories_below_grade] + args[:num_stories_above_grade]

  # handle user-assigned single floor plate size condition
  if args[:single_floor_area] > 0.0
    footprint_si = single_floor_area_si
    total_bldg_floor_area_si = footprint_si * num_stories.to_f
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'User-defined single floor area was used for calculation of total building floor area')
    # add warning if custom_height_bar is true and applicable building type is selected
    if args[:custom_height_bar]
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Cannot use custom height bar with single floor area method, will not create custom height bar.')
      args[:custom_height_bar] = false
    end
  else
    footprint_si = nil
  end

  # populate space_types_hash
  space_types_hash = {}
  multi_height_space_types_hash = {}
  custom_story_heights = []
  if args[:space_type_sort_logic] == 'Building Type > Size'
    building_type_hash = building_type_hash.sort_by { |k, v| v[:frac_bldg_area] }
  end
  building_type_hash.each do |building_type, building_type_hash|
    if args[:double_loaded_corridor] == 'Primary Space Type'

      # 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
      default_st = nil
      circ_st = nil
      building_type_hash[:space_types].each do |space_type_name, hash|
        if hash[:default] then default_st = space_type_name end
        if hash[:circ] then circ_st = space_type_name end
      end

      # update building hash
      if !default_st.nil? && !circ_st.nil?
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Combining #{default_st} and #{circ_st} into a group representing a double loaded corridor")

        # add new item
        building_type_hash[:space_types]['Double Loaded Corridor'] = {}
        double_loaded_st = building_type_hash[:space_types]['Double Loaded Corridor']
        double_loaded_st[:ratio] = building_type_hash[:space_types][default_st][:ratio] + building_type_hash[:space_types][circ_st][:ratio]
        double_loaded_st[:double_loaded_corridor] = true
        double_loaded_st[:space_type] = model.getBuilding
        double_loaded_st[:children] = {}
        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]
        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]
        building_type_hash[:space_types][default_st][:name] = default_st
        building_type_hash[:space_types][circ_st][:name] = circ_st
        double_loaded_st[:children][:default] = building_type_hash[:space_types][default_st]
        double_loaded_st[:children][:circ] = building_type_hash[:space_types][circ_st]
        double_loaded_st[:orig_ratio] = 0.0

        # zero out ratios from old item (don't delete because I still want the space types made)
        building_type_hash[:space_types][default_st][:ratio] = 0.0
        building_type_hash[:space_types][circ_st][:ratio] = 0.0
      end
    end

    building_type_hash[:space_types].each do |space_type_name, hash|
      next if hash[:space_type_gen] == false

      space_type = hash[:space_type]
      ratio_of_bldg_total = hash[:ratio] * building_type_hash[:ratio_adjustment_multiplier] * building_type_hash[:frac_bldg_area]
      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

      # only add custom height space if 0 is used for floor_height
      if defaulted_args.include?('floor_height') && hash.key?(:story_height) && args[:custom_height_bar]
        multi_height_space_types_hash[space_type] = { floor_area: final_floor_area, space_type: space_type, story_height: hash[:story_height] }
        if hash.key?(:orig_ratio) then multi_height_space_types_hash[space_type][:orig_ratio] = hash[:orig_ratio] end
        custom_story_heights << hash[:story_height]
        if args[:wwr] == 0 && hash.key?(:wwr)
          multi_height_space_types_hash[space_type][:wwr] = hash[:wwr]
        end
      else
        # only add wwr if 0 used for wwr arg and if space type has wwr as key
        space_types_hash[space_type] = { floor_area: final_floor_area, space_type: space_type }
        if hash.key?(:orig_ratio) then space_types_hash[space_type][:orig_ratio] = hash[:orig_ratio] end
        if args[:wwr] == 0 && hash.key?(:wwr)
          space_types_hash[space_type][:wwr] = hash[:wwr]
        end
        if hash[:double_loaded_corridor]
          space_types_hash[space_type][:children] = hash[:children]
        end
      end
    end
  end

  # resort if not sorted by building type
  if args[:space_type_sort_logic] == 'Size'
    # added code to convert to hash. I use sort_by 3 other times, but those seem to be working fine as is now.
    space_types_hash = Hash[space_types_hash.sort_by { |k, v| v[:floor_area] }]
  end

  # calculate targets for testing
  target_areas = {} # used for checks
  target_areas_cust_height = 0.0
  space_types_hash.each do |k, v|
    if v.key?(:orig_ratio)
      target_areas[k] = v[:orig_ratio] * total_bldg_floor_area_si
    else
      target_areas[k] = v[:floor_area]
    end
  end
  multi_height_space_types_hash.each do |k, v|
    if v.key?(:orig_ratio)
      target_areas[k] = v[:orig_ratio] * total_bldg_floor_area_si
      target_areas_cust_height += v[:orig_ratio] * total_bldg_floor_area_si
    else
      target_areas[k] = v[:floor_area]
      target_areas_cust_height += v[:floor_area]
    end
  end

  # gather inputs
  if footprint_si.nil?
    footprint_si = (total_bldg_floor_area_si - target_areas_cust_height) / num_stories.to_f
  end
  floor_height = OpenStudio.convert(args[:floor_height], 'ft', 'm').get
  min_allow_size = OpenStudio.convert(15.0, 'ft', 'm').get
  specified_bar_width_si = OpenStudio.convert(args[:bar_width], 'ft', 'm').get

  # set custom width
  if specified_bar_width_si > 0
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Ignoring perimeter multiplier argument when non zero width argument is used')
    if footprint_si / specified_bar_width_si >= min_allow_size
      width = specified_bar_width_si
      length = footprint_si / width
    else
      length = min_allow_size
      width = footprint_si / length
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'User specified width results in a length that is too short, adjusting width to be narrower than specified.')
    end
    width_cust_height = specified_bar_width_si
  else
    width = Math.sqrt(footprint_si / args[:ns_to_ew_ratio])
    length = footprint_si / width
    width_cust_height = Math.sqrt(target_areas_cust_height / args[:ns_to_ew_ratio])
  end
  length_cust_height = target_areas_cust_height / width_cust_height
  if args[:perim_mult] > 1.0 && target_areas_cust_height > 0.0
    # @todo update tests that hit this warning
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Ignoring perimeter multiplier for bar that represents custom height spaces.')
  end

  # check if dual bar is needed
  dual_bar = false
  if specified_bar_width_si > 0.0 && args[:bar_division_method] == 'Multiple Space Types - Individual Stories Sliced'
    if length / width != args[:ns_to_ew_ratio]

      if args[:ns_to_ew_ratio] >= 1.0 && args[:ns_to_ew_ratio] > length / width
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "Can't meet target aspect ratio of #{args[:ns_to_ew_ratio]}, Lowering it to #{length / width} ")
        args[:ns_to_ew_ratio] = length / width
      elsif args[:ns_to_ew_ratio] < 1.0 && args[:ns_to_ew_ratio] > length / width
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "Can't meet target aspect ratio of #{args[:ns_to_ew_ratio]}, Increasing it to #{length / width} ")
        args[:ns_to_ew_ratio] = length / width
      else
        # check if each bar would be longer then 15 feet, then set as dual bar and override perimeter multiplier
        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])
        length_alt2 = length - length_alt1
        if [length_alt1, length_alt2].min >= min_allow_size
          dual_bar = true
        else
          OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Second bar would be below minimum length, will model as single bar')
          # swap length and width if single bar and aspect ratio less than 1
          if args[:ns_to_ew_ratio] < 1.0
            width = length
            length = specified_bar_width_si
          end
        end
      end
    end
  elsif args[:perim_mult] > 1.0 && args[:bar_division_method] == 'Multiple Space Types - Individual Stories Sliced'
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', '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.')
    dual_bar = true
  elsif args[:perim_mult] > 1.0
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "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")
  end

  # calculations for dual bar, which later will be setup to run create_bar twice
  if dual_bar
    min_perim = (2 * width) + (2 * length)
    target_area = footprint_si
    target_perim = min_perim * args[:perim_mult]
    tol_testing = 0.00001
    dual_bar_calc_approach = nil # stretched, adiabatic_ends_bar_b, dual_bar
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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.")
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Target dual bar perimeter is #{OpenStudio.toNeatString(OpenStudio.convert(target_perim, 'm', 'ft').get, 0, true)} ft.")

    # determine which of the three paths to hit target perimeter multiplier are possible
    # A use dual bar non adiabatic
    # B use dual bar adiabatic
    # C use stretched bar (requires model to miss ns/ew ratio)

    # custom quadratic equation to solve two bars with common width 2l^2 - p*l + 4a = 0
    if ((target_perim**2) - (32 * footprint_si)) > 0
      if specified_bar_width_si > 0
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Ignoring perimeter multiplier argument and using use specified bar width.')
        dual_double_end_width = specified_bar_width_si
        dual_double_end_length = footprint_si / dual_double_end_width
      else
        dual_double_end_length = 0.25 * (target_perim + Math.sqrt((target_perim**2) - (32 * footprint_si)))
        dual_double_end_width = footprint_si / dual_double_end_length
      end

      # now that stretched  bar is made, determine where to split it and rotate
      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])
      bar_b_length = dual_double_end_length - bar_a_length
      area_a = bar_a_length * dual_double_end_width
      area_b = bar_b_length * dual_double_end_width
    else
      # this will throw it to adiabatic ends test
      bar_a_length = 0
      bar_b_length = 0
    end

    if bar_a_length >= min_allow_size && bar_b_length >= min_allow_size
      dual_bar_calc_approach = 'dual_bar'
    else
      # adiabatic bar input calcs
      if ((target_perim**2) - (16 * footprint_si)) > 0
        adiabatic_dual_double_end_length = 0.25 * (target_perim + Math.sqrt((target_perim**2) - (16 * footprint_si)))
        adiabatic_dual_double_end_width = footprint_si / adiabatic_dual_double_end_length
        # test for unexpected
        unexpected = false
        if (target_area - (adiabatic_dual_double_end_length * adiabatic_dual_double_end_width)).abs > tol_testing then unexpected = true end
        if specified_bar_width_si == 0
          if (target_perim - ((adiabatic_dual_double_end_length * 2) + (adiabatic_dual_double_end_width * 2))).abs > tol_testing then unexpected = true end
        end
        if unexpected
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Unexpected values for dual rectangle adiabatic ends bar b.')
        end
        # now that stretched  bar is made, determine where to split it and rotate
        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])
        adiabatic_bar_b_length = adiabatic_dual_double_end_length - adiabatic_bar_a_length
        adiabatic_area_a = adiabatic_bar_a_length * adiabatic_dual_double_end_width
        adiabatic_area_b = adiabatic_bar_b_length * adiabatic_dual_double_end_width
      else
        # this will throw it stretched single bar
        adiabatic_bar_a_length = 0
        adiabatic_bar_b_length = 0
      end
      if adiabatic_bar_a_length >= min_allow_size && adiabatic_bar_b_length >= min_allow_size
        dual_bar_calc_approach = 'adiabatic_ends_bar_b'
      else
        dual_bar_calc_approach = 'stretched'
      end
    end

    # apply prescribed approach for stretched or dual bar
    if dual_bar_calc_approach == 'dual_bar'
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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")
      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
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Unexpected values for dual rectangle.')
      end

      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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")
      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
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Unexpected values for rotated dual rectangle')
      end
    elsif dual_bar_calc_approach == 'adiabatic_ends_bar_b'
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Can't hit target perimeter with two rectangles, need to make two ends adiabatic")

      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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")
      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
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Unexpected values for rotated dual rectangle adiabatic ends bar b')
      end
    else
      # stretched bar
      dual_bar = false

      stretched_length = 0.25 * (target_perim + Math.sqrt((target_perim**2) - (16 * footprint_si)))
      stretched_width = footprint_si / stretched_length
      if (target_area - (stretched_length * stretched_width)).abs > tol_testing || (target_perim - ((stretched_length + stretched_width) * 2)) > tol_testing
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Unexpected values for single stretched')
      end

      width = stretched_width
      length = stretched_length
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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.")
    end
  end

  bars = {}
  bars['primary'] = {}
  if dual_bar
    if mirror_ns_ew && dual_bar_calc_approach == 'dual_bar'
      bars['primary'][:length] = dual_double_end_width
      bars['primary'][:width] = bar_a_length
    elsif dual_bar_calc_approach == 'dual_bar'
      bars['primary'][:length] = bar_a_length
      bars['primary'][:width] = dual_double_end_width
    elsif mirror_ns_ew
      bars['primary'][:length] = adiabatic_dual_double_end_width
      bars['primary'][:width] = adiabatic_bar_a_length
    else
      bars['primary'][:length] = adiabatic_bar_a_length
      bars['primary'][:width] = adiabatic_dual_double_end_width
    end
  else
    if mirror_ns_ew
      bars['primary'][:length] = width
      bars['primary'][:width] = length
    else
      bars['primary'][:length] = length
      bars['primary'][:width] = width
    end
  end
  bars['primary'][:floor_height] = floor_height # can make use of this when breaking out multi-height spaces
  bars['primary'][:num_stories] = num_stories
  bars['primary'][:center_of_footprint] = OpenStudio::Point3d.new(0.0, 0.0, 0.0)
  space_types_hash_secondary = {}
  if dual_bar
    # loop through each story and move portion for other bar to its own hash
    primary_footprint = bars['primary'][:length] * bars['primary'][:width]
    secondary_footprint = target_area - primary_footprint
    footprint_counter = primary_footprint
    secondary_footprint_counter = secondary_footprint
    story_counter = 0
    pri_sec_tol = 0.0001 # m^2
    pri_sec_min_area = 0.0001 # m^2
    space_types_hash.each do |k, v|
      space_type_left = v[:floor_area]

      # do not go to next space type until this one is evaulate, which may span stories
      until (space_type_left.abs < 0.01) || (story_counter >= num_stories)

        # use secondary footprint if any left
        if secondary_footprint_counter > 0.0
          hash_area = [space_type_left, secondary_footprint_counter].min

          # confirm that the part of space type use or what is left is greater than min allowed value
          projected_space_type_left = space_type_left - hash_area
          test_a = hash_area >= pri_sec_min_area
          test_b = (projected_space_type_left >= pri_sec_min_area) || (projected_space_type_left.abs < 0.01)
          test_c = k == space_types_hash.keys.last # if last space type accept sliver, no other space to infil
          if (test_a && test_b) || test_c
            if space_types_hash_secondary.key?(k)
              # add to what was added for previous story
              space_types_hash_secondary[k][:floor_area] += hash_area
            else
              # add new space type to hash
              if v.key?(:children)
                space_types_hash_secondary[k] = { floor_area: hash_area, space_type: v[:space_type], children: v[:children] }
              else
                space_types_hash_secondary[k] = { floor_area: hash_area, space_type: v[:space_type] }
              end
            end
            space_types_hash[k][:floor_area] -= hash_area
            secondary_footprint_counter -= hash_area
            space_type_left -= hash_area
          else
            OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Shifting space types between bars to avoid sliver of #{k.name}.")
          end
        end

        # remove space if entirely used up by secondary bar
        if space_types_hash[k][:floor_area] <= pri_sec_tol
          space_types_hash.delete(k)
          space_type_left = 0.0
        else
          # then look at primary bar
          hash_area_pri = [space_type_left, footprint_counter].min
          footprint_counter -= hash_area_pri
          space_type_left -= hash_area_pri
        end

        # reset counter when full
        if footprint_counter <= pri_sec_tol && secondary_footprint_counter <= pri_sec_tol
          # check if this is partial top floor
          story_counter += 1
          if num_stories < story_counter + 1
            footprint_counter = primary_footprint * (num_stories - story_counter)
            secondary_footprint_counter = secondary_footprint * (num_stories - story_counter)
          else
            footprint_counter = primary_footprint
            secondary_footprint_counter = secondary_footprint
          end
        end
      end
    end
  end

  # setup bar_hash and run create_bar
  bars['primary'][:space_types_hash] = space_types_hash
  bars['primary'][:args] = args
  v = bars['primary']
  OpenstudioStandards::Geometry.bar_hash_setup_run(model, v[:args], v[:length], v[:width], v[:floor_height], v[:center_of_footprint], v[:space_types_hash], v[:num_stories])

  # store offset value for multiple bars
  if args.key?(:bar_sep_dist_mult) && args[:bar_sep_dist_mult] > 0.0
    offset_val = num_stories.ceil * floor_height * args[:bar_sep_dist_mult]
  elsif args.key?(:bar_sep_dist_mult)
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Positive value is required for bar_sep_dist_mult, ignoring input and using value of 0.1')
    offset_val = num_stories.ceil * floor_height * 0.1
  else
    offset_val = num_stories.ceil * floor_height * 10.0
  end

  if dual_bar
    args2 = args.clone
    bars['secondary'] = {}
    if mirror_ns_ew && dual_bar_calc_approach == 'dual_bar'
      bars['secondary'][:length] = bar_b_length
      bars['secondary'][:width] = dual_double_end_width
    elsif dual_bar_calc_approach == 'dual_bar'
      bars['secondary'][:length] = dual_double_end_width
      bars['secondary'][:width] = bar_b_length
    elsif mirror_ns_ew
      bars['secondary'][:length] = adiabatic_bar_b_length
      bars['secondary'][:width] = adiabatic_dual_double_end_width
      args2[:party_wall_stories_east] = num_stories.ceil
      args2[:party_wall_stories_west] = num_stories.ceil
    else
      bars['secondary'][:length] = adiabatic_dual_double_end_width
      bars['secondary'][:width] = adiabatic_bar_b_length
      args2[:party_wall_stories_south] = num_stories.ceil
      args2[:party_wall_stories_north] = num_stories.ceil
    end
    bars['secondary'][:floor_height] = floor_height # can make use of this when breaking out multi-height spaces
    bars['secondary'][:num_stories] = num_stories
    bars['secondary'][:space_types_hash] = space_types_hash_secondary
    if dual_bar_calc_approach == 'adiabatic_ends_bar_b'
      # 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
      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
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', '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.')
      else
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Adiabatic ends added to secondary bar because target perimeter multiplier could not be met with two full rectangular footprints.')
      end
      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)
    else
      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)
    end
    bars['secondary'][:args] = args2

    # setup bar_hash and run create_bar
    v = bars['secondary']
    OpenstudioStandards::Geometry.bar_hash_setup_run(model, v[:args], v[:length], v[:width], v[:floor_height], v[:center_of_footprint], v[:space_types_hash], v[:num_stories])
  end

  # future development (up against primary bar run intersection and surface matching after add all bars, avoid interior windows)
  # 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
  if !multi_height_space_types_hash.empty?
    args3 = args.clone
    bars['custom_height'] = {}
    if mirror_ns_ew
      bars['custom_height'][:length] = width_cust_height
      bars['custom_height'][:width] = length_cust_height
    else
      bars['custom_height'][:length] = length_cust_height
      bars['custom_height'][:width] = width_cust_height
    end
    if args[:party_wall_stories_east] + args[:party_wall_stories_west] + args[:party_wall_stories_south] + args[:party_wall_stories_north] > 0.0
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Ignorning party wall inputs for custom height bar')
    end

    # disable party walls
    args3[:party_wall_stories_east] = 0
    args3[:party_wall_stories_west] = 0
    args3[:party_wall_stories_south] = 0
    args3[:party_wall_stories_north] = 0

    # setup stories
    args3[:num_stories_below_grade] = 0
    args3[:num_stories_above_grade] = 1

    # can make use of this when breaking out multi-height spaces
    bars['custom_height'][:floor_height] = floor_height
    bars['custom_height'][:num_stories] = num_stories
    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)
    bars['custom_height'][:floor_height] = OpenStudio.convert(custom_story_heights.max, 'ft', 'm').get
    bars['custom_height'][:num_stories] = 1
    bars['custom_height'][:space_types_hash] = multi_height_space_types_hash
    bars['custom_height'][:args] = args3

    v = bars['custom_height']
    OpenstudioStandards::Geometry.bar_hash_setup_run(model, v[:args], v[:length], v[:width], v[:floor_height], v[:center_of_footprint], v[:space_types_hash], v[:num_stories])
  end

  # diagnostic log
  sum_actual = 0.0
  sum_target = 0.0
  throw_error = false

  # check expected floor areas against actual
  model.getSpaceTypes.sort.each do |space_type|
    next if !target_areas.key? space_type # space type in model not part of building type(s), maybe issue warning

    # convert to IP
    actual_ip = OpenStudio.convert(space_type.floorArea, 'm^2', 'ft^2').get
    target_ip = OpenStudio.convert(target_areas[space_type], 'm^2', 'ft^2').get
    sum_actual += actual_ip
    sum_target += target_ip

    if (space_type.floorArea - target_areas[space_type]).abs >= 1.0

      if !args[:bar_division_method].include? 'Single Space Type'
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "#{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)")
        throw_error = true
      else
        # will see this if use Single Space type division method on multi-use building or single building type without whole building space type
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{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)")
      end

    end
  end

  # report summary then throw error
  if throw_error
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "Sum of actual floor area is #{sum_actual} ft^2, sum of target floor area is #{sum_target}.")
    return false
  end

  # check party wall fraction by looping through surfaces
  if args[:party_wall_fraction] > 0
    actual_ext_wall_area = model.getBuilding.exteriorWallArea
    actual_party_wall_area = 0.0
    model.getSurfaces.sort.each do |surface|
      next if surface.outsideBoundaryCondition != 'Adiabatic'
      next if surface.surfaceType != 'Wall'

      actual_party_wall_area += surface.grossArea * surface.space.get.multiplier
    end
    actual_party_wall_fraction = actual_party_wall_area / (actual_party_wall_area + actual_ext_wall_area)
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Target party wall fraction is #{args[:party_wall_fraction]}. Realized fraction is #{actual_party_wall_fraction.round(2)}")
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "party_wall_fraction_actual: #{actual_party_wall_fraction}")
  end

  # check ns/ew aspect ratio (harder to check when party walls are added)
  wall_and_window_by_orientation = OpenstudioStandards::Geometry.model_get_exterior_window_and_wall_area_by_orientation(model)
  wall_ns = (wall_and_window_by_orientation['north_wall'] + wall_and_window_by_orientation['south_wall'])
  wall_ew = wall_and_window_by_orientation['east_wall'] + wall_and_window_by_orientation['west_wall']
  wall_ns_ip = OpenStudio.convert(wall_ns, 'm^2', 'ft^2').get
  wall_ew_ip = OpenStudio.convert(wall_ew, 'm^2', 'ft^2').get
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "wall_area_ip: #{wall_ns_ip + wall_ew_ip} ft^2")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "ns_wall_area_ip: #{wall_ns_ip} ft^2")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "ew_wall_area_ip: #{wall_ew_ip} ft^2")
  # for now using perimeter of ground floor and average story area (building area / num_stories)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "floor_area_to_perim_ratio: #{model.getBuilding.floorArea / (OpenstudioStandards::Geometry.model_get_perimeter(model) * num_stories)}")
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "bar_width: #{OpenStudio.convert(bars['primary'][:width], 'm', 'ft').get} ft")

  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
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Target facade area by orientation not validated when party walls are applied')
  elsif args[:num_stories_above_grade] != args[:num_stories_above_grade].ceil
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Target facade area by orientation not validated when partial top story is used')
  elsif dual_bar_calc_approach == 'stretched'
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Target facade area by orientation not validated when single stretched bar has to be used to meet target minimum perimeter multiplier')
  elsif defaulted_args.include?('floor_height') && args[:custom_height_bar] && !multi_height_space_types_hash.empty?
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Target facade area by orientation not validated when a dedicated bar is added for space types with custom heights')
  elsif args[:bar_width] > 0
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Target facade area by orientation not validated when a dedicated custom bar width is defined')
  else

    # adjust length versus width based on building rotation
    if mirror_ns_ew
      wall_target_ns_ip = 2 * OpenStudio.convert(width, 'm', 'ft').get * args[:perim_mult] * args[:num_stories_above_grade] * args[:floor_height]
      wall_target_ew_ip = 2 * OpenStudio.convert(length, 'm', 'ft').get * args[:perim_mult] * args[:num_stories_above_grade] * args[:floor_height]
    else
      wall_target_ns_ip = 2 * OpenStudio.convert(length, 'm', 'ft').get * args[:perim_mult] * args[:num_stories_above_grade] * args[:floor_height]
      wall_target_ew_ip = 2 * OpenStudio.convert(width, 'm', 'ft').get  * args[:perim_mult] * args[:num_stories_above_grade] * args[:floor_height]
    end
    flag_error = false
    if (wall_target_ns_ip - wall_ns_ip).abs > 0.1
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "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)")
      flag_error = true
    end
    if (wall_target_ew_ip - wall_ew_ip).abs > 0.1
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "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)")
      flag_error = true
    end
    if flag_error
      return false
    end
  end

  # test for excessive exterior roof area (indication of problem with intersection and or surface matching)
  ext_roof_area = model.getBuilding.exteriorSurfaceArea - model.getBuilding.exteriorWallArea
  expected_roof_area = args[:total_bldg_floor_area] / (args[:num_stories_above_grade] + args[:num_stories_below_grade]).to_f
  if ext_roof_area > expected_roof_area && (single_floor_area_si.abs < 0.01) # only test if using whole-building area input
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', 'Roof area larger than expected, may indicate problem with inter-floor surface intersection or matching.')
    return false
  end

  # set building rotation
  initial_rotation = model.getBuilding.northAxis
  if args[:building_rotation] != initial_rotation
    model.getBuilding.setNorthAxis(args[:building_rotation])
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "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.")
  end

  # report final condition of model
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "The building finished with #{model.getSpaces.size} spaces.")

  return true
end

.create_bar_from_building_type_ratios(model, args) ⇒ Boolean

create bar from building type ratios arguments are passed through to lower level methods. See create_bar_from_args_and_building_type_hash for additional argument options.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • args (Hash)

    user arguments

Options Hash (args):

  • :bldg_type_a (String) — default: 'SmallOffice'

    primary building type

  • :bldg_type_b (String) — default: nil

    building type b

  • :bldg_type_c (String) — default: nil

    building type c

  • :bldg_type_d (String) — default: nil

    building type d

  • :bldg_subtype_a (String) — default: 'NA'

    primary building subtype

  • :bldg_subtype_b (String) — default: 'NA'

    building type b subtype

  • :bldg_subtype_c (String) — default: 'NA'

    building type c subtype

  • :bldg_subtype_d (String) — default: 'NA'

    building type d subtype

  • :bldg_type_a_fract_bldg_area (String) — default: 1.0

    building type a area fraction of total floor area

  • :bldg_type_b_fract_bldg_area (String) — default: 0.0

    building type b area fraction of total floor area

  • :bldg_type_c_fract_bldg_area (String) — default: 0.0

    building type c area fraction of total floor area

  • :bldg_type_d_fract_bldg_area (String) — default: 0.0

    building type d area fraction of total floor area

  • :template (String) — default: '90.1-2013'

    target standard

Returns:

  • (Boolean)

    returns true if successful, false if not



1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 1996

def self.create_bar_from_building_type_ratios(model, args)
  # convert arguments to symbols
  args = args.transform_keys(&:to_sym)
  bldg_type_a = args.fetch(:bldg_type_a, 'SmallOffice')
  bldg_type_b = args.fetch(:bldg_type_b, nil)
  bldg_type_c = args.fetch(:bldg_type_c, nil)
  bldg_type_d = args.fetch(:bldg_type_d, nil)
  bldg_subtype_a = args.fetch(:bldg_subtype_a, 'NA')
  bldg_subtype_b = args.fetch(:bldg_subtype_b, 'NA')
  bldg_subtype_c = args.fetch(:bldg_subtype_c, 'NA')
  bldg_subtype_d = args.fetch(:bldg_subtype_d, 'NA')
  bldg_type_a_fract_bldg_area = args.fetch(:bldg_type_a_fract_bldg_area, 1.0)
  bldg_type_b_fract_bldg_area = args.fetch(:bldg_type_b_fract_bldg_area, 0.0)
  bldg_type_c_fract_bldg_area = args.fetch(:bldg_type_c_fract_bldg_area, 0.0)
  bldg_type_d_fract_bldg_area = args.fetch(:bldg_type_d_fract_bldg_area, 0.0)
  template = args.fetch(:template, '90.1-2013')

  # If DOE building type is supplied with a DEER template, map to the nearest DEER building type
  if template.include?('DEER')
    unless bldg_type_a.nil?
      bldg_type_a_deer = OpenstudioStandards::CreateTypical.doe_to_deer_building_type(bldg_type_a)
      if bldg_type_a_deer.nil?
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "Could not find a DEER equivalent to #{bldg_type_a} to align with template #{template}.")
        return false
      elsif bldg_type_a == bldg_type_a_deer
        # Already using a DEER building type with a DEER template, no need to change
      else
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Mapped input of DOE building type #{bldg_type_a} to DEER building type #{bldg_type_a_deer} to match template #{template}")
        bldg_type_a = bldg_type_a_deer
      end
    end

    unless bldg_type_b.nil?
      bldg_type_b_deer = OpenstudioStandards::CreateTypical.doe_to_deer_building_type(bldg_type_b)
      if bldg_type_b_deer.nil?
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "Could not find a DEER equivalent to #{bldg_type_b} to align with template #{template}.")
        return false
      elsif bldg_type_b == bldg_type_b_deer
        # Already using a DEER building type with a DEER template, no need to change
      else
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Mapped input of DOE building type #{bldg_type_b} to DEER building type #{bldg_type_b_deer} to match template #{template}")
        bldg_type_b = bldg_type_b_deer
      end
    end

    unless bldg_type_c.nil?
      bldg_type_c_deer = OpenstudioStandards::CreateTypical.doe_to_deer_building_type(bldg_type_c)
      if bldg_type_c_deer.nil?
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "Could not find a DEER equivalent to #{bldg_type_c} to align with template #{template}.")
        return false
      elsif bldg_type_c == bldg_type_c_deer
        # Already using a DEER building type with a DEER template, no need to change
      else
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Mapped input of DOE building type #{bldg_type_c} to DEER building type #{bldg_type_c_deer} to match template #{template}")
        bldg_type_c = bldg_type_c_deer
      end
    end

    unless bldg_type_d.nil?
      bldg_type_d_deer = OpenstudioStandards::CreateTypical.doe_to_deer_building_type(bldg_type_d)
      if bldg_type_d_deer.nil?
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', "Could not find a DEER equivalent to #{bldg_type_d} to align with template #{template}.")
        return false
      elsif bldg_type_d == bldg_type_d_deer
        # Already using a DEER building type with a DEER template, no need to change
      else
        OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Mapped input of DOE building type #{bldg_type_d} to DEER building type #{bldg_type_d_deer} to match template #{template}")
        bldg_type_d = bldg_type_d_deer
      end
    end
  end

  # check that sum of fractions for b,c, and d is less than 1.0 (so something is left for primary building type)
  bldg_type_a_fract_bldg_area = 1.0 - bldg_type_b_fract_bldg_area - bldg_type_c_fract_bldg_area - bldg_type_d_fract_bldg_area
  if bldg_type_a_fract_bldg_area <= 0.0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create', '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.')
    return false
  end

  # report initial condition of model
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "The building started with #{model.getSpaces.size} spaces.")

  # determine of ns_ew needs to be mirrored
  mirror_ns_ew = false
  rotation = model.getBuilding.northAxis
  if rotation > 45.0 && rotation < 135.0
    mirror_ns_ew = true
  elsif rotation > 45.0 && rotation < 135.0
    mirror_ns_ew = true
  end

  # remove non-resource objects not removed by removing the building
  # remove_non_resource_objects(model)

  # rename building to infer template in downstream measure
  name_array = [template, bldg_type_a]
  if bldg_type_b_fract_bldg_area > 0 then name_array << bldg_type_b end
  if bldg_type_c_fract_bldg_area > 0 then name_array << bldg_type_c end
  if bldg_type_d_fract_bldg_area > 0 then name_array << bldg_type_d end
  model.getBuilding.setName(name_array.join('|').to_s)

  # hash to whole building type data
  building_type_hash = {}

  # gather data for bldg_type_a
  building_type_hash[bldg_type_a] = {}
  building_type_hash[bldg_type_a][:frac_bldg_area] = bldg_type_a_fract_bldg_area
  building_type_hash[bldg_type_a][:space_types] = OpenstudioStandards::CreateTypical.get_space_types_from_building_type(bldg_type_a, building_subtype: bldg_subtype_a, template: template, whole_building: true)

  # gather data for bldg_type_b
  if bldg_type_b_fract_bldg_area > 0
    building_type_hash[bldg_type_b] = {}
    building_type_hash[bldg_type_b][:frac_bldg_area] = bldg_type_b_fract_bldg_area
    building_type_hash[bldg_type_b][:space_types] = OpenstudioStandards::CreateTypical.get_space_types_from_building_type(bldg_type_b, building_subtype: bldg_subtype_b, template: template, whole_building: true)
  end

  # gather data for bldg_type_c
  if bldg_type_c_fract_bldg_area > 0
    building_type_hash[bldg_type_c] = {}
    building_type_hash[bldg_type_c][:frac_bldg_area] = bldg_type_c_fract_bldg_area
    building_type_hash[bldg_type_c][:space_types] = OpenstudioStandards::CreateTypical.get_space_types_from_building_type(bldg_type_c, building_subtype: bldg_subtype_c, template: template, whole_building: true)
  end

  # gather data for bldg_type_d
  if bldg_type_d_fract_bldg_area > 0
    building_type_hash[bldg_type_d] = {}
    building_type_hash[bldg_type_d][:frac_bldg_area] = bldg_type_d_fract_bldg_area
    building_type_hash[bldg_type_d][:space_types] = OpenstudioStandards::CreateTypical.get_space_types_from_building_type(bldg_type_d, building_subtype: bldg_subtype_d, template: template, whole_building: true)
  end

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Creating bar based on space ratios from #{bldg_type_a} for building form defaults.")

  # call create_bar_from_args_and_building_type_hash to generate bar
  args[:primary_building_type] = bldg_type_a
  OpenstudioStandards::Geometry.create_bar_from_args_and_building_type_hash(model, args, building_type_hash)

  return true
end

.create_bar_from_space_type_ratios(model, args) ⇒ Boolean

create bar from space type ratios arguments are passed through to lower level methods. See create_bar_from_args_and_building_type_hash for additional argument options.

Parameters:

  • args (Hash)

    user arguments

Options Hash (args):

  • :space_type_hash_string (String)

    Space types ratio string in the form ‘BuildingType | SpaceType => 0.75, BuildingType | SpaceType => 0.25’. Fractions should add up to 1. All space types should come from the selected OpenStudio Standards template.

  • :template (String) — default: '90.1-2013'

    target standard

Returns:

  • (Boolean)

    returns true if successful, false if not



2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 2143

def self.create_bar_from_space_type_ratios(model, args)
  if args[:space_type_hash_string].empty?
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'args hash passed to create_bar_from_space_type_ratios must include a non-empty :space_type_hash_string')
    return false
  end
  template = args.fetch(:template, '90.1-2013')

  # process arg into hash
  space_type_hash_name = {}
  args[:space_type_hash_string][0..-1].split(/, /) do |entry|
    entry_map = entry.split(/=>/)
    value_str = entry_map[1]
    space_type_hash_name[entry_map[0].strip[0..-1].to_s] = value_str.nil? ? '' : value_str.strip[0..-1].to_f
  end

  # create building type hash from space type ratios
  building_type_hash = {}
  building_type_fraction_of_building = 0.0
  space_type_hash_name.each do |building_space_type, ratio|
    building_type = building_space_type.split('|')[0].strip
    space_type = building_space_type.split('|')[1].strip

    # harvest height and circ info from get_space_types_from_building_type(building_type, template, whole_building = true)
    building_type_lookup_info = OpenstudioStandards::CreateTypical.get_space_types_from_building_type(building_type, template: template)
    if building_type_lookup_info.empty?
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{building_type} looks like an invalid building type for #{template}")
    end
    space_type_info_hash = {}
    if building_type_lookup_info.key?(space_type)
      if building_type_lookup_info[space_type].key?(:story_height)
        space_type_info_hash[:story_height] = building_type_lookup_info[space_type][:story_height]
      end
      if building_type_lookup_info[space_type].key?(:default)
        space_type_info_hash[:default] = building_type_lookup_info[space_type][:default]
      end
      if building_type_lookup_info[space_type].key?(:circ)
        space_type_info_hash[:circ] = building_type_lookup_info[space_type][:circ]
      end
    else
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{space_type} looks like an invalid space type for #{building_type}")
    end

    # extend harvested data with custom ratios from space type ratio string argument.
    if building_type_hash.key?(building_type)
      building_type_hash[building_type][:frac_bldg_area] += ratio
      space_type_info_hash[:ratio] = ratio
      building_type_hash[building_type][:space_types][space_type] = space_type_info_hash
    else
      building_type_hash[building_type] = {}
      building_type_hash[building_type][:frac_bldg_area] = ratio
      space_type_info_hash[:ratio] = ratio
      space_types = {}
      space_types[space_type] = space_type_info_hash
      building_type_hash[building_type][:space_types] = space_types
    end
    building_type_fraction_of_building += ratio
  end

  # @todo confirm if this will get normalized up/down later of if I should fix or stop here instead of just a warning
  if building_type_fraction_of_building > 1.0
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "Sum of Space Type Ratio of #{building_type_fraction_of_building} is greater than the expected value of 1.0")
  elsif building_type_fraction_of_building < 1.0
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "Sum of Space Type Ratio of #{building_type_fraction_of_building} is less than the expected value of 1.0")
  end

  # identify primary building type for building form defaults
  # update to choose building with highest ratio
  primary_building_type = building_type_hash.keys.first

  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Creating bar based space type ratios provided. Using building type #{primary_building_type} from the first ratio as the primary building type. This determines the building form defaults.")

  # call create_bar_from_args_and_building_type_hash to generate bar
  args[:primary_building_type] = primary_building_type
  OpenstudioStandards::Geometry.create_bar_from_args_and_building_type_hash(model, args, building_type_hash)

  return true
end

.create_core_and_perimeter_polygons(length, width, footprint_origin_point = OpenStudio::Point3d.new(0.0, 0.0, 0.0), perimeter_zone_depth = OpenStudio.convert(15.0, 'ft', 'm').get) ⇒ Hash

create core and perimeter polygons from length width and origin

Parameters:

  • length (Double)

    length of building in meters

  • width (Double)

    width of building in meters

  • footprint_origin_point (OpenStudio::Point3d) (defaults to: OpenStudio::Point3d.new(0.0, 0.0, 0.0))

    Optional OpenStudio Point3d object for the new origin

  • perimeter_zone_depth (Double) (defaults to: OpenStudio.convert(15.0, 'ft', 'm').get)

    Optional perimeter zone depth in meters

Returns:

  • (Hash)

    Hash of point vectors that define the space geometry for each direction



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/openstudio-standards/geometry/create.rb', line 80

def self.create_core_and_perimeter_polygons(length, width,
                                            footprint_origin_point = OpenStudio::Point3d.new(0.0, 0.0, 0.0),
                                            perimeter_zone_depth = OpenStudio.convert(15.0, 'ft', 'm').get)
  # key is name, value is a hash, one item of which is polygon. Another could be space type.
  hash_of_point_vectors = {}

  # determine if core and perimeter zoning can be used
  if !(length > perimeter_zone_depth * 2.5 && width > perimeter_zone_depth * 2.5)
    # if any size is to small then just model floor as single zone, issue warning
    perimeter_zone_depth = 0.0
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Due to the size of the building modeling each floor as a single zone.')
  end

  x_delta = footprint_origin_point.x - (length / 2.0)
  y_delta = footprint_origin_point.y - (width / 2.0)
  z = 0
  nw_point = OpenStudio::Point3d.new(x_delta, y_delta + width, z)
  ne_point = OpenStudio::Point3d.new(x_delta + length, y_delta + width, z)
  se_point = OpenStudio::Point3d.new(x_delta + length, y_delta, z)
  sw_point = OpenStudio::Point3d.new(x_delta, y_delta, z)

  # Define polygons for a rectangular building
  if perimeter_zone_depth > 0
    perimeter_nw_point = nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
    perimeter_ne_point = ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
    perimeter_se_point = se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
    perimeter_sw_point = sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)

    west_polygon = OpenStudio::Point3dVector.new
    west_polygon << sw_point
    west_polygon << nw_point
    west_polygon << perimeter_nw_point
    west_polygon << perimeter_sw_point
    hash_of_point_vectors['West Perimeter Space'] = {}
    hash_of_point_vectors['West Perimeter Space'][:space_type] = nil # other methods being used by makeSpacesFromPolygons may have space types associated with each polygon but this doesn't.
    hash_of_point_vectors['West Perimeter Space'][:polygon] = west_polygon

    north_polygon = OpenStudio::Point3dVector.new
    north_polygon << nw_point
    north_polygon << ne_point
    north_polygon << perimeter_ne_point
    north_polygon << perimeter_nw_point
    hash_of_point_vectors['North Perimeter Space'] = {}
    hash_of_point_vectors['North Perimeter Space'][:space_type] = nil
    hash_of_point_vectors['North Perimeter Space'][:polygon] = north_polygon

    east_polygon = OpenStudio::Point3dVector.new
    east_polygon << ne_point
    east_polygon << se_point
    east_polygon << perimeter_se_point
    east_polygon << perimeter_ne_point
    hash_of_point_vectors['East Perimeter Space'] = {}
    hash_of_point_vectors['East Perimeter Space'][:space_type] = nil
    hash_of_point_vectors['East Perimeter Space'][:polygon] = east_polygon

    south_polygon = OpenStudio::Point3dVector.new
    south_polygon << se_point
    south_polygon << sw_point
    south_polygon << perimeter_sw_point
    south_polygon << perimeter_se_point
    hash_of_point_vectors['South Perimeter Space'] = {}
    hash_of_point_vectors['South Perimeter Space'][:space_type] = nil
    hash_of_point_vectors['South Perimeter Space'][:polygon] = south_polygon

    core_polygon = OpenStudio::Point3dVector.new
    core_polygon << perimeter_sw_point
    core_polygon << perimeter_nw_point
    core_polygon << perimeter_ne_point
    core_polygon << perimeter_se_point
    hash_of_point_vectors['Core Space'] = {}
    hash_of_point_vectors['Core Space'][:space_type] = nil
    hash_of_point_vectors['Core Space'][:polygon] = core_polygon

    # Minimal zones
  else
    whole_story_polygon = OpenStudio::Point3dVector.new
    whole_story_polygon << sw_point
    whole_story_polygon << nw_point
    whole_story_polygon << ne_point
    whole_story_polygon << se_point
    hash_of_point_vectors['Whole Story Space'] = {}
    hash_of_point_vectors['Whole Story Space'][:space_type] = nil
    hash_of_point_vectors['Whole Story Space'][:polygon] = whole_story_polygon
  end

  return hash_of_point_vectors
end

.create_shape_aspect_ratio(model, aspect_ratio = 0.5, floor_area = 1000.0, rotation = 0.0, num_floors = 3, floor_to_floor_height = 3.8, plenum_height = 1.0, perimeter_zone_depth = 4.57) ⇒ OpenStudio::Model::Model

Create a Rectangle shape in a model based on a given aspect ratio

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • aspect_ratio (Double) (defaults to: 0.5)

    Aspect ratio

  • floor_area (Double) (defaults to: 1000.0)

    Building floor area in m2

  • rotation (Double) (defaults to: 0.0)

    Building rotation in degrees from North

  • num_floors (Integer) (defaults to: 3)

    Number of floors

  • floor_to_floor_height (Double) (defaults to: 3.8)

    Floor to floor height in meters

  • plenum_height (Double) (defaults to: 1.0)

    Plenum height in meters

  • perimeter_zone_depth (Double) (defaults to: 4.57)

    Perimeter zone depth in meters

Returns:

  • (OpenStudio::Model::Model)

    OpenStudio model object



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/openstudio-standards/geometry/create_shape.rb', line 199

def self.create_shape_aspect_ratio(model,
                                   aspect_ratio = 0.5,
                                   floor_area = 1000.0,
                                   rotation = 0.0,
                                   num_floors = 3,
                                   floor_to_floor_height = 3.8,
                                   plenum_height = 1.0,
                                   perimeter_zone_depth = 4.57)
  # determine length and width
  length = Math.sqrt((floor_area / (num_floors * 1.0)) / aspect_ratio)
  width = Math.sqrt((floor_area / (num_floors * 1.0)) * aspect_ratio)
  OpenstudioStandards::Geometry.create_shape_rectangle(model,
                                                       length = length,
                                                       width = width,
                                                       above_ground_storys = num_floors,
                                                       under_ground_storys = 0,
                                                       floor_to_floor_height = floor_to_floor_height,
                                                       plenum_height = plenum_height,
                                                       perimeter_zone_depth = perimeter_zone_depth)
  BTAP::Geometry.rotate_model(model, rotation)

  return model
end

.create_shape_courtyard(model, length = 50.0, width = 30.0, courtyard_length = 15.0, courtyard_width = 5.0, num_floors = 3, floor_to_floor_height = 3.8, plenum_height = 1.0, perimeter_zone_depth = 4.57) ⇒ OpenStudio::Model::Model

Create a Courtyard shape in a model

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • length (Double) (defaults to: 50.0)

    Building length in meters

  • width (Double) (defaults to: 30.0)

    Building width in meters

  • courtyard_length (Double) (defaults to: 15.0)

    Courtyard depth in meters

  • courtyard_width (Double) (defaults to: 5.0)

    Courtyard width in meters

  • num_floors (Integer) (defaults to: 3)

    Number of floors

  • floor_to_floor_height (Double) (defaults to: 3.8)

    Floor to floor height in meters

  • plenum_height (Double) (defaults to: 1.0)

    Plenum height in meters

  • perimeter_zone_depth (Double) (defaults to: 4.57)

    Perimeter zone depth in meters

Returns:

  • (OpenStudio::Model::Model)

    OpenStudio model object



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# File 'lib/openstudio-standards/geometry/create_shape.rb', line 235

def self.create_shape_courtyard(model,
                                length = 50.0,
                                width = 30.0,
                                courtyard_length = 15.0,
                                courtyard_width = 5.0,
                                num_floors = 3,
                                floor_to_floor_height = 3.8,
                                plenum_height = 1.0,
                                perimeter_zone_depth = 4.57)
  if length <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Length must be greater than 0.')
    return nil
  end

  if width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Width must be greater than 0.')
    return nil
  end

  if courtyard_length <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Courtyard length must be greater than 0.')
    return nil
  end

  if courtyard_width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Courtyard width must be greater than 0.')
    return nil
  end

  if num_floors <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Number of floors must be greater than 0.')
    return nil
  end

  if floor_to_floor_height <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Floor to floor height must be greater than 0.')
    return nil
  end

  if plenum_height < 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Plenum height must be greater than 0.')
    return nil
  end

  shortest_side = [length, width].min
  if perimeter_zone_depth < 0 || 4 * perimeter_zone_depth >= (shortest_side - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Perimeter zone depth must be greater than or equal to 0 and less than #{shortest_side / 4.0}m.")
    return nil
  end

  if courtyard_length >= (length - (4 * perimeter_zone_depth) - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Courtyard length must be less than #{length - (4.0 * perimeter_zone_depth)}m.")
    return nil
  end

  if courtyard_width >= (width - (4 * perimeter_zone_depth) - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Courtyard width must be less than #{width - (4.0 * perimeter_zone_depth)}m.")
    return nil
  end

  # Loop through the number of floors
  for floor in (0..num_floors - 1)
    z = floor_to_floor_height * floor

    # Create a new story within the building
    story = OpenStudio::Model::BuildingStory.new(model)
    story.setNominalFloortoFloorHeight(floor_to_floor_height)
    story.setName("Story #{floor + 1}")

    nw_point = OpenStudio::Point3d.new(0.0, width, z)
    ne_point = OpenStudio::Point3d.new(length, width, z)
    se_point = OpenStudio::Point3d.new(length, 0.0, z)
    sw_point = OpenStudio::Point3d.new(0.0, 0.0, z)

    courtyard_nw_point = OpenStudio::Point3d.new((length - courtyard_length) / 2.0, ((width - courtyard_width) / 2.0) + courtyard_width, z)
    courtyard_ne_point = OpenStudio::Point3d.new(((length - courtyard_length) / 2.0) + courtyard_length, ((width - courtyard_width) / 2.0) + courtyard_width, z)
    courtyard_se_point = OpenStudio::Point3d.new(((length - courtyard_length) / 2.0) + courtyard_length, (width - courtyard_width) / 2.0, z)
    courtyard_sw_point = OpenStudio::Point3d.new((length - courtyard_length) / 2.0, (width - courtyard_width) / 2.0, z)

    # Identity matrix for setting space origins
    m = OpenStudio::Matrix.new(4, 4, 0.0)
    m[0, 0] = 1.0
    m[1, 1] = 1.0
    m[2, 2] = 1.0
    m[3, 3] = 1.0

    # Define polygons for a building with a courtyard
    if perimeter_zone_depth > 0
      outer_perimeter_nw_point = nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0.0)
      outer_perimeter_ne_point = ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0.0)
      outer_perimeter_se_point = se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0.0)
      outer_perimeter_sw_point = sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0.0)
      inner_perimeter_nw_point = courtyard_nw_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0.0)
      inner_perimeter_ne_point = courtyard_ne_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0.0)
      inner_perimeter_se_point = courtyard_se_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0.0)
      inner_perimeter_sw_point = courtyard_sw_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0.0)

      west_outer_perimeter_polygon = OpenStudio::Point3dVector.new
      west_outer_perimeter_polygon << sw_point
      west_outer_perimeter_polygon << nw_point
      west_outer_perimeter_polygon << outer_perimeter_nw_point
      west_outer_perimeter_polygon << outer_perimeter_sw_point
      west_outer_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_outer_perimeter_polygon, floor_to_floor_height, model)
      west_outer_perimeter_space = west_outer_perimeter_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      west_outer_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_outer_perimeter_space.setBuildingStory(story)
      west_outer_perimeter_space.setName("Story #{floor + 1} West Outer Perimeter Space")

      north_outer_perimeter_polygon = OpenStudio::Point3dVector.new
      north_outer_perimeter_polygon << nw_point
      north_outer_perimeter_polygon << ne_point
      north_outer_perimeter_polygon << outer_perimeter_ne_point
      north_outer_perimeter_polygon << outer_perimeter_nw_point
      north_outer_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_outer_perimeter_polygon, floor_to_floor_height, model)
      north_outer_perimeter_space = north_outer_perimeter_space.get
      m[0, 3] = outer_perimeter_nw_point.x
      m[1, 3] = outer_perimeter_nw_point.y
      m[2, 3] = outer_perimeter_nw_point.z
      north_outer_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_outer_perimeter_space.setBuildingStory(story)
      north_outer_perimeter_space.setName("Story #{floor + 1} North Outer Perimeter Space")

      east_outer_perimeter_polygon = OpenStudio::Point3dVector.new
      east_outer_perimeter_polygon << ne_point
      east_outer_perimeter_polygon << se_point
      east_outer_perimeter_polygon << outer_perimeter_se_point
      east_outer_perimeter_polygon << outer_perimeter_ne_point
      east_outer_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_outer_perimeter_polygon, floor_to_floor_height, model)
      east_outer_perimeter_space = east_outer_perimeter_space.get
      m[0, 3] = outer_perimeter_se_point.x
      m[1, 3] = outer_perimeter_se_point.y
      m[2, 3] = outer_perimeter_se_point.z
      east_outer_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_outer_perimeter_space.setBuildingStory(story)
      east_outer_perimeter_space.setName("Story #{floor + 1} East Outer Perimeter Space")

      south_outer_perimeter_polygon = OpenStudio::Point3dVector.new
      south_outer_perimeter_polygon << se_point
      south_outer_perimeter_polygon << sw_point
      south_outer_perimeter_polygon << outer_perimeter_sw_point
      south_outer_perimeter_polygon << outer_perimeter_se_point
      south_outer_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_outer_perimeter_polygon, floor_to_floor_height, model)
      south_outer_perimeter_space = south_outer_perimeter_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      south_outer_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_outer_perimeter_space.setBuildingStory(story)
      south_outer_perimeter_space.setName("Story #{floor + 1} South Outer Perimeter Space")

      west_core_polygon = OpenStudio::Point3dVector.new
      west_core_polygon << outer_perimeter_sw_point
      west_core_polygon << outer_perimeter_nw_point
      west_core_polygon << inner_perimeter_nw_point
      west_core_polygon << inner_perimeter_sw_point
      west_core_space = OpenStudio::Model::Space.fromFloorPrint(west_core_polygon, floor_to_floor_height, model)
      west_core_space = west_core_space.get
      m[0, 3] = outer_perimeter_sw_point.x
      m[1, 3] = outer_perimeter_sw_point.y
      m[2, 3] = outer_perimeter_sw_point.z
      west_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_core_space.setBuildingStory(story)
      west_core_space.setName("Story #{floor + 1} West Core Space")

      north_core_polygon = OpenStudio::Point3dVector.new
      north_core_polygon << outer_perimeter_nw_point
      north_core_polygon << outer_perimeter_ne_point
      north_core_polygon << inner_perimeter_ne_point
      north_core_polygon << inner_perimeter_nw_point
      north_core_space = OpenStudio::Model::Space.fromFloorPrint(north_core_polygon, floor_to_floor_height, model)
      north_core_space = north_core_space.get
      m[0, 3] = inner_perimeter_nw_point.x
      m[1, 3] = inner_perimeter_nw_point.y
      m[2, 3] = inner_perimeter_nw_point.z
      north_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_core_space.setBuildingStory(story)
      north_core_space.setName("Story #{floor + 1} North Core Space")

      east_core_polygon = OpenStudio::Point3dVector.new
      east_core_polygon << outer_perimeter_ne_point
      east_core_polygon << outer_perimeter_se_point
      east_core_polygon << inner_perimeter_se_point
      east_core_polygon << inner_perimeter_ne_point
      east_core_space = OpenStudio::Model::Space.fromFloorPrint(east_core_polygon, floor_to_floor_height, model)
      east_core_space = east_core_space.get
      m[0, 3] = inner_perimeter_se_point.x
      m[1, 3] = inner_perimeter_se_point.y
      m[2, 3] = inner_perimeter_se_point.z
      east_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_core_space.setBuildingStory(story)
      east_core_space.setName("Story #{floor + 1} East Core Space")

      south_core_polygon = OpenStudio::Point3dVector.new
      south_core_polygon << outer_perimeter_se_point
      south_core_polygon << outer_perimeter_sw_point
      south_core_polygon << inner_perimeter_sw_point
      south_core_polygon << inner_perimeter_se_point
      south_core_space = OpenStudio::Model::Space.fromFloorPrint(south_core_polygon, floor_to_floor_height, model)
      south_core_space = south_core_space.get
      m[0, 3] = outer_perimeter_sw_point.x
      m[1, 3] = outer_perimeter_sw_point.y
      m[2, 3] = outer_perimeter_sw_point.z
      south_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_core_space.setBuildingStory(story)
      south_core_space.setName("Story #{floor + 1} South Core Space")

      west_inner_perimeter_polygon = OpenStudio::Point3dVector.new
      west_inner_perimeter_polygon << inner_perimeter_sw_point
      west_inner_perimeter_polygon << inner_perimeter_nw_point
      west_inner_perimeter_polygon << courtyard_nw_point
      west_inner_perimeter_polygon << courtyard_sw_point
      west_inner_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_inner_perimeter_polygon, floor_to_floor_height, model)
      west_inner_perimeter_space = west_inner_perimeter_space.get
      m[0, 3] = inner_perimeter_sw_point.x
      m[1, 3] = inner_perimeter_sw_point.y
      m[2, 3] = inner_perimeter_sw_point.z
      west_inner_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_inner_perimeter_space.setBuildingStory(story)
      west_inner_perimeter_space.setName("Story #{floor + 1} West Inner Perimeter Space")

      north_inner_perimeter_polygon = OpenStudio::Point3dVector.new
      north_inner_perimeter_polygon << inner_perimeter_nw_point
      north_inner_perimeter_polygon << inner_perimeter_ne_point
      north_inner_perimeter_polygon << courtyard_ne_point
      north_inner_perimeter_polygon << courtyard_nw_point
      north_inner_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_inner_perimeter_polygon, floor_to_floor_height, model)
      north_inner_perimeter_space = north_inner_perimeter_space.get
      m[0, 3] = courtyard_nw_point.x
      m[1, 3] = courtyard_nw_point.y
      m[2, 3] = courtyard_nw_point.z
      north_inner_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_inner_perimeter_space.setBuildingStory(story)
      north_inner_perimeter_space.setName("Story #{floor + 1} North Inner Perimeter Space")

      east_inner_perimeter_polygon = OpenStudio::Point3dVector.new
      east_inner_perimeter_polygon << inner_perimeter_ne_point
      east_inner_perimeter_polygon << inner_perimeter_se_point
      east_inner_perimeter_polygon << courtyard_se_point
      east_inner_perimeter_polygon << courtyard_ne_point
      east_inner_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_inner_perimeter_polygon, floor_to_floor_height, model)
      east_inner_perimeter_space = east_inner_perimeter_space.get
      m[0, 3] = courtyard_se_point.x
      m[1, 3] = courtyard_se_point.y
      m[2, 3] = courtyard_se_point.z
      east_inner_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_inner_perimeter_space.setBuildingStory(story)
      east_inner_perimeter_space.setName("Story #{floor + 1} East Inner Perimeter Space")

      south_inner_perimeter_polygon = OpenStudio::Point3dVector.new
      south_inner_perimeter_polygon << inner_perimeter_se_point
      south_inner_perimeter_polygon << inner_perimeter_sw_point
      south_inner_perimeter_polygon << courtyard_sw_point
      south_inner_perimeter_polygon << courtyard_se_point
      south_inner_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_inner_perimeter_polygon, floor_to_floor_height, model)
      south_inner_perimeter_space = south_inner_perimeter_space.get
      m[0, 3] = inner_perimeter_sw_point.x
      m[1, 3] = inner_perimeter_sw_point.y
      m[2, 3] = inner_perimeter_sw_point.z
      south_inner_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_inner_perimeter_space.setBuildingStory(story)
      south_inner_perimeter_space.setName("Story #{floor + 1} South Inner Perimeter Space")
    else
      # Minimal zones
      west_polygon = OpenStudio::Point3dVector.new
      west_polygon << sw_point
      west_polygon << nw_point
      west_polygon << courtyard_nw_point
      west_polygon << courtyard_sw_point
      west_space = OpenStudio::Model::Space.fromFloorPrint(west_polygon, floor_to_floor_height, model)
      west_space = west_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      west_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_space.setBuildingStory(story)
      west_space.setName("Story #{floor + 1} West Space")

      north_polygon = OpenStudio::Point3dVector.new
      north_polygon << nw_point
      north_polygon << ne_point
      north_polygon << courtyard_ne_point
      north_polygon << courtyard_nw_point
      north_space = OpenStudio::Model::Space.fromFloorPrint(north_polygon, floor_to_floor_height, model)
      north_space = north_space.get
      m[0, 3] = courtyard_nw_point.x
      m[1, 3] = courtyard_nw_point.y
      m[2, 3] = courtyard_nw_point.z
      north_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_space.setBuildingStory(story)
      north_space.setName("Story #{floor + 1} North Space")

      east_polygon = OpenStudio::Point3dVector.new
      east_polygon << ne_point
      east_polygon << se_point
      east_polygon << courtyard_se_point
      east_polygon << courtyard_ne_point
      east_space = OpenStudio::Model::Space.fromFloorPrint(east_polygon, floor_to_floor_height, model)
      east_space = east_space.get
      m[0, 3] = courtyard_se_point.x
      m[1, 3] = courtyard_se_point.y
      m[2, 3] = courtyard_se_point.z
      east_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_space.setBuildingStory(story)
      east_space.setName("Story #{floor + 1} East Space")

      south_polygon = OpenStudio::Point3dVector.new
      south_polygon << se_point
      south_polygon << sw_point
      south_polygon << courtyard_sw_point
      south_polygon << courtyard_se_point
      south_space = OpenStudio::Model::Space.fromFloorPrint(south_polygon, floor_to_floor_height, model)
      south_space = south_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      south_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_space.setBuildingStory(story)
      south_space.setName("Story #{floor + 1} South Space")
    end
    # Set vertical story position
    story.setNominalZCoordinate(z)
  end
  BTAP::Geometry.match_surfaces(model)

  return model
end

.create_shape_h(model, length = 40.0, left_width = 40.0, center_width = 10.0, right_width = 40.0, left_end_length = 15.0, right_end_length = 15.0, left_upper_end_offset = 15.0, right_upper_end_offset = 15.0, num_floors = 3, floor_to_floor_height = 3.8, plenum_height = 1, perimeter_zone_depth = 4.57) ⇒ OpenStudio::Model::Model

Create an H shape in a model

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • length (Double) (defaults to: 40.0)

    Building length in meters

  • left_width (Double) (defaults to: 40.0)

    Left width in meters

  • center_width (Double) (defaults to: 10.0)

    Center width in meters

  • right_width (Double) (defaults to: 40.0)

    Right width in meters

  • left_end_length (Double) (defaults to: 15.0)

    Left end length in meters

  • right_end_length (Double) (defaults to: 15.0)

    Right end length in meters

  • left_upper_end_offset (Double) (defaults to: 15.0)

    Left upper end offset in meters

  • right_upper_end_offset (Double) (defaults to: 15.0)

    Right upper end offset in meters

  • num_floors (Integer) (defaults to: 3)

    Number of floors

  • floor_to_floor_height (Double) (defaults to: 3.8)

    Floor to floor height in meters

  • plenum_height (Double) (defaults to: 1)

    Plenum height in meters

  • perimeter_zone_depth (Double) (defaults to: 4.57)

    Perimeter zone depth in meters

Returns:

  • (OpenStudio::Model::Model)

    OpenStudio model object



581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
# File 'lib/openstudio-standards/geometry/create_shape.rb', line 581

def self.create_shape_h(model,
                        length = 40.0,
                        left_width = 40.0,
                        center_width = 10.0,
                        right_width = 40.0,
                        left_end_length = 15.0,
                        right_end_length = 15.0,
                        left_upper_end_offset = 15.0,
                        right_upper_end_offset = 15.0,
                        num_floors = 3,
                        floor_to_floor_height = 3.8,
                        plenum_height = 1,
                        perimeter_zone_depth = 4.57)
  if length <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Length must be greater than 0.')
    return nil
  end

  if left_width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Left width must be greater than 0.')
    return nil
  end

  if right_width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Right width must be greater than 0.')
    return nil
  end

  if center_width <= 1e-4 || center_width >= ([left_width, right_width].min - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Center width must be greater than 0 and less than #{[left_width, right_width].min}m.")
    return nil
  end

  if left_end_length <= 1e-4 || left_end_length >= (length - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Left end length must be greater than 0 and less than #{length}m.")
    return nil
  end

  if right_end_length <= 1e-4 || right_end_length >= (length - left_end_length - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Right end length must be greater than 0 and less than #{length - left_end_length}m.")
    return nil
  end

  if left_upper_end_offset <= 1e-4 || left_upper_end_offset >= (left_width - center_width - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Left upper end offset must be greater than 0 and less than #{left_width - center_width}m.")
    return nil
  end

  if right_upper_end_offset <= 1e-4 || right_upper_end_offset >= (right_width - center_width - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Right upper end offset must be greater than 0 and less than #{right_width - center_width}m.")
    return nil
  end

  if num_floors <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Number of floors must be greater than 0.')
    return nil
  end

  if floor_to_floor_height <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Floor to floor height must be greater than 0.')
    return nil
  end

  if plenum_height < 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Plenum height must be greater than 0.')
    return nil
  end

  shortest_side = [length / 2, left_width, center_width, right_width, left_end_length, right_end_length].min
  if perimeter_zone_depth < 0 || 2 * perimeter_zone_depth >= (shortest_side - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Perimeter zone depth must be greater than or equal to 0 and less than #{shortest_side / 2}m.")
    return nil
  end

  # Loop through the number of floors
  for floor in (0..num_floors - 1)
    z = floor_to_floor_height * floor

    # Create a new story within the building
    story = OpenStudio::Model::BuildingStory.new(model)
    story.setNominalFloortoFloorHeight(floor_to_floor_height)
    story.setName("Story #{floor + 1}")

    left_origin = (right_width - right_upper_end_offset) > (left_width - left_upper_end_offset) ? (right_width - right_upper_end_offset) - (left_width - left_upper_end_offset) : 0
    left_nw_point = OpenStudio::Point3d.new(0, left_width + left_origin, z)
    left_ne_point = OpenStudio::Point3d.new(left_end_length, left_width + left_origin, z)
    left_se_point = OpenStudio::Point3d.new(left_end_length, left_origin, z)
    left_sw_point = OpenStudio::Point3d.new(0, left_origin, z)
    center_nw_point = OpenStudio::Point3d.new(left_end_length, left_ne_point.y - left_upper_end_offset, z)
    center_ne_point = OpenStudio::Point3d.new(length - right_end_length, center_nw_point.y, z)
    center_se_point = OpenStudio::Point3d.new(length - right_end_length, center_nw_point.y - center_width, z)
    center_sw_point = OpenStudio::Point3d.new(left_end_length, center_se_point.y, z)
    right_nw_point = OpenStudio::Point3d.new(length - right_end_length, center_ne_point.y + right_upper_end_offset, z)
    right_ne_point = OpenStudio::Point3d.new(length, right_nw_point.y, z)
    right_se_point = OpenStudio::Point3d.new(length, right_ne_point.y - right_width, z)
    right_sw_point = OpenStudio::Point3d.new(length - right_end_length, right_se_point.y, z)

    # Identity matrix for setting space origins
    m = OpenStudio::Matrix.new(4, 4, 0)
    m[0, 0] = 1
    m[1, 1] = 1
    m[2, 2] = 1
    m[3, 3] = 1

    # Define polygons for an H-shape building with perimeter core zoning
    if perimeter_zone_depth > 0
      perimeter_left_nw_point = left_nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_left_ne_point = left_ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_left_se_point = left_se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_left_sw_point = left_sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_center_nw_point = center_nw_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_center_ne_point = center_ne_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_center_se_point = center_se_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_center_sw_point = center_sw_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_right_nw_point = right_nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_right_ne_point = right_ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_right_se_point = right_se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_right_sw_point = right_sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)

      west_left_perimeter_polygon = OpenStudio::Point3dVector.new
      west_left_perimeter_polygon << left_sw_point
      west_left_perimeter_polygon << left_nw_point
      west_left_perimeter_polygon << perimeter_left_nw_point
      west_left_perimeter_polygon << perimeter_left_sw_point
      west_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_left_perimeter_polygon, floor_to_floor_height, model)
      west_left_perimeter_space = west_left_perimeter_space.get
      m[0, 3] = left_sw_point.x
      m[1, 3] = left_sw_point.y
      m[2, 3] = left_sw_point.z
      west_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_left_perimeter_space.setBuildingStory(story)
      west_left_perimeter_space.setName("Story #{floor + 1} West Left Perimeter Space")

      north_left_perimeter_polygon = OpenStudio::Point3dVector.new
      north_left_perimeter_polygon << left_nw_point
      north_left_perimeter_polygon << left_ne_point
      north_left_perimeter_polygon << perimeter_left_ne_point
      north_left_perimeter_polygon << perimeter_left_nw_point
      north_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_left_perimeter_polygon, floor_to_floor_height, model)
      north_left_perimeter_space = north_left_perimeter_space.get
      m[0, 3] = perimeter_left_nw_point.x
      m[1, 3] = perimeter_left_nw_point.y
      m[2, 3] = perimeter_left_nw_point.z
      north_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_left_perimeter_space.setBuildingStory(story)
      north_left_perimeter_space.setName("Story #{floor + 1} North Left Perimeter Space")

      east_upper_left_perimeter_polygon = OpenStudio::Point3dVector.new
      east_upper_left_perimeter_polygon << left_ne_point
      east_upper_left_perimeter_polygon << center_nw_point
      east_upper_left_perimeter_polygon << perimeter_center_nw_point
      east_upper_left_perimeter_polygon << perimeter_left_ne_point
      east_upper_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_upper_left_perimeter_polygon, floor_to_floor_height, model)
      east_upper_left_perimeter_space = east_upper_left_perimeter_space.get
      m[0, 3] = perimeter_center_nw_point.x
      m[1, 3] = perimeter_center_nw_point.y
      m[2, 3] = perimeter_center_nw_point.z
      east_upper_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_upper_left_perimeter_space.setBuildingStory(story)
      east_upper_left_perimeter_space.setName("Story #{floor + 1} East Upper Left Perimeter Space")

      north_center_perimeter_polygon = OpenStudio::Point3dVector.new
      north_center_perimeter_polygon << center_nw_point
      north_center_perimeter_polygon << center_ne_point
      north_center_perimeter_polygon << perimeter_center_ne_point
      north_center_perimeter_polygon << perimeter_center_nw_point
      north_center_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_center_perimeter_polygon, floor_to_floor_height, model)
      north_center_perimeter_space = north_center_perimeter_space.get
      m[0, 3] = perimeter_center_nw_point.x
      m[1, 3] = perimeter_center_nw_point.y
      m[2, 3] = perimeter_center_nw_point.z
      north_center_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_center_perimeter_space.setBuildingStory(story)
      north_center_perimeter_space.setName("Story #{floor + 1} North Center Perimeter Space")

      west_upper_right_perimeter_polygon = OpenStudio::Point3dVector.new
      west_upper_right_perimeter_polygon << center_ne_point
      west_upper_right_perimeter_polygon << right_nw_point
      west_upper_right_perimeter_polygon << perimeter_right_nw_point
      west_upper_right_perimeter_polygon << perimeter_center_ne_point
      west_upper_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_upper_right_perimeter_polygon, floor_to_floor_height, model)
      west_upper_right_perimeter_space = west_upper_right_perimeter_space.get
      m[0, 3] = center_ne_point.x
      m[1, 3] = center_ne_point.y
      m[2, 3] = center_ne_point.z
      west_upper_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_upper_right_perimeter_space.setBuildingStory(story)
      west_upper_right_perimeter_space.setName("Story #{floor + 1} West Upper Right Perimeter Space")

      north_right_perimeter_polygon = OpenStudio::Point3dVector.new
      north_right_perimeter_polygon << right_nw_point
      north_right_perimeter_polygon << right_ne_point
      north_right_perimeter_polygon << perimeter_right_ne_point
      north_right_perimeter_polygon << perimeter_right_nw_point
      north_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_right_perimeter_polygon, floor_to_floor_height, model)
      north_right_perimeter_space = north_right_perimeter_space.get
      m[0, 3] = perimeter_right_nw_point.x
      m[1, 3] = perimeter_right_nw_point.y
      m[2, 3] = perimeter_right_nw_point.z
      north_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_right_perimeter_space.setBuildingStory(story)
      north_right_perimeter_space.setName("Story #{floor + 1} North Right Perimeter Space")

      east_right_perimeter_polygon = OpenStudio::Point3dVector.new
      east_right_perimeter_polygon << right_ne_point
      east_right_perimeter_polygon << right_se_point
      east_right_perimeter_polygon << perimeter_right_se_point
      east_right_perimeter_polygon << perimeter_right_ne_point
      east_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_right_perimeter_polygon, floor_to_floor_height, model)
      east_right_perimeter_space = east_right_perimeter_space.get
      m[0, 3] = perimeter_right_se_point.x
      m[1, 3] = perimeter_right_se_point.y
      m[2, 3] = perimeter_right_se_point.z
      east_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_right_perimeter_space.setBuildingStory(story)
      east_right_perimeter_space.setName("Story #{floor + 1} East Right Perimeter Space")

      south_right_perimeter_polygon = OpenStudio::Point3dVector.new
      south_right_perimeter_polygon << right_se_point
      south_right_perimeter_polygon << right_sw_point
      south_right_perimeter_polygon << perimeter_right_sw_point
      south_right_perimeter_polygon << perimeter_right_se_point
      south_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_right_perimeter_polygon, floor_to_floor_height, model)
      south_right_perimeter_space = south_right_perimeter_space.get
      m[0, 3] = right_sw_point.x
      m[1, 3] = right_sw_point.y
      m[2, 3] = right_sw_point.z
      south_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_right_perimeter_space.setBuildingStory(story)
      south_right_perimeter_space.setName("Story #{floor + 1} South Right Perimeter Space")

      west_lower_right_perimeter_polygon = OpenStudio::Point3dVector.new
      west_lower_right_perimeter_polygon << right_sw_point
      west_lower_right_perimeter_polygon << center_se_point
      west_lower_right_perimeter_polygon << perimeter_center_se_point
      west_lower_right_perimeter_polygon << perimeter_right_sw_point
      west_lower_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_lower_right_perimeter_polygon, floor_to_floor_height, model)
      west_lower_right_perimeter_space = west_lower_right_perimeter_space.get
      m[0, 3] = right_sw_point.x
      m[1, 3] = right_sw_point.y
      m[2, 3] = right_sw_point.z
      west_lower_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_lower_right_perimeter_space.setBuildingStory(story)
      west_lower_right_perimeter_space.setName("Story #{floor + 1} West Lower Right Perimeter Space")

      south_center_perimeter_polygon = OpenStudio::Point3dVector.new
      south_center_perimeter_polygon << center_se_point
      south_center_perimeter_polygon << center_sw_point
      south_center_perimeter_polygon << perimeter_center_sw_point
      south_center_perimeter_polygon << perimeter_center_se_point
      south_center_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_center_perimeter_polygon, floor_to_floor_height, model)
      south_center_perimeter_space = south_center_perimeter_space.get
      m[0, 3] = center_sw_point.x
      m[1, 3] = center_sw_point.y
      m[2, 3] = center_sw_point.z
      south_center_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_center_perimeter_space.setBuildingStory(story)
      south_center_perimeter_space.setName("Story #{floor + 1} South Center Perimeter Space")

      east_lower_left_perimeter_polygon = OpenStudio::Point3dVector.new
      east_lower_left_perimeter_polygon << center_sw_point
      east_lower_left_perimeter_polygon << left_se_point
      east_lower_left_perimeter_polygon << perimeter_left_se_point
      east_lower_left_perimeter_polygon << perimeter_center_sw_point
      east_lower_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_lower_left_perimeter_polygon, floor_to_floor_height, model)
      east_lower_left_perimeter_space = east_lower_left_perimeter_space.get
      m[0, 3] = perimeter_left_se_point.x
      m[1, 3] = perimeter_left_se_point.y
      m[2, 3] = perimeter_left_se_point.z
      east_lower_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_lower_left_perimeter_space.setBuildingStory(story)
      east_lower_left_perimeter_space.setName("Story #{floor + 1} East Lower Left Perimeter Space")

      south_left_perimeter_polygon = OpenStudio::Point3dVector.new
      south_left_perimeter_polygon << left_se_point
      south_left_perimeter_polygon << left_sw_point
      south_left_perimeter_polygon << perimeter_left_sw_point
      south_left_perimeter_polygon << perimeter_left_se_point
      south_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_left_perimeter_polygon, floor_to_floor_height, model)
      south_left_perimeter_space = south_left_perimeter_space.get
      m[0, 3] = left_sw_point.x
      m[1, 3] = left_sw_point.y
      m[2, 3] = left_sw_point.z
      south_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_left_perimeter_space.setBuildingStory(story)
      south_left_perimeter_space.setName("Story #{floor + 1} South Left Perimeter Space")

      west_core_polygon = OpenStudio::Point3dVector.new
      west_core_polygon << perimeter_left_sw_point
      west_core_polygon << perimeter_left_nw_point
      west_core_polygon << perimeter_left_ne_point
      west_core_polygon << perimeter_center_nw_point
      west_core_polygon << perimeter_center_sw_point
      west_core_polygon << perimeter_left_se_point
      west_core_space = OpenStudio::Model::Space.fromFloorPrint(west_core_polygon, floor_to_floor_height, model)
      west_core_space = west_core_space.get
      m[0, 3] = perimeter_left_sw_point.x
      m[1, 3] = perimeter_left_sw_point.y
      m[2, 3] = perimeter_left_sw_point.z
      west_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_core_space.setBuildingStory(story)
      west_core_space.setName("Story #{floor + 1} West Core Space")

      center_core_polygon = OpenStudio::Point3dVector.new
      center_core_polygon << perimeter_center_sw_point
      center_core_polygon << perimeter_center_nw_point
      center_core_polygon << perimeter_center_ne_point
      center_core_polygon << perimeter_center_se_point
      center_core_space = OpenStudio::Model::Space.fromFloorPrint(center_core_polygon, floor_to_floor_height, model)
      center_core_space = center_core_space.get
      m[0, 3] = perimeter_center_sw_point.x
      m[1, 3] = perimeter_center_sw_point.y
      m[2, 3] = perimeter_center_sw_point.z
      center_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      center_core_space.setBuildingStory(story)
      center_core_space.setName("Story #{floor + 1} Center Core Space")

      east_core_polygon = OpenStudio::Point3dVector.new
      east_core_polygon << perimeter_right_sw_point
      east_core_polygon << perimeter_center_se_point
      east_core_polygon << perimeter_center_ne_point
      east_core_polygon << perimeter_right_nw_point
      east_core_polygon << perimeter_right_ne_point
      east_core_polygon << perimeter_right_se_point
      east_core_space = OpenStudio::Model::Space.fromFloorPrint(east_core_polygon, floor_to_floor_height, model)
      east_core_space = east_core_space.get
      m[0, 3] = perimeter_right_sw_point.x
      m[1, 3] = perimeter_right_sw_point.y
      m[2, 3] = perimeter_right_sw_point.z
      east_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_core_space.setBuildingStory(story)
      east_core_space.setName("Story #{floor + 1} East Core Space")
    else
      # Minimal zones
      west_polygon = OpenStudio::Point3dVector.new
      west_polygon << left_sw_point
      west_polygon << left_nw_point
      west_polygon << left_ne_point
      west_polygon << center_nw_point
      west_polygon << center_sw_point
      west_polygon << left_se_point
      west_space = OpenStudio::Model::Space.fromFloorPrint(west_polygon, floor_to_floor_height, model)
      west_space = west_space.get
      m[0, 3] = left_sw_point.x
      m[1, 3] = left_sw_point.y
      m[2, 3] = left_sw_point.z
      west_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_space.setBuildingStory(story)
      west_space.setName("Story #{floor + 1} West Space")

      center_polygon = OpenStudio::Point3dVector.new
      center_polygon << center_sw_point
      center_polygon << center_nw_point
      center_polygon << center_ne_point
      center_polygon << center_se_point
      center_space = OpenStudio::Model::Space.fromFloorPrint(center_polygon, floor_to_floor_height, model)
      center_space = center_space.get
      m[0, 3] = center_sw_point.x
      m[1, 3] = center_sw_point.y
      m[2, 3] = center_sw_point.z
      center_space.changeTransformation(OpenStudio::Transformation.new(m))
      center_space.setBuildingStory(story)
      center_space.setName("Story #{floor + 1} Center Space")

      east_polygon = OpenStudio::Point3dVector.new
      east_polygon << right_sw_point
      east_polygon << center_se_point
      east_polygon << center_ne_point
      east_polygon << right_nw_point
      east_polygon << right_ne_point
      east_polygon << right_se_point
      east_space = OpenStudio::Model::Space.fromFloorPrint(east_polygon, floor_to_floor_height, model)
      east_space = east_space.get
      m[0, 3] = right_sw_point.x
      m[1, 3] = right_sw_point.y
      m[2, 3] = right_sw_point.z
      east_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_space.setBuildingStory(story)
      east_space.setName("Story #{floor + 1} East Space")
    end
    # Set vertical story position
    story.setNominalZCoordinate(z)

  end
  BTAP::Geometry.match_surfaces(model)

  return model
end

.create_shape_l(model, length = 40.0, width = 40.0, lower_end_width = 20.0, upper_end_length = 20.0, num_floors = 3, floor_to_floor_height = 3.8, plenum_height = 1.0, perimeter_zone_depth = 4.57) ⇒ OpenStudio::Model::Model

Create an L shape in a model

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • length (Double) (defaults to: 40.0)

    Building length in meters

  • width (Double) (defaults to: 40.0)

    Building width in meters

  • lower_end_width (Double) (defaults to: 20.0)

    Lower end width in meters

  • upper_end_length (Double) (defaults to: 20.0)

    Upper end width in meters

  • num_floors (Integer) (defaults to: 3)

    Number of floors

  • floor_to_floor_height (Double) (defaults to: 3.8)

    Floor to floor height in meters

  • plenum_height (Double) (defaults to: 1.0)

    Plenum height in meters

  • perimeter_zone_depth (Double) (defaults to: 4.57)

    Perimeter zone depth in meters

Returns:

  • (OpenStudio::Model::Model)

    OpenStudio model object



982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
# File 'lib/openstudio-standards/geometry/create_shape.rb', line 982

def self.create_shape_l(model,
                        length = 40.0,
                        width = 40.0,
                        lower_end_width = 20.0,
                        upper_end_length = 20.0,
                        num_floors = 3,
                        floor_to_floor_height = 3.8,
                        plenum_height = 1.0,
                        perimeter_zone_depth = 4.57)
  if length <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Length must be greater than 0.')
    return nil
  end

  if width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Width must be greater than 0.')
    return nil
  end

  if lower_end_width <= 1e-4 || lower_end_width >= (width - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Lower end width must be greater than 0 and less than #{width}m.")
    return nil
  end

  if upper_end_length <= 1e-4 || upper_end_length >= (length - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Upper end length must be greater than 0 and less than #{length}m.")
    return nil
  end

  if num_floors <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Number of floors must be greater than 0.')
    return nil
  end

  if floor_to_floor_height <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Floor to floor height must be greater than 0.')
    return nil
  end

  if plenum_height < 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Plenum height must be greater than 0.')
    return nil
  end

  shortest_side = [lower_end_width, upper_end_length].min
  if perimeter_zone_depth < 0 || 2 * perimeter_zone_depth >= (shortest_side - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Perimeter zone depth must be greater than or equal to 0 and less than #{shortest_side / 2}m.")
    return nil
  end

  # Loop through the number of floors
  for floor in (0..num_floors - 1)
    z = floor_to_floor_height * floor

    # Create a new story within the building
    story = OpenStudio::Model::BuildingStory.new(model)
    story.setNominalFloortoFloorHeight(floor_to_floor_height)
    story.setName("Story #{floor + 1}")

    nw_point = OpenStudio::Point3d.new(0, width, z)
    upper_ne_point = OpenStudio::Point3d.new(upper_end_length, width, z)
    upper_sw_point = OpenStudio::Point3d.new(upper_end_length, lower_end_width, z)
    lower_ne_point = OpenStudio::Point3d.new(length, lower_end_width, z)
    se_point = OpenStudio::Point3d.new(length, 0, z)
    sw_point = OpenStudio::Point3d.new(0, 0, z)

    # Identity matrix for setting space origins
    m = OpenStudio::Matrix.new(4, 4, 0)
    m[0, 0] = 1
    m[1, 1] = 1
    m[2, 2] = 1
    m[3, 3] = 1

    # Define polygons for a L-shape building with perimeter core zoning
    if perimeter_zone_depth > 0
      perimeter_nw_point = nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_upper_ne_point = upper_ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_upper_sw_point = upper_sw_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_lower_ne_point = lower_ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_se_point = se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_lower_sw_point = sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)

      west_perimeter_polygon = OpenStudio::Point3dVector.new
      west_perimeter_polygon << sw_point
      west_perimeter_polygon << nw_point
      west_perimeter_polygon << perimeter_nw_point
      west_perimeter_polygon << perimeter_lower_sw_point
      west_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_perimeter_polygon, floor_to_floor_height, model)
      west_perimeter_space = west_perimeter_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      west_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_perimeter_space.setBuildingStory(story)
      west_perimeter_space.setName("Story #{floor + 1} West Perimeter Space")

      north_upper_perimeter_polygon = OpenStudio::Point3dVector.new
      north_upper_perimeter_polygon << nw_point
      north_upper_perimeter_polygon << upper_ne_point
      north_upper_perimeter_polygon << perimeter_upper_ne_point
      north_upper_perimeter_polygon << perimeter_nw_point
      north_upper_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_upper_perimeter_polygon, floor_to_floor_height, model)
      north_upper_perimeter_space = north_upper_perimeter_space.get
      m[0, 3] = perimeter_nw_point.x
      m[1, 3] = perimeter_nw_point.y
      m[2, 3] = perimeter_nw_point.z
      north_upper_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_upper_perimeter_space.setBuildingStory(story)
      north_upper_perimeter_space.setName("Story #{floor + 1} North Upper Perimeter Space")

      east_upper_perimeter_polygon = OpenStudio::Point3dVector.new
      east_upper_perimeter_polygon << upper_ne_point
      east_upper_perimeter_polygon << upper_sw_point
      east_upper_perimeter_polygon << perimeter_upper_sw_point
      east_upper_perimeter_polygon << perimeter_upper_ne_point
      east_upper_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_upper_perimeter_polygon, floor_to_floor_height, model)
      east_upper_perimeter_space = east_upper_perimeter_space.get
      m[0, 3] = perimeter_upper_sw_point.x
      m[1, 3] = perimeter_upper_sw_point.y
      m[2, 3] = perimeter_upper_sw_point.z
      east_upper_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_upper_perimeter_space.setBuildingStory(story)
      east_upper_perimeter_space.setName("Story #{floor + 1} East Upper Perimeter Space")

      north_lower_perimeter_polygon = OpenStudio::Point3dVector.new
      north_lower_perimeter_polygon << upper_sw_point
      north_lower_perimeter_polygon << lower_ne_point
      north_lower_perimeter_polygon << perimeter_lower_ne_point
      north_lower_perimeter_polygon << perimeter_upper_sw_point
      north_lower_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_lower_perimeter_polygon, floor_to_floor_height, model)
      north_lower_perimeter_space = north_lower_perimeter_space.get
      m[0, 3] = perimeter_upper_sw_point.x
      m[1, 3] = perimeter_upper_sw_point.y
      m[2, 3] = perimeter_upper_sw_point.z
      north_lower_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_lower_perimeter_space.setBuildingStory(story)
      north_lower_perimeter_space.setName("Story #{floor + 1} North Lower Perimeter Space")

      east_lower_perimeter_polygon = OpenStudio::Point3dVector.new
      east_lower_perimeter_polygon << lower_ne_point
      east_lower_perimeter_polygon << se_point
      east_lower_perimeter_polygon << perimeter_se_point
      east_lower_perimeter_polygon << perimeter_lower_ne_point
      east_lower_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_lower_perimeter_polygon, floor_to_floor_height, model)
      east_lower_perimeter_space = east_lower_perimeter_space.get
      m[0, 3] = perimeter_se_point.x
      m[1, 3] = perimeter_se_point.y
      m[2, 3] = perimeter_se_point.z
      east_lower_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_lower_perimeter_space.setBuildingStory(story)
      east_lower_perimeter_space.setName("Story #{floor + 1} East Lower Perimeter Space")

      south_perimeter_polygon = OpenStudio::Point3dVector.new
      south_perimeter_polygon << se_point
      south_perimeter_polygon << sw_point
      south_perimeter_polygon << perimeter_lower_sw_point
      south_perimeter_polygon << perimeter_se_point
      south_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_perimeter_polygon, floor_to_floor_height, model)
      south_perimeter_space = south_perimeter_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      south_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_perimeter_space.setBuildingStory(story)
      south_perimeter_space.setName("Story #{floor + 1} South Perimeter Space")

      west_core_polygon = OpenStudio::Point3dVector.new
      west_core_polygon << perimeter_lower_sw_point
      west_core_polygon << perimeter_nw_point
      west_core_polygon << perimeter_upper_ne_point
      west_core_polygon << perimeter_upper_sw_point
      west_core_space = OpenStudio::Model::Space.fromFloorPrint(west_core_polygon, floor_to_floor_height, model)
      west_core_space = west_core_space.get
      m[0, 3] = perimeter_lower_sw_point.x
      m[1, 3] = perimeter_lower_sw_point.y
      m[2, 3] = perimeter_lower_sw_point.z
      west_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_core_space.setBuildingStory(story)
      west_core_space.setName("Story #{floor + 1} West Core Space")

      east_core_polygon = OpenStudio::Point3dVector.new
      east_core_polygon << perimeter_upper_sw_point
      east_core_polygon << perimeter_lower_ne_point
      east_core_polygon << perimeter_se_point
      east_core_polygon << perimeter_lower_sw_point
      east_core_space = OpenStudio::Model::Space.fromFloorPrint(east_core_polygon, floor_to_floor_height, model)
      east_core_space = east_core_space.get
      m[0, 3] = perimeter_lower_sw_point.x
      m[1, 3] = perimeter_lower_sw_point.y
      m[2, 3] = perimeter_lower_sw_point.z
      east_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_core_space.setBuildingStory(story)
      east_core_space.setName("Story #{floor + 1} East Core Space")
    else
      # Minimal zones
      west_polygon = OpenStudio::Point3dVector.new
      west_polygon << sw_point
      west_polygon << nw_point
      west_polygon << upper_ne_point
      west_polygon << upper_sw_point
      west_space = OpenStudio::Model::Space.fromFloorPrint(west_polygon, floor_to_floor_height, model)
      west_space = west_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      west_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_space.setBuildingStory(story)
      west_space.setName("Story #{floor + 1} West Space")

      east_polygon = OpenStudio::Point3dVector.new
      east_polygon << sw_point
      east_polygon << upper_sw_point
      east_polygon << lower_ne_point
      east_polygon << se_point
      east_space = OpenStudio::Model::Space.fromFloorPrint(east_polygon, floor_to_floor_height, model)
      east_space = east_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      east_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_space.setBuildingStory(story)
      east_space.setName("Story #{floor + 1} East Space")
    end
    # Set vertical story position
    story.setNominalZCoordinate(z)
  end
  BTAP::Geometry.match_surfaces(model)

  return model
end

.create_shape_rectangle(model, length = 100.0, width = 100.0, above_ground_storys = 3, under_ground_storys = 1, floor_to_floor_height = 3.8, plenum_height = 1.0, perimeter_zone_depth = 4.57, initial_height = 0.0) ⇒ OpenStudio::Model::Model

Create a Rectangle shape in a model

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • length (Double) (defaults to: 100.0)

    Building length in meters

  • width (Double) (defaults to: 100.0)

    Building width in meters

  • above_ground_storys (Integer) (defaults to: 3)

    Number of above ground stories

  • under_ground_storys (Integer) (defaults to: 1)

    Number of below ground stories

  • floor_to_floor_height (Double) (defaults to: 3.8)

    Floor to floor height in meters

  • plenum_height (Double) (defaults to: 1.0)

    Plenum height in meters

  • perimeter_zone_depth (Double) (defaults to: 4.57)

    Perimeter zone depth in meters

  • initial_height (Double) (defaults to: 0.0)

    Initial height in meters

Returns:

  • (OpenStudio::Model::Model)

    OpenStudio model object



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/openstudio-standards/geometry/create_shape.rb', line 20

def self.create_shape_rectangle(model,
                                length = 100.0,
                                width = 100.0,
                                above_ground_storys = 3,
                                under_ground_storys = 1,
                                floor_to_floor_height = 3.8,
                                plenum_height = 1.0,
                                perimeter_zone_depth = 4.57,
                                initial_height = 0.0)
  if length <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Length must be greater than 0.')
    return nil
  end

  if width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Width must be greater than 0.')
    return nil
  end

  if (above_ground_storys + under_ground_storys) <= 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Number of floors must be greater than 0.')
    return nil
  end

  if floor_to_floor_height <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Floor to floor height must be greater than 0.')
    return nil
  end

  if plenum_height < 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Plenum height must be greater than 0.')
    return nil
  end

  shortest_side = [length, width].min
  if perimeter_zone_depth < 0 || 2 * perimeter_zone_depth >= (shortest_side - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Perimeter zone depth must be greater than or equal to 0 and less than half of the smaller of length and width, #{(shortest_side / 2).round(2)}m")
    return nil
  end

  # Loop through the number of floors
  building_stories = []
  for floor in ((under_ground_storys * -1)..above_ground_storys - 1)
    z = (floor_to_floor_height * floor) + initial_height

    # Create a new story within the building
    story = OpenStudio::Model::BuildingStory.new(model)
    story.setNominalFloortoFloorHeight(floor_to_floor_height)
    story.setName("Story #{floor + 1}")
    building_stories << story

    nw_point = OpenStudio::Point3d.new(0, width, z)
    ne_point = OpenStudio::Point3d.new(length, width, z)
    se_point = OpenStudio::Point3d.new(length, 0, z)
    sw_point = OpenStudio::Point3d.new(0, 0, z)

    # Identity matrix for setting space origins
    m = OpenStudio::Matrix.new(4, 4, 0)
    m[0, 0] = 1
    m[1, 1] = 1
    m[2, 2] = 1
    m[3, 3] = 1

    # Define polygons for a rectangular building
    if perimeter_zone_depth > 0
      perimeter_nw_point = nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_ne_point = ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_se_point = se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_sw_point = sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)

      west_polygon = OpenStudio::Point3dVector.new
      west_polygon << sw_point
      west_polygon << nw_point
      west_polygon << perimeter_nw_point
      west_polygon << perimeter_sw_point
      west_space = OpenStudio::Model::Space.fromFloorPrint(west_polygon, floor_to_floor_height, model)
      west_space = west_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      west_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_space.setBuildingStory(story)
      west_space.setName("Story #{floor + 1} West Perimeter Space")

      north_polygon = OpenStudio::Point3dVector.new
      north_polygon << nw_point
      north_polygon << ne_point
      north_polygon << perimeter_ne_point
      north_polygon << perimeter_nw_point
      north_space = OpenStudio::Model::Space.fromFloorPrint(north_polygon, floor_to_floor_height, model)
      north_space = north_space.get
      m[0, 3] = perimeter_nw_point.x
      m[1, 3] = perimeter_nw_point.y
      m[2, 3] = perimeter_nw_point.z
      north_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_space.setBuildingStory(story)
      north_space.setName("Story #{floor + 1} North Perimeter Space")

      east_polygon = OpenStudio::Point3dVector.new
      east_polygon << ne_point
      east_polygon << se_point
      east_polygon << perimeter_se_point
      east_polygon << perimeter_ne_point
      east_space = OpenStudio::Model::Space.fromFloorPrint(east_polygon, floor_to_floor_height, model)
      east_space = east_space.get
      m[0, 3] = perimeter_se_point.x
      m[1, 3] = perimeter_se_point.y
      m[2, 3] = perimeter_se_point.z
      east_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_space.setBuildingStory(story)
      east_space.setName("Story #{floor + 1} East Perimeter Space")

      south_polygon = OpenStudio::Point3dVector.new
      south_polygon << se_point
      south_polygon << sw_point
      south_polygon << perimeter_sw_point
      south_polygon << perimeter_se_point
      south_space = OpenStudio::Model::Space.fromFloorPrint(south_polygon, floor_to_floor_height, model)
      south_space = south_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      south_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_space.setBuildingStory(story)
      south_space.setName("Story #{floor + 1} South Perimeter Space")

      core_polygon = OpenStudio::Point3dVector.new
      core_polygon << perimeter_sw_point
      core_polygon << perimeter_nw_point
      core_polygon << perimeter_ne_point
      core_polygon << perimeter_se_point
      core_space = OpenStudio::Model::Space.fromFloorPrint(core_polygon, floor_to_floor_height, model)
      core_space = core_space.get
      m[0, 3] = perimeter_sw_point.x
      m[1, 3] = perimeter_sw_point.y
      m[2, 3] = perimeter_sw_point.z
      core_space.changeTransformation(OpenStudio::Transformation.new(m))
      core_space.setBuildingStory(story)
      core_space.setName("Story #{floor + 1} Core Space")
    else
      # Minimal zones
      core_polygon = OpenStudio::Point3dVector.new
      core_polygon << sw_point
      core_polygon << nw_point
      core_polygon << ne_point
      core_polygon << se_point
      core_space = OpenStudio::Model::Space.fromFloorPrint(core_polygon, floor_to_floor_height, model)
      core_space = core_space.get
      m[0, 3] = sw_point.x
      m[1, 3] = sw_point.y
      m[2, 3] = sw_point.z
      core_space.changeTransformation(OpenStudio::Transformation.new(m))
      core_space.setBuildingStory(story)
      core_space.setName("Story #{floor + 1} Core Space")
    end
    # Set vertical story position
    story.setNominalZCoordinate(z)

    # Ensure that underground stories (when z<0 have Ground set as Boundary conditions).
    # Apply the Ground BC to all surfaces, the top ceiling will be corrected below when the surface matching algorithm is called.
    underground_surfaces = story.spaces.flat_map(&:surfaces)
    BTAP::Geometry::Surfaces.set_surfaces_boundary_condition(model, underground_surfaces, 'Ground') if z < 0
  end

  BTAP::Geometry.match_surfaces(model)
  return model
end

.create_shape_t(model, length = 40.0, width = 40.0, upper_end_width = 20.0, lower_end_length = 20.0, left_end_offset = 10.0, num_floors = 3, floor_to_floor_height = 3.8, plenum_height = 1.0, perimeter_zone_depth = 4.57) ⇒ OpenStudio::Model::Model

Create a T shape in a model

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • length (Double) (defaults to: 40.0)

    Building length in meters

  • width (Double) (defaults to: 40.0)

    Building width in meters

  • upper_end_width (Double) (defaults to: 20.0)

    Upper end width in meters

  • lower_end_length (Double) (defaults to: 20.0)

    Lower end length in meters

  • left_end_offset (Double) (defaults to: 10.0)

    Left end offset in meters

  • num_floors (Integer) (defaults to: 3)

    Number of floors

  • floor_to_floor_height (Double) (defaults to: 3.8)

    Floor to floor height in meters

  • plenum_height (Double) (defaults to: 1.0)

    Plenum height in meters

  • perimeter_zone_depth (Double) (defaults to: 4.57)

    Perimeter zone depth in meters

Returns:

  • (OpenStudio::Model::Model)

    OpenStudio model object



1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
# File 'lib/openstudio-standards/geometry/create_shape.rb', line 1226

def self.create_shape_t(model,
                        length = 40.0,
                        width = 40.0,
                        upper_end_width = 20.0,
                        lower_end_length = 20.0,
                        left_end_offset = 10.0,
                        num_floors = 3,
                        floor_to_floor_height = 3.8,
                        plenum_height = 1.0,
                        perimeter_zone_depth = 4.57)
  if length <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Length must be greater than 0.')
    return nil
  end

  if width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Width must be greater than 0.')
    return nil
  end

  if upper_end_width <= 1e-4 || upper_end_width >= (width - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Upper end width must be greater than 0 and less than #{width}m.")
    return nil
  end

  if lower_end_length <= 1e-4 || lower_end_length >= (length - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Lower end length must be greater than 0 and less than #{length}m.")
    return nil
  end

  if left_end_offset <= 1e-4 || left_end_offset >= (length - lower_end_length - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Left end offset must be greater than 0 and less than #{length - lower_end_length}m.")
    return nil
  end

  if num_floors <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Number of floors must be greater than 0.')
    return nil
  end

  if floor_to_floor_height <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Floor to floor height must be greater than 0.')
    return nil
  end

  if plenum_height < 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Plenum height must be greater than 0.')
    return nil
  end

  shortest_side = [length, width, upper_end_width, lower_end_length].min
  if perimeter_zone_depth < 0 || 2 * perimeter_zone_depth >= (shortest_side - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Perimeter zone depth must be greater than or equal to 0 and less than #{shortest_side / 2}m.")
    return nil
  end

  # Loop through the number of floors
  for floor in (0..num_floors - 1)
    z = floor_to_floor_height * floor

    # Create a new story within the building
    story = OpenStudio::Model::BuildingStory.new(model)
    story.setNominalFloortoFloorHeight(floor_to_floor_height)
    story.setName("Story #{floor + 1}")

    lower_ne_point = OpenStudio::Point3d.new(left_end_offset, width - upper_end_width, z)
    upper_sw_point = OpenStudio::Point3d.new(0, width - upper_end_width, z)
    upper_nw_point = OpenStudio::Point3d.new(0, width, z)
    upper_ne_point = OpenStudio::Point3d.new(length, width, z)
    upper_se_point = OpenStudio::Point3d.new(length, width - upper_end_width, z)
    lower_nw_point = OpenStudio::Point3d.new(left_end_offset + lower_end_length, width - upper_end_width, z)
    lower_se_point = OpenStudio::Point3d.new(left_end_offset + lower_end_length, 0, z)
    lower_sw_point = OpenStudio::Point3d.new(left_end_offset, 0, z)

    # Identity matrix for setting space origins
    m = OpenStudio::Matrix.new(4, 4, 0)
    m[0, 0] = 1
    m[1, 1] = 1
    m[2, 2] = 1
    m[3, 3] = 1

    # Define polygons for a T-shape building with perimeter core zoning
    if perimeter_zone_depth > 0
      perimeter_lower_ne_point = lower_ne_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_upper_sw_point = upper_sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_upper_nw_point = upper_nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_upper_ne_point = upper_ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_upper_se_point = upper_se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_lower_nw_point = lower_nw_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_lower_se_point = lower_se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_lower_sw_point = lower_sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)

      west_lower_perimeter_polygon = OpenStudio::Point3dVector.new
      west_lower_perimeter_polygon << lower_sw_point
      west_lower_perimeter_polygon << lower_ne_point
      west_lower_perimeter_polygon << perimeter_lower_ne_point
      west_lower_perimeter_polygon << perimeter_lower_sw_point
      west_lower_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_lower_perimeter_polygon, floor_to_floor_height, model)
      west_lower_perimeter_space = west_lower_perimeter_space.get
      m[0, 3] = lower_sw_point.x
      m[1, 3] = lower_sw_point.y
      m[2, 3] = lower_sw_point.z
      west_lower_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_lower_perimeter_space.setBuildingStory(story)
      west_lower_perimeter_space.setName("Story #{floor + 1} West Lower Perimeter Space")

      south_upper_left_perimeter_polygon = OpenStudio::Point3dVector.new
      south_upper_left_perimeter_polygon << lower_ne_point
      south_upper_left_perimeter_polygon << upper_sw_point
      south_upper_left_perimeter_polygon << perimeter_upper_sw_point
      south_upper_left_perimeter_polygon << perimeter_lower_ne_point
      south_upper_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_upper_left_perimeter_polygon, floor_to_floor_height, model)
      south_upper_left_perimeter_space = south_upper_left_perimeter_space.get
      m[0, 3] = upper_sw_point.x
      m[1, 3] = upper_sw_point.y
      m[2, 3] = upper_sw_point.z
      south_upper_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_upper_left_perimeter_space.setBuildingStory(story)
      south_upper_left_perimeter_space.setName("Story #{floor + 1} South Upper Left Perimeter Space")

      west_upper_perimeter_polygon = OpenStudio::Point3dVector.new
      west_upper_perimeter_polygon << upper_sw_point
      west_upper_perimeter_polygon << upper_nw_point
      west_upper_perimeter_polygon << perimeter_upper_nw_point
      west_upper_perimeter_polygon << perimeter_upper_sw_point
      west_upper_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_upper_perimeter_polygon, floor_to_floor_height, model)
      west_upper_perimeter_space = west_upper_perimeter_space.get
      m[0, 3] = upper_sw_point.x
      m[1, 3] = upper_sw_point.y
      m[2, 3] = upper_sw_point.z
      west_upper_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_upper_perimeter_space.setBuildingStory(story)
      west_upper_perimeter_space.setName("Story #{floor + 1} West Upper Perimeter Space")

      north_perimeter_polygon = OpenStudio::Point3dVector.new
      north_perimeter_polygon << upper_nw_point
      north_perimeter_polygon << upper_ne_point
      north_perimeter_polygon << perimeter_upper_ne_point
      north_perimeter_polygon << perimeter_upper_nw_point
      north_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_perimeter_polygon, floor_to_floor_height, model)
      north_perimeter_space = north_perimeter_space.get
      m[0, 3] = perimeter_upper_nw_point.x
      m[1, 3] = perimeter_upper_nw_point.y
      m[2, 3] = perimeter_upper_nw_point.z
      north_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_perimeter_space.setBuildingStory(story)
      north_perimeter_space.setName("Story #{floor + 1} North Perimeter Space")

      east_upper_perimeter_polygon = OpenStudio::Point3dVector.new
      east_upper_perimeter_polygon << upper_ne_point
      east_upper_perimeter_polygon << upper_se_point
      east_upper_perimeter_polygon << perimeter_upper_se_point
      east_upper_perimeter_polygon << perimeter_upper_ne_point
      east_upper_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_upper_perimeter_polygon, floor_to_floor_height, model)
      east_upper_perimeter_space = east_upper_perimeter_space.get
      m[0, 3] = perimeter_upper_se_point.x
      m[1, 3] = perimeter_upper_se_point.y
      m[2, 3] = perimeter_upper_se_point.z
      east_upper_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_upper_perimeter_space.setBuildingStory(story)
      east_upper_perimeter_space.setName("Story #{floor + 1} East Upper Perimeter Space")

      south_upper_right_perimeter_polygon = OpenStudio::Point3dVector.new
      south_upper_right_perimeter_polygon << upper_se_point
      south_upper_right_perimeter_polygon << lower_nw_point
      south_upper_right_perimeter_polygon << perimeter_lower_nw_point
      south_upper_right_perimeter_polygon << perimeter_upper_se_point
      south_upper_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_upper_right_perimeter_polygon, floor_to_floor_height, model)
      south_upper_right_perimeter_space = south_upper_right_perimeter_space.get
      m[0, 3] = lower_nw_point.x
      m[1, 3] = lower_nw_point.y
      m[2, 3] = lower_nw_point.z
      south_upper_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_upper_right_perimeter_space.setBuildingStory(story)
      south_upper_right_perimeter_space.setName("Story #{floor + 1} South Upper Left Perimeter Space")

      east_lower_perimeter_polygon = OpenStudio::Point3dVector.new
      east_lower_perimeter_polygon << lower_nw_point
      east_lower_perimeter_polygon << lower_se_point
      east_lower_perimeter_polygon << perimeter_lower_se_point
      east_lower_perimeter_polygon << perimeter_lower_nw_point
      east_lower_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_lower_perimeter_polygon, floor_to_floor_height, model)
      east_lower_perimeter_space = east_lower_perimeter_space.get
      m[0, 3] = perimeter_lower_se_point.x
      m[1, 3] = perimeter_lower_se_point.y
      m[2, 3] = perimeter_lower_se_point.z
      east_lower_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_lower_perimeter_space.setBuildingStory(story)
      east_lower_perimeter_space.setName("Story #{floor + 1} East Lower Perimeter Space")

      south_lower_perimeter_polygon = OpenStudio::Point3dVector.new
      south_lower_perimeter_polygon << lower_se_point
      south_lower_perimeter_polygon << lower_sw_point
      south_lower_perimeter_polygon << perimeter_lower_sw_point
      south_lower_perimeter_polygon << perimeter_lower_se_point
      south_lower_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_lower_perimeter_polygon, floor_to_floor_height, model)
      south_lower_perimeter_space = south_lower_perimeter_space.get
      m[0, 3] = lower_sw_point.x
      m[1, 3] = lower_sw_point.y
      m[2, 3] = lower_sw_point.z
      south_lower_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_lower_perimeter_space.setBuildingStory(story)
      south_lower_perimeter_space.setName("Story #{floor + 1} South Lower Perimeter Space")

      north_core_polygon = OpenStudio::Point3dVector.new
      north_core_polygon << perimeter_upper_sw_point
      north_core_polygon << perimeter_upper_nw_point
      north_core_polygon << perimeter_upper_ne_point
      north_core_polygon << perimeter_upper_se_point
      north_core_polygon << perimeter_lower_nw_point
      north_core_polygon << perimeter_lower_ne_point
      north_core_space = OpenStudio::Model::Space.fromFloorPrint(north_core_polygon, floor_to_floor_height, model)
      north_core_space = north_core_space.get
      m[0, 3] = perimeter_upper_sw_point.x
      m[1, 3] = perimeter_upper_sw_point.y
      m[2, 3] = perimeter_upper_sw_point.z
      north_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_core_space.setBuildingStory(story)
      north_core_space.setName("Story #{floor + 1} North Core Space")

      south_core_polygon = OpenStudio::Point3dVector.new
      south_core_polygon << perimeter_lower_sw_point
      south_core_polygon << perimeter_lower_ne_point
      south_core_polygon << perimeter_lower_nw_point
      south_core_polygon << perimeter_lower_se_point
      south_core_space = OpenStudio::Model::Space.fromFloorPrint(south_core_polygon, floor_to_floor_height, model)
      south_core_space = south_core_space.get
      m[0, 3] = perimeter_lower_sw_point.x
      m[1, 3] = perimeter_lower_sw_point.y
      m[2, 3] = perimeter_lower_sw_point.z
      south_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_core_space.setBuildingStory(story)
      south_core_space.setName("Story #{floor + 1} South Core Space")
    else
      # Minimal zones
      north_polygon = OpenStudio::Point3dVector.new
      north_polygon << upper_sw_point
      north_polygon << upper_nw_point
      north_polygon << upper_ne_point
      north_polygon << upper_se_point
      north_polygon << lower_nw_point
      north_polygon << lower_ne_point
      north_space = OpenStudio::Model::Space.fromFloorPrint(north_polygon, floor_to_floor_height, model)
      north_space = north_space.get
      m[0, 3] = upper_sw_point.x
      m[1, 3] = upper_sw_point.y
      m[2, 3] = upper_sw_point.z
      north_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_space.setBuildingStory(story)
      north_space.setName("Story #{floor + 1} North Space")

      south_polygon = OpenStudio::Point3dVector.new
      south_polygon << lower_sw_point
      south_polygon << lower_ne_point
      south_polygon << lower_nw_point
      south_polygon << lower_se_point
      south_space = OpenStudio::Model::Space.fromFloorPrint(south_polygon, floor_to_floor_height, model)
      south_space = south_space.get
      m[0, 3] = lower_sw_point.x
      m[1, 3] = lower_sw_point.y
      m[2, 3] = lower_sw_point.z
      south_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_space.setBuildingStory(story)
      south_space.setName("Story #{floor + 1} South Space")
    end
    # Set vertical story position
    story.setNominalZCoordinate(z)
  end
  BTAP::Geometry.match_surfaces(model)

  return model
end

.create_shape_u(model, length = 40.0, left_width = 40.0, right_width = 40.0, left_end_length = 15.0, right_end_length = 15.0, left_end_offset = 25.0, num_floors = 3.0, floor_to_floor_height = 3.8, plenum_height = 1.0, perimeter_zone_depth = 4.57) ⇒ OpenStudio::Model::Model

Create a U shape in a model

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • length (Double) (defaults to: 40.0)

    Building length in meters

  • left_width (Double) (defaults to: 40.0)

    Left width in meters

  • right_width (Double) (defaults to: 40.0)

    Right width in meters

  • left_end_length (Double) (defaults to: 15.0)

    Left end length in meters

  • right_end_length (Double) (defaults to: 15.0)

    Right end length in meters

  • left_end_offset (Double) (defaults to: 25.0)

    Left end offset in meters

  • num_floors (Integer) (defaults to: 3.0)

    Number of floors

  • floor_to_floor_height (Double) (defaults to: 3.8)

    Floor to floor height in meters

  • plenum_height (Double) (defaults to: 1.0)

    Plenum height in meters

  • perimeter_zone_depth (Double) (defaults to: 4.57)

    Perimeter zone depth in meters

Returns:

  • (OpenStudio::Model::Model)

    OpenStudio model object



1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
# File 'lib/openstudio-standards/geometry/create_shape.rb', line 1513

def self.create_shape_u(model,
                        length = 40.0,
                        left_width = 40.0,
                        right_width = 40.0,
                        left_end_length = 15.0,
                        right_end_length = 15.0,
                        left_end_offset = 25.0,
                        num_floors = 3.0,
                        floor_to_floor_height = 3.8,
                        plenum_height = 1.0,
                        perimeter_zone_depth = 4.57)
  if length <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Length must be greater than 0.')
    return nil
  end

  if left_width <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Left width must be greater than 0.')
    return nil
  end

  if left_end_length <= 1e-4 || left_end_length >= (length - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Left end length must be greater than 0 and less than #{length}m.")
    return nil
  end

  if right_end_length <= 1e-4 || right_end_length >= (length - left_end_length - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Right end length must be greater than 0 and less than #{length - left_end_length}m.")
    return nil
  end

  if left_end_offset <= 1e-4 || left_end_offset >= (left_width - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Left end offset must be greater than 0 and less than #{left_width}m.")
    return nil
  end

  if right_width <= (left_width - left_end_offset - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Right width must be greater than #{left_width - left_end_offset}m.")
    return nil
  end

  if num_floors <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Number of floors must be greater than 0.')
    return nil
  end

  if floor_to_floor_height <= 1e-4
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Floor to floor height must be greater than 0.')
    return nil
  end

  if plenum_height < 0
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', 'Plenum height must be greater than 0.')
    return nil
  end

  shortest_side = [length / 2, left_width, right_width, left_end_length, right_end_length, left_width - left_end_offset].min
  if perimeter_zone_depth < 0 || 2 * perimeter_zone_depth >= (shortest_side - 1e-4)
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Create.Shape', "Perimeter zone depth must be greater than or equal to 0 and less than #{shortest_side / 2}m.")
    return nil
  end

  # Loop through the number of floors
  for floor in (0..num_floors - 1)
    z = floor_to_floor_height * floor

    # Create a new story within the building
    story = OpenStudio::Model::BuildingStory.new(model)
    story.setNominalFloortoFloorHeight(floor_to_floor_height)
    story.setName("Story #{floor + 1}")

    left_nw_point = OpenStudio::Point3d.new(0, left_width, z)
    left_ne_point = OpenStudio::Point3d.new(left_end_length, left_width, z)
    upper_sw_point = OpenStudio::Point3d.new(left_end_length, left_width - left_end_offset, z)
    upper_se_point = OpenStudio::Point3d.new(length - right_end_length, left_width - left_end_offset, z)
    right_nw_point = OpenStudio::Point3d.new(length - right_end_length, right_width, z)
    right_ne_point = OpenStudio::Point3d.new(length, right_width, z)
    lower_se_point = OpenStudio::Point3d.new(length, 0, z)
    lower_sw_point = OpenStudio::Point3d.new(0, 0, z)

    # Identity matrix for setting space origins
    m = OpenStudio::Matrix.new(4, 4, 0)
    m[0, 0] = 1
    m[1, 1] = 1
    m[2, 2] = 1
    m[3, 3] = 1

    # Define polygons for a U-shape building with perimeter core zoning
    if perimeter_zone_depth > 0
      perimeter_left_nw_point = left_nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_left_ne_point = left_ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_upper_sw_point = upper_sw_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_upper_se_point = upper_se_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_right_nw_point = right_nw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_right_ne_point = right_ne_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, -perimeter_zone_depth, 0)
      perimeter_lower_se_point = lower_se_point + OpenStudio::Vector3d.new(-perimeter_zone_depth, perimeter_zone_depth, 0)
      perimeter_lower_sw_point = lower_sw_point + OpenStudio::Vector3d.new(perimeter_zone_depth, perimeter_zone_depth, 0)

      west_left_perimeter_polygon = OpenStudio::Point3dVector.new
      west_left_perimeter_polygon << lower_sw_point
      west_left_perimeter_polygon << left_nw_point
      west_left_perimeter_polygon << perimeter_left_nw_point
      west_left_perimeter_polygon << perimeter_lower_sw_point
      west_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_left_perimeter_polygon, floor_to_floor_height, model)
      west_left_perimeter_space = west_left_perimeter_space.get
      m[0, 3] = lower_sw_point.x
      m[1, 3] = lower_sw_point.y
      m[2, 3] = lower_sw_point.z
      west_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_left_perimeter_space.setBuildingStory(story)
      west_left_perimeter_space.setName("Story #{floor + 1} West Left Perimeter Space")

      north_left_perimeter_polygon = OpenStudio::Point3dVector.new
      north_left_perimeter_polygon << left_nw_point
      north_left_perimeter_polygon << left_ne_point
      north_left_perimeter_polygon << perimeter_left_ne_point
      north_left_perimeter_polygon << perimeter_left_nw_point
      north_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_left_perimeter_polygon, floor_to_floor_height, model)
      north_left_perimeter_space = north_left_perimeter_space.get
      m[0, 3] = perimeter_left_nw_point.x
      m[1, 3] = perimeter_left_nw_point.y
      m[2, 3] = perimeter_left_nw_point.z
      north_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_left_perimeter_space.setBuildingStory(story)
      north_left_perimeter_space.setName("Story #{floor + 1} North Left Perimeter Space")

      east_left_perimeter_polygon = OpenStudio::Point3dVector.new
      east_left_perimeter_polygon << left_ne_point
      east_left_perimeter_polygon << upper_sw_point
      east_left_perimeter_polygon << perimeter_upper_sw_point
      east_left_perimeter_polygon << perimeter_left_ne_point
      east_left_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_left_perimeter_polygon, floor_to_floor_height, model)
      east_left_perimeter_space = east_left_perimeter_space.get
      m[0, 3] = perimeter_upper_sw_point.x
      m[1, 3] = perimeter_upper_sw_point.y
      m[2, 3] = perimeter_upper_sw_point.z
      east_left_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_left_perimeter_space.setBuildingStory(story)
      east_left_perimeter_space.setName("Story #{floor + 1} East Left Perimeter Space")

      north_lower_perimeter_polygon = OpenStudio::Point3dVector.new
      north_lower_perimeter_polygon << upper_sw_point
      north_lower_perimeter_polygon << upper_se_point
      north_lower_perimeter_polygon << perimeter_upper_se_point
      north_lower_perimeter_polygon << perimeter_upper_sw_point
      north_lower_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_lower_perimeter_polygon, floor_to_floor_height, model)
      north_lower_perimeter_space = north_lower_perimeter_space.get
      m[0, 3] = perimeter_upper_sw_point.x
      m[1, 3] = perimeter_upper_sw_point.y
      m[2, 3] = perimeter_upper_sw_point.z
      north_lower_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_lower_perimeter_space.setBuildingStory(story)
      north_lower_perimeter_space.setName("Story #{floor + 1} North Lower Perimeter Space")

      west_right_perimeter_polygon = OpenStudio::Point3dVector.new
      west_right_perimeter_polygon << upper_se_point
      west_right_perimeter_polygon << right_nw_point
      west_right_perimeter_polygon << perimeter_right_nw_point
      west_right_perimeter_polygon << perimeter_upper_se_point
      west_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(west_right_perimeter_polygon, floor_to_floor_height, model)
      west_right_perimeter_space = west_right_perimeter_space.get
      m[0, 3] = upper_se_point.x
      m[1, 3] = upper_se_point.y
      m[2, 3] = upper_se_point.z
      west_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_right_perimeter_space.setBuildingStory(story)
      west_right_perimeter_space.setName("Story #{floor + 1} West Right Perimeter Space")

      north_right_perimeter_polygon = OpenStudio::Point3dVector.new
      north_right_perimeter_polygon << right_nw_point
      north_right_perimeter_polygon << right_ne_point
      north_right_perimeter_polygon << perimeter_right_ne_point
      north_right_perimeter_polygon << perimeter_right_nw_point
      north_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(north_right_perimeter_polygon, floor_to_floor_height, model)
      north_right_perimeter_space = north_right_perimeter_space.get
      m[0, 3] = perimeter_right_nw_point.x
      m[1, 3] = perimeter_right_nw_point.y
      m[2, 3] = perimeter_right_nw_point.z
      north_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      north_right_perimeter_space.setBuildingStory(story)
      north_right_perimeter_space.setName("Story #{floor + 1} North Right Perimeter Space")

      east_right_perimeter_polygon = OpenStudio::Point3dVector.new
      east_right_perimeter_polygon << right_ne_point
      east_right_perimeter_polygon << lower_se_point
      east_right_perimeter_polygon << perimeter_lower_se_point
      east_right_perimeter_polygon << perimeter_right_ne_point
      east_right_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(east_right_perimeter_polygon, floor_to_floor_height, model)
      east_right_perimeter_space = east_right_perimeter_space.get
      m[0, 3] = perimeter_lower_se_point.x
      m[1, 3] = perimeter_lower_se_point.y
      m[2, 3] = perimeter_lower_se_point.z
      east_right_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_right_perimeter_space.setBuildingStory(story)
      east_right_perimeter_space.setName("Story #{floor + 1} East Right Perimeter Space")

      south_lower_perimeter_polygon = OpenStudio::Point3dVector.new
      south_lower_perimeter_polygon << lower_se_point
      south_lower_perimeter_polygon << lower_sw_point
      south_lower_perimeter_polygon << perimeter_lower_sw_point
      south_lower_perimeter_polygon << perimeter_lower_se_point
      south_lower_perimeter_space = OpenStudio::Model::Space.fromFloorPrint(south_lower_perimeter_polygon, floor_to_floor_height, model)
      south_lower_perimeter_space = south_lower_perimeter_space.get
      m[0, 3] = lower_sw_point.x
      m[1, 3] = lower_sw_point.y
      m[2, 3] = lower_sw_point.z
      south_lower_perimeter_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_lower_perimeter_space.setBuildingStory(story)
      south_lower_perimeter_space.setName("Story #{floor + 1} South Lower Perimeter Space")

      west_core_polygon = OpenStudio::Point3dVector.new
      west_core_polygon << perimeter_lower_sw_point
      west_core_polygon << perimeter_left_nw_point
      west_core_polygon << perimeter_left_ne_point
      west_core_polygon << perimeter_upper_sw_point
      west_core_space = OpenStudio::Model::Space.fromFloorPrint(west_core_polygon, floor_to_floor_height, model)
      west_core_space = west_core_space.get
      m[0, 3] = perimeter_lower_sw_point.x
      m[1, 3] = perimeter_lower_sw_point.y
      m[2, 3] = perimeter_lower_sw_point.z
      west_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_core_space.setBuildingStory(story)
      west_core_space.setName("Story #{floor + 1} West Core Space")

      south_core_polygon = OpenStudio::Point3dVector.new
      south_core_polygon << perimeter_upper_sw_point
      south_core_polygon << perimeter_upper_se_point
      south_core_polygon << perimeter_lower_se_point
      south_core_polygon << perimeter_lower_sw_point
      south_core_space = OpenStudio::Model::Space.fromFloorPrint(south_core_polygon, floor_to_floor_height, model)
      south_core_space = south_core_space.get
      m[0, 3] = perimeter_lower_sw_point.x
      m[1, 3] = perimeter_lower_sw_point.y
      m[2, 3] = perimeter_lower_sw_point.z
      south_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_core_space.setBuildingStory(story)
      south_core_space.setName("Story #{floor + 1} South Core Space")

      east_core_polygon = OpenStudio::Point3dVector.new
      east_core_polygon << perimeter_upper_se_point
      east_core_polygon << perimeter_right_nw_point
      east_core_polygon << perimeter_right_ne_point
      east_core_polygon << perimeter_lower_se_point
      east_core_space = OpenStudio::Model::Space.fromFloorPrint(east_core_polygon, floor_to_floor_height, model)
      east_core_space = east_core_space.get
      m[0, 3] = perimeter_upper_se_point.x
      m[1, 3] = perimeter_upper_se_point.y
      m[2, 3] = perimeter_upper_se_point.z
      east_core_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_core_space.setBuildingStory(story)
      east_core_space.setName("Story #{floor + 1} East Core Space")
    else
      # Minimal zones
      west_polygon = OpenStudio::Point3dVector.new
      west_polygon << lower_sw_point
      west_polygon << left_nw_point
      west_polygon << left_ne_point
      west_polygon << upper_sw_point
      west_space = OpenStudio::Model::Space.fromFloorPrint(west_polygon, floor_to_floor_height, model)
      west_space = west_space.get
      m[0, 3] = lower_sw_point.x
      m[1, 3] = lower_sw_point.y
      m[2, 3] = lower_sw_point.z
      west_space.changeTransformation(OpenStudio::Transformation.new(m))
      west_space.setBuildingStory(story)
      west_space.setName("Story #{floor + 1} West Space")

      south_polygon = OpenStudio::Point3dVector.new
      south_polygon << lower_sw_point
      south_polygon << upper_sw_point
      south_polygon << upper_se_point
      south_polygon << lower_se_point
      south_space = OpenStudio::Model::Space.fromFloorPrint(south_polygon, floor_to_floor_height, model)
      south_space = south_space.get
      m[0, 3] = lower_sw_point.x
      m[1, 3] = lower_sw_point.y
      m[2, 3] = lower_sw_point.z
      south_space.changeTransformation(OpenStudio::Transformation.new(m))
      south_space.setBuildingStory(story)
      south_space.setName("Story #{floor + 1} South Space")

      east_polygon = OpenStudio::Point3dVector.new
      east_polygon << upper_se_point
      east_polygon << right_nw_point
      east_polygon << right_ne_point
      east_polygon << lower_se_point
      east_space = OpenStudio::Model::Space.fromFloorPrint(east_polygon, floor_to_floor_height, model)
      east_space = east_space.get
      m[0, 3] = upper_se_point.x
      m[1, 3] = upper_se_point.y
      m[2, 3] = upper_se_point.z
      east_space.changeTransformation(OpenStudio::Transformation.new(m))
      east_space.setBuildingStory(story)
      east_space.setName("Story #{floor + 1} East Space")
    end
    # Set vertical story position
    story.setNominalZCoordinate(z)
  end
  BTAP::Geometry.match_surfaces(model)

  return model
end

.create_sliced_bar_multi_polygons(space_types, length, width, footprint_origin_point, story_hash) ⇒ Hash

sliced bar multi creates and array of multiple sliced bar simple hashes

Parameters:

  • space_types (Array<Hash>)

    Array of hashes with the space type and floor area

  • length (Double)

    length of building in meters

  • width (Double)

    width of building in meters

  • footprint_origin_point (OpenStudio::Point3d)

    OpenStudio Point3d object for the new origin

  • story_hash (Hash)

    A hash of building story information including space origin z value and space height

Returns:

  • (Hash)

    Hash of point vectors that define the space geometry for each direction



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/openstudio-standards/geometry/create.rb', line 176

def self.create_sliced_bar_multi_polygons(space_types, length, width, footprint_origin_point, story_hash)
  # total building floor area to calculate ratios from space type floor areas
  total_floor_area = 0.0
  target_per_space_type = {}
  space_types.each do |space_type, space_type_hash|
    total_floor_area += space_type_hash[:floor_area]
    target_per_space_type[space_type] = space_type_hash[:floor_area]
  end

  # sort array by floor area, this hash will be altered to reduce floor area for each space type to 0
  space_types_running_count = space_types.sort_by { |k, v| v[:floor_area] }

  # array entry for each story
  footprints = []

  # variables for sliver check
  # re-evaluate what the default should be
  valid_bar_width_min_m = OpenStudio.convert(3.0, 'ft', 'm').get
  # building width
  bar_length = width
  valid_bar_area_min_m2 = valid_bar_width_min_m * bar_length

  # loop through stories to populate footprints
  story_hash.each_with_index do |(k, v), i|
    # update the length and width for partial floors
    if i + 1 == story_hash.size
      area_multiplier = v[:partial_story_multiplier]
      edge_multiplier = Math.sqrt(area_multiplier)
      length *= edge_multiplier
      width *= edge_multiplier
    end

    # this will be populated for each building story
    target_footprint_area = v[:multiplier] * length * width
    current_footprint_area = 0.0
    space_types_local_count = {}

    space_types_running_count.each do |space_type, space_type_hash|
      # next if floor area is full or space type is empty

      tol_value = 0.0001
      next if current_footprint_area + tol_value >= target_footprint_area
      next if space_type_hash[:floor_area] <= tol_value

      # special test for when total floor area is smaller than valid_bar_area_min_m2, just make bar smaller that valid min and warn user
      if target_per_space_type[space_type] < valid_bar_area_min_m2
        sliver_override = true
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "Floor area of #{space_type.name} results in a bar with smaller than target minimum width.")
      else
        sliver_override = false
      end

      # add entry for space type if it doesn't have one yet
      if !space_types_local_count.key?(space_type)
        if space_type_hash.key?(:children)
          space_type = space_type_hash[:children][:default][:space_type] # will re-using space type create issue
          space_types_local_count[space_type] = { floor_area: 0.0 }
          space_types_local_count[space_type][:children] = space_type_hash[:children]
        else
          space_types_local_count[space_type] = { floor_area: 0.0 }
        end
      end

      # if there is enough of this space type to fill rest of floor area
      remaining_in_footprint = target_footprint_area - current_footprint_area
      raw_footprint_area_used = [space_type_hash[:floor_area], remaining_in_footprint].min

      # add to local hash
      space_types_local_count[space_type][:floor_area] = raw_footprint_area_used / v[:multiplier].to_f

      # adjust balance ot running and local counts
      current_footprint_area += raw_footprint_area_used
      space_type_hash[:floor_area] -= raw_footprint_area_used

      # test if think sliver left on current floor.
      # fix by moving smallest space type to next floor and and the same amount more of the sliver space type to this story
      raw_footprint_area_used < valid_bar_area_min_m2 && sliver_override == false ? (test_a = true) : (test_a = false)

      # test if what would be left of the current space type would result in a sliver on the next story.
      # fix by removing some of this space type so their is enough left for the next story, and replace the removed amount with the largest space type in the model
      (space_type_hash[:floor_area] < valid_bar_area_min_m2) && (space_type_hash[:floor_area] > tol_value) ? (test_b = true) : (test_b = false)

      # identify very small slices and re-arrange spaces to different stories to avoid this
      if test_a

        # get first/smallest space type to move to another story
        first_space = space_types_local_count.first

        # adjustments running counter for space type being removed from this story
        space_types_running_count.each do |k2, v2|
          next if k2 != first_space[0]

          v2[:floor_area] += first_space[1][:floor_area] * v[:multiplier]
        end

        # adjust running count for current space type
        space_type_hash[:floor_area] -= first_space[1][:floor_area] * v[:multiplier]

        # add to local count for current space type
        space_types_local_count[space_type][:floor_area] += first_space[1][:floor_area]

        # remove from local count for removed space type
        space_types_local_count.shift

      elsif test_b

        # swap size
        swap_size = valid_bar_area_min_m2 * 5.0 # currently equal to default perimeter zone depth of 15'
        # this prevents too much area from being swapped resulting in a negative number for floor area
        if swap_size > space_types_local_count[space_type][:floor_area] * v[:multiplier].to_f
          swap_size = space_types_local_count[space_type][:floor_area] * v[:multiplier].to_f
        end

        # adjust running count for current space type
        space_type_hash[:floor_area] += swap_size

        # remove from local count for current space type
        space_types_local_count[space_type][:floor_area] -= swap_size / v[:multiplier].to_f

        # adjust footprint used
        current_footprint_area -= swap_size

        # the next larger space type will be brought down to fill out the footprint without any additional code
      end
    end

    # creating footprint for story
    footprints << OpenstudioStandards::Geometry.create_sliced_bar_simple_polygons(space_types_local_count, length, width, footprint_origin_point)
  end
  return footprints
end

.create_sliced_bar_simple_polygons(space_types, length, width, footprint_origin_point = OpenStudio::Point3d.new(0.0, 0.0, 0.0), perimeter_zone_depth = OpenStudio.convert(15.0, 'ft', 'm').get) ⇒ Hash

sliced bar simple creates a single sliced bar for space types passed in look at length and width to adjust slicing direction

Parameters:

  • space_types (Array<Hash>)

    Array of hashes with the space type and floor area

  • length (Double)

    length of building in meters

  • width (Double)

    width of building in meters

  • footprint_origin_point (OpenStudio::Point3d) (defaults to: OpenStudio::Point3d.new(0.0, 0.0, 0.0))

    Optional OpenStudio Point3d object for the new origin

  • perimeter_zone_depth (Double) (defaults to: OpenStudio.convert(15.0, 'ft', 'm').get)

    Optional perimeter zone depth in meters

Returns:

  • (Hash)

    Hash of point vectors that define the space geometry for each direction



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/openstudio-standards/geometry/create.rb', line 317

def self.create_sliced_bar_simple_polygons(space_types, length, width,
                                           footprint_origin_point = OpenStudio::Point3d.new(0.0, 0.0, 0.0),
                                           perimeter_zone_depth = OpenStudio.convert(15.0, 'ft', 'm').get)
  hash_of_point_vectors = {} # key is name, value is a hash, one item of which is polygon. Another could be space type

  reverse_slice = false
  if length < width
    reverse_slice = true
    # OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', "Reverse typical slice direction for bar because of aspect ratio less than 1.0.")
  end

  # determine if core and perimeter zoning can be used
  if !([length, width].min > perimeter_zone_depth * 2.5 && [length, width].min > perimeter_zone_depth * 2.5)
    perimeter_zone_depth = 0 # if any size is to small then just model floor as single zone, issue warning
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', 'Not modeling core and perimeter zones for some portion of the model.')
  end

  x_delta = footprint_origin_point.x - (length / 2.0)
  y_delta = footprint_origin_point.y - (width / 2.0)
  z = 0.0
  # this represents the entire bar, not individual space type slices
  nw_point = OpenStudio::Point3d.new(x_delta, y_delta + width, z)
  sw_point = OpenStudio::Point3d.new(x_delta, y_delta, z)
  # used when length is less than width
  se_point = OpenStudio::Point3d.new(x_delta + length, y_delta, z)

  # total building floor area to calculate ratios from space type floor areas
  total_floor_area = 0.0
  space_types.each do |space_type, space_type_hash|
    total_floor_area += space_type_hash[:floor_area]
  end

  # sort array by floor area but shift largest object to front
  space_types = space_types.sort_by { |k, v| v[:floor_area] }
  space_types.insert(0, space_types.delete_at(space_types.size - 1)) # .to_h

  # min and max bar end values
  min_bar_end_multiplier = 0.75
  max_bar_end_multiplier = 1.5

  # sort_by results in arrays with two items , first is key, second is hash value
  re_apply_largest_space_type_at_end = false
  max_reduction = nil # used when looping through section_hash_for_space_type if first space type needs to also be at far end of bar
  space_types.each do |space_type, space_type_hash|
    # setup end perimeter zones if needed
    start_perimeter_width_deduction = 0.0
    end_perimeter_width_deduction = 0.0
    if space_type == space_types.first[0]
      if [length, width].max * space_type_hash[:floor_area] / total_floor_area > max_bar_end_multiplier * perimeter_zone_depth
        start_perimeter_width_deduction = perimeter_zone_depth
      end
      # see if last space type is too small for perimeter. If it is then save some of this space type
      if [length, width].max * space_types.last[1][:floor_area] / total_floor_area < perimeter_zone_depth * min_bar_end_multiplier
        re_apply_largest_space_type_at_end = true
      end
    end
    if space_type == space_types.last[0]
      if [length, width].max * space_type_hash[:floor_area] / total_floor_area > max_bar_end_multiplier * perimeter_zone_depth
        end_perimeter_width_deduction = perimeter_zone_depth
      end
    end
    non_end_adjusted_width = ([length, width].max * space_type_hash[:floor_area] / total_floor_area) - start_perimeter_width_deduction - end_perimeter_width_deduction

    # adjustment of end space type is too small and is replaced with largest space type
    if (space_type == space_types.first[0]) && re_apply_largest_space_type_at_end
      max_reduction = [perimeter_zone_depth, non_end_adjusted_width].min
      non_end_adjusted_width -= max_reduction
    end
    if (space_type == space_types.last[0]) && re_apply_largest_space_type_at_end
      end_perimeter_width_deduction = space_types.first[0]
      end_b_flag = true
    else
      end_b_flag = false
    end

    # populate data for core and perimeter of slice
    section_hash_for_space_type = {}
    section_hash_for_space_type['end_a'] = start_perimeter_width_deduction
    section_hash_for_space_type[''] = non_end_adjusted_width
    section_hash_for_space_type['end_b'] = end_perimeter_width_deduction

    # determine if this space+type is double loaded corridor, and if so what the perimeter zone depth should be based on building width
    # look at reverse_slice to see if length or width should be used to determine perimeter depth
    if space_type_hash.key?(:children)
      core_ratio = space_type_hash[:children][:circ][:orig_ratio]
      perim_ratio = space_type_hash[:children][:default][:orig_ratio]
      core_ratio_adj = core_ratio / (core_ratio + perim_ratio)
      perim_ratio_adj = perim_ratio / (core_ratio + perim_ratio)
      core_space_type = space_type_hash[:children][:circ][:space_type]
      perim_space_type = space_type_hash[:children][:default][:space_type]
      if reverse_slice
        custom_cor_val = length * core_ratio_adj
        custom_perim_val = (length - custom_cor_val) / 2.0
      else
        custom_cor_val = width * core_ratio_adj
        custom_perim_val = (width - custom_cor_val) / 2.0
      end
      actual_perim = custom_perim_val
      double_loaded_corridor = true
    else
      actual_perim = perimeter_zone_depth
      double_loaded_corridor = false
    end

    # may overwrite
    first_space_type_hash = space_types.first[1]
    if end_b_flag && first_space_type_hash.key?(:children)
      end_b_core_ratio = first_space_type_hash[:children][:circ][:orig_ratio]
      end_b_perim_ratio = first_space_type_hash[:children][:default][:orig_ratio]
      end_b_core_ratio_adj = end_b_core_ratio / (end_b_core_ratio + end_b_perim_ratio)
      end_b_perim_ratio_adj = end_b_perim_ratio / (end_b_core_ratio + end_b_perim_ratio)
      end_b_core_space_type = first_space_type_hash[:children][:circ][:space_type]
      end_b_perim_space_type = first_space_type_hash[:children][:default][:space_type]
      if reverse_slice
        end_b_custom_cor_val = length * end_b_core_ratio_adj
        end_b_custom_perim_val = (length - end_b_custom_cor_val) / 2.0
      else
        end_b_custom_cor_val = width * end_b_core_ratio_adj
        end_b_custom_perim_val = (width - end_b_custom_cor_val) / 2.0
      end
      end_b_actual_perim = end_b_custom_perim_val
      end_b_double_loaded_corridor = true
    else
      end_b_actual_perim = perimeter_zone_depth
      end_b_double_loaded_corridor = false
    end

    # loop through sections for space type (main and possibly one or two end perimeter sections)
    section_hash_for_space_type.each do |k, slice|
      # need to use different space type for end_b
      if end_b_flag && k == 'end_b' && space_types.first[1].key?(:children)
        slice = space_types.first[0]
        actual_perim = end_b_actual_perim
        double_loaded_corridor = end_b_double_loaded_corridor
        core_ratio = end_b_core_ratio
        perim_ratio = end_b_perim_ratio
        core_ratio_adj = end_b_core_ratio_adj
        perim_ratio_adj = end_b_perim_ratio_adj
        core_space_type = end_b_core_space_type
        perim_space_type = end_b_perim_space_type
      end

      if slice.class.to_s == 'OpenStudio::Model::SpaceType' || slice.class.to_s == 'OpenStudio::Model::Building'
        space_type = slice
        max_reduction = [perimeter_zone_depth, max_reduction].min
        slice = max_reduction
      end
      if slice == 0
        next
      end

      if reverse_slice
        # create_bar at 90 degrees if aspect ration is less than 1.0
        # typical order (sw,nw,ne,se)
        # order used here (se,sw,nw,ne)
        nw_point = (sw_point + OpenStudio::Vector3d.new(0, slice, 0))
        ne_point = (se_point + OpenStudio::Vector3d.new(0, slice, 0))

        if actual_perim > 0 && (actual_perim * 2.0) < length
          polygon_a = OpenStudio::Point3dVector.new
          polygon_a << se_point
          polygon_a << (se_point + OpenStudio::Vector3d.new(- actual_perim, 0, 0))
          polygon_a << (ne_point + OpenStudio::Vector3d.new(- actual_perim, 0, 0))
          polygon_a << ne_point
          if double_loaded_corridor
            hash_of_point_vectors["#{perim_space_type.name} A #{k}"] = {}
            hash_of_point_vectors["#{perim_space_type.name} A #{k}"][:space_type] = perim_space_type
            hash_of_point_vectors["#{perim_space_type.name} A #{k}"][:polygon] = polygon_a
          else
            hash_of_point_vectors["#{space_type.name} A #{k}"] = {}
            hash_of_point_vectors["#{space_type.name} A #{k}"][:space_type] = space_type
            hash_of_point_vectors["#{space_type.name} A #{k}"][:polygon] = polygon_a
          end

          polygon_b = OpenStudio::Point3dVector.new
          polygon_b << (se_point + OpenStudio::Vector3d.new(- actual_perim, 0, 0))
          polygon_b << (sw_point + OpenStudio::Vector3d.new(actual_perim, 0, 0))
          polygon_b << (nw_point + OpenStudio::Vector3d.new(actual_perim, 0, 0))
          polygon_b << (ne_point + OpenStudio::Vector3d.new(- actual_perim, 0, 0))
          if double_loaded_corridor
            hash_of_point_vectors["#{core_space_type.name} B #{k}"] = {}
            hash_of_point_vectors["#{core_space_type.name} B #{k}"][:space_type] = core_space_type
            hash_of_point_vectors["#{core_space_type.name} B #{k}"][:polygon] = polygon_b
          else
            hash_of_point_vectors["#{space_type.name} B #{k}"] = {}
            hash_of_point_vectors["#{space_type.name} B #{k}"][:space_type] = space_type
            hash_of_point_vectors["#{space_type.name} B #{k}"][:polygon] = polygon_b
          end

          polygon_c = OpenStudio::Point3dVector.new
          polygon_c << (sw_point + OpenStudio::Vector3d.new(actual_perim, 0, 0))
          polygon_c << sw_point
          polygon_c << nw_point
          polygon_c << (nw_point + OpenStudio::Vector3d.new(actual_perim, 0, 0))
          if double_loaded_corridor
            hash_of_point_vectors["#{perim_space_type.name} C #{k}"] = {}
            hash_of_point_vectors["#{perim_space_type.name} C #{k}"][:space_type] = perim_space_type
            hash_of_point_vectors["#{perim_space_type.name} C #{k}"][:polygon] = polygon_c
          else
            hash_of_point_vectors["#{space_type.name} C #{k}"] = {}
            hash_of_point_vectors["#{space_type.name} C #{k}"][:space_type] = space_type
            hash_of_point_vectors["#{space_type.name} C #{k}"][:polygon] = polygon_c
          end
        else
          polygon_a = OpenStudio::Point3dVector.new
          polygon_a << se_point
          polygon_a << sw_point
          polygon_a << nw_point
          polygon_a << ne_point
          hash_of_point_vectors["#{space_type.name} #{k}"] = {}
          hash_of_point_vectors["#{space_type.name} #{k}"][:space_type] = space_type
          hash_of_point_vectors["#{space_type.name} #{k}"][:polygon] = polygon_a
        end

        # update west points
        sw_point = nw_point
        se_point = ne_point
      else
        ne_point = nw_point + OpenStudio::Vector3d.new(slice, 0, 0)
        se_point = sw_point + OpenStudio::Vector3d.new(slice, 0, 0)

        if actual_perim > 0 && (actual_perim * 2.0) < width
          polygon_a = OpenStudio::Point3dVector.new
          polygon_a << sw_point
          polygon_a << (sw_point + OpenStudio::Vector3d.new(0, actual_perim, 0))
          polygon_a << (se_point + OpenStudio::Vector3d.new(0, actual_perim, 0))
          polygon_a << se_point
          if double_loaded_corridor
            hash_of_point_vectors["#{perim_space_type.name} A #{k}"] = {}
            hash_of_point_vectors["#{perim_space_type.name} A #{k}"][:space_type] = perim_space_type
            hash_of_point_vectors["#{perim_space_type.name} A #{k}"][:polygon] = polygon_a
          else
            hash_of_point_vectors["#{space_type.name} A #{k}"] = {}
            hash_of_point_vectors["#{space_type.name} A #{k}"][:space_type] = space_type
            hash_of_point_vectors["#{space_type.name} A #{k}"][:polygon] = polygon_a
          end

          polygon_b = OpenStudio::Point3dVector.new
          polygon_b << (sw_point + OpenStudio::Vector3d.new(0, actual_perim, 0))
          polygon_b << (nw_point + OpenStudio::Vector3d.new(0, - actual_perim, 0))
          polygon_b << (ne_point + OpenStudio::Vector3d.new(0, - actual_perim, 0))
          polygon_b << (se_point + OpenStudio::Vector3d.new(0, actual_perim, 0))
          if double_loaded_corridor
            hash_of_point_vectors["#{core_space_type.name} B #{k}"] = {}
            hash_of_point_vectors["#{core_space_type.name} B #{k}"][:space_type] = core_space_type
            hash_of_point_vectors["#{core_space_type.name} B #{k}"][:polygon] = polygon_b
          else
            hash_of_point_vectors["#{space_type.name} B #{k}"] = {}
            hash_of_point_vectors["#{space_type.name} B #{k}"][:space_type] = space_type
            hash_of_point_vectors["#{space_type.name} B #{k}"][:polygon] = polygon_b
          end

          polygon_c = OpenStudio::Point3dVector.new
          polygon_c << (nw_point + OpenStudio::Vector3d.new(0, - actual_perim, 0))
          polygon_c << nw_point
          polygon_c << ne_point
          polygon_c << (ne_point + OpenStudio::Vector3d.new(0, - actual_perim, 0))
          if double_loaded_corridor
            hash_of_point_vectors["#{perim_space_type.name} C #{k}"] = {}
            hash_of_point_vectors["#{perim_space_type.name} C #{k}"][:space_type] = perim_space_type
            hash_of_point_vectors["#{perim_space_type.name} C #{k}"][:polygon] = polygon_c
          else
            hash_of_point_vectors["#{space_type.name} C #{k}"] = {}
            hash_of_point_vectors["#{space_type.name} C #{k}"][:space_type] = space_type
            hash_of_point_vectors["#{space_type.name} C #{k}"][:polygon] = polygon_c
          end
        else
          polygon_a = OpenStudio::Point3dVector.new
          polygon_a << sw_point
          polygon_a << nw_point
          polygon_a << ne_point
          polygon_a << se_point
          hash_of_point_vectors["#{space_type.name} #{k}"] = {}
          hash_of_point_vectors["#{space_type.name} #{k}"][:space_type] = space_type
          hash_of_point_vectors["#{space_type.name} #{k}"][:polygon] = polygon_a
        end

        # update west points
        nw_point = ne_point
        sw_point = se_point
      end
    end
  end

  return hash_of_point_vectors
end

.create_space_from_polygon(model, space_origin, point_3d_vector, options = {}) ⇒ OpenStudio::Model::Space

add def to create a space from input, optionally take a name, space type, story and thermal zone.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object describing the space footprint polygon

  • space_origin (OpenStudio::Point3d)

    origin point

  • point_3d_vector (OpenStudio::Point3dVector)

    OpenStudio Point3dVector defining the space footprint

  • options (Hash) (defaults to: {})

    Hash of options for additional arguments

Options Hash (options):

  • :name (String)

    name of the space

  • :space_type (OpenStudio::Model::SpaceType)

    OpenStudio SpaceType object

  • :story (String)

    name name of the building story

  • :make_thermal_zone (Boolean)

    set to true to make an thermal zone object, defaults to true.

  • :thermal_zone (OpenStudio::Model::ThermalZone)

    attach a specific ThermalZone object to the space

  • :thermal_zone_multiplier (Integer)

    the thermal zone multiplier, defaults to 1.

  • :floor_to_floor_height (Double)

    floor to floor height in meters, defaults to 10 ft.

Returns:

  • (OpenStudio::Model::Space)

    OpenStudio Space object



746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
# File 'lib/openstudio-standards/geometry/create.rb', line 746

def self.create_space_from_polygon(model, space_origin, point_3d_vector, options = {})
  # set defaults to use if user inputs not passed in
  defaults = {
    'name' => nil,
    'space_type' => nil,
    'story' => nil,
    'make_thermal_zone' => nil,
    'thermal_zone' => nil,
    'thermal_zone_multiplier' => 1,
    'floor_to_floor_height' => OpenStudio.convert(10.0, 'ft', 'm').get
  }

  # merge user inputs with defaults
  options = defaults.merge(options)

  # Identity matrix for setting space origins
  m = OpenStudio::Matrix.new(4, 4, 0)
  m[0, 0] = 1
  m[1, 1] = 1
  m[2, 2] = 1
  m[3, 3] = 1

  # make space from floor print
  space = OpenStudio::Model::Space.fromFloorPrint(point_3d_vector, options['floor_to_floor_height'], model)
  space = space.get
  m[0, 3] = space_origin.x
  m[1, 3] = space_origin.y
  m[2, 3] = space_origin.z
  space.changeTransformation(OpenStudio::Transformation.new(m))
  space.setBuildingStory(options['story'])
  if !options['name'].nil?
    space.setName(options['name'])
  end

  if !options['space_type'].nil? && options['space_type'].class.to_s == 'OpenStudio::Model::SpaceType'
    space.setSpaceType(options['space_type'])
  end

  # create thermal zone if requested and assign
  if options['make_thermal_zone']
    new_zone = OpenStudio::Model::ThermalZone.new(model)
    new_zone.setMultiplier(options['thermal_zone_multiplier'])
    space.setThermalZone(new_zone)
    new_zone.setName("Zone #{space.name}")
  else
    if !options['thermal_zone'].nil? then space.setThermalZone(options['thermal_zone']) end
  end

  return space
end

.create_spaces_from_polygons(model, footprints, typical_story_height, effective_num_stories, footprint_origin_point = OpenStudio::Point3d.new(0.0, 0.0, 0.0), story_hash = {}) ⇒ Array<OpenStudio::Model::Space>

TODO:

add option to create shading surfaces when using multiplier. Mainly important for non rectangular buildings where self shading would be an issue.

take diagram made by create_core_and_perimeter_polygons and make multi-story building

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • footprints (Hash)

    Array of footprint polygons that make up the spaces

  • typical_story_height (Double)

    typical story height in meters

  • effective_num_stories (Double)

    effective number of stories

  • footprint_origin_point (OpenStudio::Point3d) (defaults to: OpenStudio::Point3d.new(0.0, 0.0, 0.0))

    Optional OpenStudio Point3d object for the new origin

  • story_hash (Hash) (defaults to: {})

    A hash of building story information including space origin z value and space height If blank, this method will default to using information in the story_hash.

Returns:

  • (Array<OpenStudio::Model::Space>)

    Array of OpenStudio Space objects



615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
# File 'lib/openstudio-standards/geometry/create.rb', line 615

def self.create_spaces_from_polygons(model, footprints, typical_story_height, effective_num_stories,
                                     footprint_origin_point = OpenStudio::Point3d.new(0.0, 0.0, 0.0),
                                     story_hash = {})
  # default story hash is for three stories with mid-story multiplier, but user can pass in custom versions
  if story_hash.empty?
    if effective_num_stories > 2
      story_hash['ground'] = { space_origin_z: footprint_origin_point.z, space_height: typical_story_height, multiplier: 1 }
      story_hash['mid'] = { space_origin_z: footprint_origin_point.z + typical_story_height + (typical_story_height * (effective_num_stories.ceil - 3) / 2.0), space_height: typical_story_height, multiplier: effective_num_stories - 2 }
      story_hash['top'] = { space_origin_z: footprint_origin_point.z + (typical_story_height * (effective_num_stories.ceil - 1)), space_height: typical_story_height, multiplier: 1 }
    elsif effective_num_stories > 1
      story_hash['ground'] = { space_origin_z: footprint_origin_point.z, space_height: typical_story_height, multiplier: 1 }
      story_hash['top'] = { space_origin_z: footprint_origin_point.z + (typical_story_height * (effective_num_stories.ceil - 1)), space_height: typical_story_height, multiplier: 1 }
    else
      # one story only
      story_hash['ground'] = { space_origin_z: footprint_origin_point.z, space_height: typical_story_height, multiplier: 1 }
    end
  end

  # hash of new spaces (only change boundary conditions for these)
  new_spaces = []

  # loop through story_hash and polygons to generate all of the spaces
  story_hash.each_with_index do |(story_name, story_data), index|
    # make new story unless story at requested height already exists.
    story = nil
    model.getBuildingStorys.sort.each do |ext_story|
      if (ext_story.nominalZCoordinate.to_f - story_data[:space_origin_z].to_f).abs < 0.01
        story = ext_story
      end
    end
    if story.nil?
      story = OpenStudio::Model::BuildingStory.new(model)
      # not used for anything
      story.setNominalFloortoFloorHeight(story_data[:space_height])
      # not used for anything
      story.setNominalZCoordinate(story_data[:space_origin_z])
      story.setName("Story #{story_name}")
    end

    # multiplier values for adjacent stories to be altered below as needed
    multiplier_story_above = 1
    multiplier_story_below = 1

    if index == 0 # bottom floor, only check above
      if story_hash.size > 1
        multiplier_story_above = story_hash.values[index + 1][:multiplier]
      end
    elsif index == story_hash.size - 1 # top floor, check only below
      multiplier_story_below = story_hash.values[index + -1][:multiplier]
    else # mid floor, check above and below
      multiplier_story_above = story_hash.values[index + 1][:multiplier]
      multiplier_story_below = story_hash.values[index + -1][:multiplier]
    end

    # if adjacent story has multiplier > 1 then make appropriate surfaces adiabatic
    adiabatic_ceilings = false
    adiabatic_floors = false
    if story_data[:multiplier] > 1
      adiabatic_ceilings = true
      adiabatic_floors = true
    elsif multiplier_story_above > 1
      adiabatic_ceilings = true
    elsif multiplier_story_below > 1
      adiabatic_floors = true
    end

    # get the right collection of polygons to make up footprint for each building story
    if index > footprints.size - 1
      # use last footprint
      target_footprint = footprints.last
    else
      target_footprint = footprints[index]
    end
    target_footprint.each do |name, space_data|
      # gather options
      options = {
        'name' => "#{name} - #{story.name}",
        'space_type' => space_data[:space_type],
        'story' => story,
        'make_thermal_zone' => true,
        'thermal_zone_multiplier' => story_data[:multiplier],
        'floor_to_floor_height' => story_data[:space_height]
      }

      # make space
      space = OpenstudioStandards::Geometry.create_space_from_polygon(model, space_data[:polygon].first, space_data[:polygon], options)
      new_spaces << space

      # set z origin to proper position
      space.setZOrigin(story_data[:space_origin_z])

      # loop through celings and floors to hard asssign constructions and set boundary condition
      if adiabatic_ceilings || adiabatic_floors
        space.surfaces.each do |surface|
          if adiabatic_floors && (surface.surfaceType == 'Floor')
            if surface.construction.is_initialized
              surface.setConstruction(surface.construction.get)
            end
            surface.setOutsideBoundaryCondition('Adiabatic')
          end
          if adiabatic_ceilings && (surface.surfaceType == 'RoofCeiling')
            if surface.construction.is_initialized
              surface.setConstruction(surface.construction.get)
            end
            surface.setOutsideBoundaryCondition('Adiabatic')
          end
        end
      end
    end

    # @tofo in future add code to include plenums or raised floor to each/any story.
  end
  # any changes to wall boundary conditions will be handled by same code that calls this method.
  # this method doesn't need to know about basements and party walls.
  return new_spaces
end

.model_assign_spaces_to_building_stories(model) ⇒ Boolean

Assign each space in the model to a building story based on common z (height) values. If no story object is found for a particular height, create a new one and assign it to the space. Does not assign a story to plenum spaces.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Boolean)

    returns true if successful, false if not



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/openstudio-standards/geometry/modify.rb', line 98

def self.model_assign_spaces_to_building_stories(model)
  # Make hash of spaces and min z values
  sorted_spaces = {}
  model.getSpaces.sort.each do |space|
    # Skip plenum spaces
    next if OpenstudioStandards::Space.space_plenum?(space)

    # loop through space surfaces to find min z value
    z_points = []
    space.surfaces.each do |surface|
      surface.vertices.each do |vertex|
        z_points << vertex.z
      end
    end
    min_z = z_points.min + space.zOrigin
    sorted_spaces[space] = min_z
  end

  # Pre-sort spaces
  sorted_spaces = sorted_spaces.sort_by { |a| a[1] }

  # Take the sorted list and assign/make stories
  sorted_spaces.each do |space|
    space_obj = space[0]
    space_min_z = space[1]
    if space_obj.buildingStory.empty?
      tolerance = 0.3
      story = OpenstudioStandards::Geometry.model_get_building_story_for_nominal_height(model, space_min_z, tolerance: tolerance)
      if story.nil?
        story = OpenStudio::Model::BuildingStory.new(model)
        story.setNominalZCoordinate(space_min_z)
        story.setName("Building Story #{space_min_z.round(1)}m")
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "No story with a min z value of #{space_min_z.round(2)} m +/- #{tolerance} m was found, so a new story called #{story.name} was created.")
      end
      space_obj.setBuildingStory(story)
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Space #{space[0].name} was not assigned to a story by the user.  It has been assigned to #{story.name}.")
    end
  end

  return true
end

.model_envelope_data(model) ⇒ Hash

TODO:

full list of hash returns aren’t documented yet

gather envelope data for envelope simplification

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Hash)

    A hash of envelope data used by other methods



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 137

def self.model_envelope_data(model)
  OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', 'Gathering envelope data.')

  # hash to contain envelope data
  envelope_data_hash = {}

  # used for overhang and party wall orientation catigorization
  facade_options = {
    'north_east' => 45.0,
    'south_east' => 125.0,
    'south_west' => 225.0,
    'north_west' => 315.0
  }

  # get building level inputs
  envelope_data_hash[:north_axis] = model.getBuilding.northAxis
  envelope_data_hash[:building_floor_area] = model.getBuilding.floorArea
  envelope_data_hash[:building_exterior_surface_area] = model.getBuilding.exteriorSurfaceArea
  envelope_data_hash[:building_exterior_wall_area] = model.getBuilding.exteriorWallArea
  envelope_data_hash[:building_exterior_roof_area] = envelope_data_hash[:building_exterior_surface_area] - envelope_data_hash[:building_exterior_wall_area]
  envelope_data_hash[:building_air_volume] = model.getBuilding.airVolume
  envelope_data_hash[:building_perimeter] = nil # will be applied for first story without ground walls

  # get bounding_box
  bounding_box = OpenStudio::BoundingBox.new
  model.getSpaces.sort.each do |space|
    space.surfaces.sort.each do |space_surface|
      bounding_box.addPoints(space.transformation * space_surface.vertices)
    end
  end
  min_x = bounding_box.minX.get
  min_y = bounding_box.minY.get
  min_z = bounding_box.minZ.get
  max_x = bounding_box.maxX.get
  max_y = bounding_box.maxY.get
  max_z = bounding_box.maxZ.get
  envelope_data_hash[:building_min_xyz] = [min_x, min_y, min_z]
  envelope_data_hash[:building_max_xyz] = [max_x, max_y, max_z]

  # add orientation specific wwr
  ext_surfaces_hash = OpenstudioStandards::Geometry.model_get_exterior_window_and_wall_area_by_orientation(model)
  envelope_data_hash[:building_wwr_n] = ext_surfaces_hash['north_window'] / ext_surfaces_hash['north_wall']
  envelope_data_hash[:building_wwr_s] = ext_surfaces_hash['south_window'] / ext_surfaces_hash['south_wall']
  envelope_data_hash[:building_wwr_e] = ext_surfaces_hash['east_window'] / ext_surfaces_hash['east_wall']
  envelope_data_hash[:building_wwr_w] = ext_surfaces_hash['west_window'] / ext_surfaces_hash['west_wall']
  envelope_data_hash[:stories] = {} # each entry will be hash with buildingStory as key and attributes has values
  envelope_data_hash[:space_types] = {} # each entry will be hash with spaceType as key and attributes has values

  # as rough estimate overhang area / glazing area should be close to projection factor assuming overhang is same width as windows
  # will only add building shading surfaces assoicated with a sub-surface.
  building_overhang_area_n = 0.0
  building_overhang_area_s = 0.0
  building_overhang_area_e = 0.0
  building_overhang_area_w = 0.0

  # loop through stories based on mine z height of surfaces.
  sorted_stories = OpenstudioStandards::Geometry.model_sort_building_stories_and_get_min_multiplier(model).sort_by { |k, v| v }
  sorted_stories.each do |story, story_min_z|
    story_min_multiplier = nil
    story_footprint = nil
    story_multiplied_floor_area = OpenstudioStandards::Geometry.spaces_get_floor_area(story.spaces)
    # goal of footprint calc is to count multiplier for hotel room on facade,but not to count what is intended as a story multiplier
    story_multiplied_exterior_surface_area = OpenstudioStandards::Geometry.spaces_get_exterior_area(story.spaces)
    story_multiplied_exterior_wall_area = OpenstudioStandards::Geometry.spaces_get_exterior_wall_area(story.spaces)
    story_multiplied_exterior_roof_area = story_multiplied_exterior_surface_area - story_multiplied_exterior_wall_area
    story_has_ground_walls = []
    story_has_adiabatic_walls = []
    story_included_in_building_area = false # will be true if any spaces on story are inclued in building area
    story_max_z = nil

    # loop through spaces for story gathering information
    story.spaces.each do |space|
      # get min multiplier value
      multiplier = space.multiplier
      if story_min_multiplier.nil? || (story_min_multiplier > multiplier)
        story_min_multiplier = multiplier
      end

      # calculate footprint
      story_footprint = story_multiplied_floor_area / story_min_multiplier

      # see if part of floor area
      if space.partofTotalFloorArea
        story_included_in_building_area = true

        # add to space type ratio hash when space is included in building floor area
        if space.spaceType.is_initialized
          space_type = space.spaceType.get
          space_floor_area = space.floorArea * space.multiplier
          if envelope_data_hash[:space_types].key?(space_type)
            envelope_data_hash[:space_types][space_type][:floor_area] += space_floor_area
          else
            envelope_data_hash[:space_types][space_type] = {}
            envelope_data_hash[:space_types][space_type][:floor_area] = space_floor_area

            # make hash for heating and cooling setpoints
            envelope_data_hash[:space_types][space_type][:htg_setpoint] = {}
            envelope_data_hash[:space_types][space_type][:clg_setpoint] = {}

          end

          # add heating and cooling setpoints
          if space.thermalZone.is_initialized && space.thermalZone.get.thermostatSetpointDualSetpoint.is_initialized
            thermostat = space.thermalZone.get.thermostatSetpointDualSetpoint.get

            # log heating schedule
            if thermostat.heatingSetpointTemperatureSchedule.is_initialized
              htg_sch = thermostat.heatingSetpointTemperatureSchedule.get
              if envelope_data_hash[:space_types][space_type][:htg_setpoint].key?(htg_sch)
                envelope_data_hash[:space_types][space_type][:htg_setpoint][htg_sch] += space_floor_area
              else
                envelope_data_hash[:space_types][space_type][:htg_setpoint][htg_sch] = space_floor_area
              end
            else
              OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{space.thermalZone.get.name} containing #{space.name} doesn't have a heating setpoint schedule.")
            end

            # log cooling schedule
            if thermostat.coolingSetpointTemperatureSchedule.is_initialized
              clg_sch = thermostat.coolingSetpointTemperatureSchedule.get
              if envelope_data_hash[:space_types][space_type][:clg_setpoint].key?(clg_sch)
                envelope_data_hash[:space_types][space_type][:clg_setpoint][clg_sch] += space_floor_area
              else
                envelope_data_hash[:space_types][space_type][:clg_setpoint][clg_sch] = space_floor_area
              end
            else
              OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{space.thermalZone.get.name} containing #{space.name} doesn't have a heating setpoint schedule.")
            end

          else
            OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{space.name} either isn't in a thermal zone or doesn't have a thermostat assigned")
          end

        else
          OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{space.name} is included in the building floor area but isn't assigned a space type.")
        end

      end

      # check for walls with adiabatic and ground boundary condition
      space.surfaces.each do |surface|
        next if surface.surfaceType != 'Wall'

        if surface.outsideBoundaryCondition == 'Ground'
          story_has_ground_walls << surface
        elsif surface.outsideBoundaryCondition == 'Adiabatic'
          story_has_adiabatic_walls << surface
        end
      end

      # populate overhang values
      space.surfaces.each do |surface|
        surface.subSurfaces.each do |sub_surface|
          sub_surface.shadingSurfaceGroups.each do |shading_surface_group|
            shading_surface_group.shadingSurfaces.each do |shading_surface|
              absolute_azimuth = OpenStudio.convert(sub_surface.azimuth, 'rad', 'deg').get + sub_surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis
              absolute_azimuth -= 360.0 until absolute_azimuth < 360.0
              # add to hash based on orientation
              if (facade_options['north_east'] <= absolute_azimuth) && (absolute_azimuth < facade_options['south_east']) # East overhang
                building_overhang_area_e += shading_surface.grossArea * space.multiplier
              elsif (facade_options['south_east'] <= absolute_azimuth) && (absolute_azimuth < facade_options['south_west']) # South overhang
                building_overhang_area_s += shading_surface.grossArea * space.multiplier
              elsif (facade_options['south_west'] <= absolute_azimuth) && (absolute_azimuth < facade_options['north_west']) # West overhang
                building_overhang_area_w += shading_surface.grossArea * space.multiplier
              else # North overhang
                building_overhang_area_n += shading_surface.grossArea * space.multiplier
              end
            end
          end
        end
      end

      # get max z
      space_z_max = OpenstudioStandards::Geometry.surfaces_get_z_values(space.surfaces.to_a).max + space.zOrigin
      if story_max_z.nil? || (story_max_z > space_z_max)
        story_max_z = space_z_max
      end
    end

    # populate hash for story data
    envelope_data_hash[:stories][story] = {}
    envelope_data_hash[:stories][story][:story_min_height] = story_min_z
    envelope_data_hash[:stories][story][:story_max_height] = story_max_z
    envelope_data_hash[:stories][story][:story_min_multiplier] = story_min_multiplier
    envelope_data_hash[:stories][story][:story_has_ground_walls] = story_has_ground_walls
    envelope_data_hash[:stories][story][:story_has_adiabatic_walls] = story_has_adiabatic_walls
    envelope_data_hash[:stories][story][:story_included_in_building_area] = story_included_in_building_area
    envelope_data_hash[:stories][story][:story_footprint] = story_footprint
    envelope_data_hash[:stories][story][:story_multiplied_floor_area] = story_multiplied_floor_area
    envelope_data_hash[:stories][story][:story_exterior_surface_area] = story_multiplied_exterior_surface_area
    envelope_data_hash[:stories][story][:story_multiplied_exterior_wall_area] = story_multiplied_exterior_wall_area
    envelope_data_hash[:stories][story][:story_multiplied_exterior_roof_area] = story_multiplied_exterior_roof_area

    # get perimeter and adiabatic walls that appear to be party walls
    perimeter_and_party_walls = OpenstudioStandards::Geometry.building_story_get_exterior_wall_perimeter(story,
                                                                                                         multiplier_adjustment: story_min_multiplier,
                                                                                                         bounding_box: bounding_box)
    envelope_data_hash[:stories][story][:story_perimeter] = perimeter_and_party_walls[:perimeter]
    envelope_data_hash[:stories][story][:story_party_walls] = []
    east = false
    south = false
    west = false
    north = false
    perimeter_and_party_walls[:party_walls].each do |surface|
      absolute_azimuth = OpenStudio.convert(surface.azimuth, 'rad', 'deg').get + surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis
      absolute_azimuth -= 360.0 until absolute_azimuth < 360.0

      # add to hash based on orientation (initially added array of sourfaces, but swtiched to just true/false flag)
      if (facade_options['north_east'] <= absolute_azimuth) && (absolute_azimuth < facade_options['south_east']) # East party walls
        east = true
      elsif (facade_options['south_east'] <= absolute_azimuth) && (absolute_azimuth < facade_options['south_west']) # South party walls
        south = true
      elsif (facade_options['south_west'] <= absolute_azimuth) && (absolute_azimuth < facade_options['north_west']) # West party walls
        west = true
      else # North party walls
        north = true
      end
    end

    if east then envelope_data_hash[:stories][story][:story_party_walls] << 'east' end
    if south then envelope_data_hash[:stories][story][:story_party_walls] << 'south' end
    if west then envelope_data_hash[:stories][story][:story_party_walls] << 'west' end
    if north then envelope_data_hash[:stories][story][:story_party_walls] << 'north' end

    # store perimeter from first story that doesn't have ground walls
    if story_has_ground_walls.empty? && envelope_data_hash[:building_perimeter].nil?
      envelope_data_hash[:building_perimeter] = envelope_data_hash[:stories][story][:story_perimeter]
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Geometry.Create', " * #{story.name} is the first above grade story and will be used for the building perimeter.")
    end
  end

  envelope_data_hash[:building_overhang_proj_factor_n] = building_overhang_area_n / ext_surfaces_hash['north_window']
  envelope_data_hash[:building_overhang_proj_factor_s] = building_overhang_area_s / ext_surfaces_hash['south_window']
  envelope_data_hash[:building_overhang_proj_factor_e] = building_overhang_area_e / ext_surfaces_hash['east_window']
  envelope_data_hash[:building_overhang_proj_factor_w] = building_overhang_area_w / ext_surfaces_hash['west_window']

  # warn for spaces that are not on a story (in future could infer stories for these)
  model.getSpaces.sort.each do |space|
    if !space.buildingStory.is_initialized
      OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Create', "#{space.name} is not on a building story, may have unexpected results.")
    end
  end

  return envelope_data_hash
end

.model_get_building_stories_above_ground(model) ⇒ Array<OpenStudio::Model::BuildingStory>

Returns an array of the above ground building stories in the model.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Array<OpenStudio::Model::BuildingStory>)

    Array of OpenStudio BuildingStory objects, empty array if none



927
928
929
930
931
932
933
934
935
936
# File 'lib/openstudio-standards/geometry/information.rb', line 927

def self.model_get_building_stories_above_ground(model)
  above_ground_stories = []
  model.getBuildingStorys.sort.each do |story|
    z = story.nominalZCoordinate
    if !z.empty? && z.to_f >= 0
      above_ground_stories << story
    end
  end
  return above_ground_stories
end

.model_get_building_stories_below_ground(model) ⇒ Array<OpenStudio::Model::BuildingStory>

Returns an array of the below ground building stories in the model.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Array<OpenStudio::Model::BuildingStory>)

    Array of OpenStudio BuildingStory objects, empty array if none



942
943
944
945
946
947
948
949
950
951
# File 'lib/openstudio-standards/geometry/information.rb', line 942

def self.model_get_building_stories_below_ground(model)
  below_ground_stories = []
  model.getBuildingStorys.sort.each do |story|
    z = story.nominalZCoordinate
    if !z.empty? && z.to_f < 0
      below_ground_stories << story
    end
  end
  return below_ground_stories
end

.model_get_building_story_for_nominal_height(model, minimum_height, tolerance: 0.3) ⇒ OpenStudio::Model::BuildingStory

Returns the building story associated with a given minimum height. This return the story that matches the minimum z value of any vertex of any surface of any space on the story, with the exception of plenum spaces.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • minimum_height (Double)

    The base height of the desired story, in meters.

  • tolerance (Double) (defaults to: 0.3)

    tolerance for comparison, in m. Default is 0.3 m ~1ft

Returns:

  • (OpenStudio::Model::BuildingStory)

    OpenStudio BuildingStory object, nil if none matching



910
911
912
913
914
915
916
917
918
919
920
921
# File 'lib/openstudio-standards/geometry/information.rb', line 910

def self.model_get_building_story_for_nominal_height(model, minimum_height, tolerance: 0.3)
  matched_story = nil
  model.getBuildingStorys.sort.each do |story|
    z = OpenstudioStandards::Geometry.building_story_get_minimum_height(story)
    if (minimum_height - z).abs < tolerance
      OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.Model', "The story with a min z value of #{minimum_height.round(2)} is #{story.name}.")
      matched_story = story
    end
  end

  return matched_story
end

.model_get_exterior_window_and_wall_area_by_orientation(model, spaces: []) ⇒ Hash

Returns the wall area and window area by orientation

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • spaces (Array<OpenStudio::Model::Space>) (defaults to: [])

    optional array of Space objects. If provided, the return will report for only those spaces.

Returns:

  • (Hash)

    Hash of wall area in square meters and window to wall ratio for each orientation



1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
# File 'lib/openstudio-standards/geometry/information.rb', line 1029

def self.model_get_exterior_window_and_wall_area_by_orientation(model, spaces: [])
  # counters
  total_gross_ext_wall_area_north = 0.0
  total_gross_ext_wall_area_south = 0.0
  total_gross_ext_wall_area_east = 0.0
  total_gross_ext_wall_area_west = 0.0
  total_ext_window_area_north = 0.0
  total_ext_window_area_south = 0.0
  total_ext_window_area_east = 0.0
  total_ext_window_area_west = 0.0

  if spaces.empty?
    spaces = model.getSpaces
  end

  spaces.each do |space|
    # get surface area adjusting for zone multiplier
    zone = space.thermalZone
    if zone.empty?
      zone_multiplier = 1 # space is not in a thermal zone
    else
      zone_multiplier = zone.get.multiplier
    end

    space.surfaces.each do |s|
      next if s.surfaceType != 'Wall'
      next if s.outsideBoundaryCondition != 'Outdoors'

      surface_gross_area = s.grossArea * zone_multiplier

      # loop through sub surfaces and add area including multiplier
      ext_window_area = 0
      s.subSurfaces.each do |sub_surface|
        ext_window_area += sub_surface.grossArea * sub_surface.multiplier * zone_multiplier
      end

      absolute_azimuth = OpenStudio.convert(s.azimuth, 'rad', 'deg').get + s.space.get.directionofRelativeNorth + model.getBuilding.northAxis
      absolute_azimuth -= 360.0 until absolute_azimuth < 360.0

      # add to exterior wall counter if north or south
      if (absolute_azimuth >= 45.0) && (absolute_azimuth < 125.0)
        # east exterior walls
        total_gross_ext_wall_area_east += surface_gross_area
        total_ext_window_area_east += ext_window_area
      elsif (absolute_azimuth >= 125.0) && (absolute_azimuth < 225.0)
        # south exterior walls
        total_gross_ext_wall_area_south += surface_gross_area
        total_ext_window_area_south += ext_window_area
      elsif (absolute_azimuth >= 225.0) && (absolute_azimuth < 315.0)
        # west exterior walls
        total_gross_ext_wall_area_west += surface_gross_area
        total_ext_window_area_west += ext_window_area
      else
        # north exterior walls
        total_gross_ext_wall_area_north += surface_gross_area
        total_ext_window_area_north += ext_window_area
      end
    end
  end

  result = { 'north_wall' => total_gross_ext_wall_area_north,
             'north_window' => total_ext_window_area_north,
             'south_wall' => total_gross_ext_wall_area_south,
             'south_window' => total_ext_window_area_south,
             'east_wall' => total_gross_ext_wall_area_east,
             'east_window' => total_ext_window_area_east,
             'west_wall' => total_gross_ext_wall_area_west,
             'west_window' => total_ext_window_area_west }
  return result
end

.model_get_exterior_window_to_wall_ratio(model, spaces: [], cardinal_direction: nil) ⇒ Double

Returns the window to wall ratio

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

  • spaces (Array<OpenStudio::Model::Space>) (defaults to: [])

    optional array of Space objects. If provided, the return will report for only those spaces.

  • cardinal_direction (String) (defaults to: nil)

    Cardinal direction ‘N’, ‘E’, ‘S’, ‘W’ If provided, the return will report for only the provided cardinal direction

Returns:

  • (Double)

    window to wall ratio



961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
# File 'lib/openstudio-standards/geometry/information.rb', line 961

def self.model_get_exterior_window_to_wall_ratio(model,
                                                 spaces: [],
                                                 cardinal_direction: nil)
  # counters
  total_gross_ext_wall_area = 0.0
  total_ext_window_area = 0.0
  window_to_wall_ratio = 0.0

  # get spaces if none provided
  if spaces.empty?
    spaces = model.getSpaces
  end

  # loop through each space and log window and wall areas
  spaces.each do |space|
    # get surface area adjusting for zone multiplier
    zone = space.thermalZone
    if zone.empty?
      # space is not in a thermal zone
      zone_multiplier = 1
    else
      zone_multiplier = zone.get.multiplier
    end

    # loop through spaces and skip all that aren't exterior walls and don't match selected cardinal direction
    space.surfaces.each do |surface|
      next if surface.surfaceType != 'Wall'
      next if surface.outsideBoundaryCondition != 'Outdoors'

      # filter by cardinal direction if specified
      case cardinal_direction
      when 'N', 'n', 'North', 'north'
        next unless OpenstudioStandards::Geometry.surface_get_cardinal_direction(surface) == 'N'
      when 'E', 'e', 'East', 'east'
        next unless OpenstudioStandards::Geometry.surface_get_cardinal_direction(surface) == 'E'
      when 'S', 's', 'South', 'south'
        next unless OpenstudioStandards::Geometry.surface_get_cardinal_direction(surface) == 'S'
      when 'W', 'w', 'West', 'west'
        next unless OpenstudioStandards::Geometry.surface_get_cardinal_direction(surface) == 'W'
      end

      # Get wall and window area
      surface_gross_area = surface.grossArea * zone_multiplier

      # loop through sub surfaces and add area including multiplier
      ext_window_area = 0
      surface.subSurfaces.each do |sub_surface|
        ext_window_area += sub_surface.grossArea * sub_surface.multiplier * zone_multiplier
      end

      total_gross_ext_wall_area += surface_gross_area
      total_ext_window_area += ext_window_area
    end
  end

  if total_gross_ext_wall_area > 0.0
    window_to_wall_ratio = total_ext_window_area / total_gross_ext_wall_area
  end

  return window_to_wall_ratio
end

.model_get_perimeter(model) ⇒ Double

TODO:

this doesn’t catch walls that are split that sit above floor surfaces that are not (e.g. main corridoor in secondary school model)

TODO:

also odd with multi-height spaces

Calculates the exterior perimeter length, checking checks for edges shared by a ground exposed floor and exterior exposed wall.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Double)

    perimeter length in meters



1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
# File 'lib/openstudio-standards/geometry/information.rb', line 1106

def self.model_get_perimeter(model)
  perimeter = 0.0
  model.getSpaces.sort.each do |space|
    # counter to use later
    edge_hash = {}
    edge_counter = 0
    space.surfaces.sort.each do |surface|
      # get vertices
      vertex_hash = {}
      vertex_counter = 0
      surface.vertices.each do |vertex|
        vertex_counter += 1
        vertex_hash[vertex_counter] = [vertex.x, vertex.y, vertex.z]
      end
      # make edges
      counter = 0
      vertex_hash.each do |k, v|
        edge_counter += 1
        counter += 1
        if vertex_hash.size == counter
          # different code for wrap around vertex
          edge_hash[edge_counter] = [v, vertex_hash[1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
        else
          edge_hash[edge_counter] = [v, vertex_hash[counter + 1], surface, surface.outsideBoundaryCondition, surface.surfaceType]
        end
      end
    end

    # check edges for matches (need opposite vertices and proper boundary conditions)
    edge_hash.each do |k1, v1|
      next if v1[3] != 'Ground' # skip if not ground exposed floor
      next if v1[4] != 'Floor'

      edge_hash.each do |k2, v2|
        next if v2[3] != 'Outdoors' # skip if not exterior exposed wall (todo - update to handle basement)
        next if v2[4] != 'Wall'

        # see if edges have same geometry
        # found cases where the two lines below removed edges and resulted in lower than actual perimeter. Added new code with tolerance.
        # next if not v1[0] == v2[1] # next if not same geometry reversed
        # next if not v1[1] == v2[0]

        # these are three item array's add in tolerance for each array entry
        tolerance = 0.0001
        test_a = true
        test_b = true
        3.times.each do |i|
          if (v1[0][i] - v2[1][i]).abs > tolerance
            test_a = false
          end
          if (v1[1][i] - v2[0][i]).abs > tolerance
            test_b = false
          end
        end

        next if test_a != true
        next if test_b != true

        point_one = OpenStudio::Point3d.new(v1[0][0], v1[0][1], v1[0][2])
        point_two = OpenStudio::Point3d.new(v1[1][0], v1[1][1], v1[1][2])
        length = OpenStudio::Vector3d.new(point_one - point_two).length
        perimeter += length
      end
    end
  end

  return perimeter
end

.model_group_thermal_zones_by_building_story(model, thermal_zones) ⇒ Array<Array<OpenStudio::Model::ThermalZone>>

Group an array of zones into multiple arrays, one for each story in the building. Zones with spaces on multiple stories will be assigned to only one of the stories. Returns an empty array when the story doesn’t contain any of the zones.

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio Model object

  • thermal_zones (Array<OpenStudio::Model::ThermalZone>)

    An array of OpenStudio ThermalZone objects

Returns:

  • (Array<Array<OpenStudio::Model::ThermalZone>>)

    An array of arrays of OpenStudio ThermalZone objects



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/openstudio-standards/geometry/group.rb', line 15

def self.model_group_thermal_zones_by_building_story(model, thermal_zones)
  story_zone_lists = []
  zones_already_assigned = []
  model.getBuildingStorys.sort.each do |story|
    # Get all the spaces on this story
    spaces = story.spaces

    # Get all the thermal zones that serve these spaces
    all_zones_on_story = []
    spaces.each do |space|
      if space.thermalZone.is_initialized
        all_zones_on_story << space.thermalZone.get
      else
        OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Model', "Space #{space.name} has no thermal zone, it is not included in the simulation.")
      end
    end

    # Find thermal zones in the list that are on this story
    zones_on_story = []
    thermal_zones.each do |zone|
      if all_zones_on_story.include?(zone)
        # Skip thermal zones that were already assigned to a story.
        # This can happen if a zone has multiple spaces on multiple stories.
        # Stairwells and atriums are typical scenarios.
        next if zones_already_assigned.include?(zone)

        zones_on_story << zone
        zones_already_assigned << zone
      end
    end

    unless zones_on_story.empty?
      story_zone_lists << zones_on_story
    end
  end

  return story_zone_lists
end

.model_group_thermal_zones_by_building_type(model, min_area_m2: 1858.0608) ⇒ Array<Hash>

Split all zones in the model into groups that are big enough to justify their own HVAC system type. Similar to the logic from 90.1 Appendix G, but without regard to the fuel type of the existing HVAC system (because the model may not have one).

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio Model object

  • min_area_m2 (Double) (defaults to: 1858.0608)

    the minimum area required to justify a different system type, default 20,000 ft^2

Returns:

  • (Array<Hash>)

    an array of hashes of area information, with keys area_ft2, type, stories, and zones (an array of zones)



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/openstudio-standards/geometry/group.rb', line 163

def self.model_group_thermal_zones_by_building_type(model, min_area_m2: 1858.0608)
  min_area_ft2 = OpenStudio.convert(min_area_m2, 'm^2', 'ft^2').get

  # Get occupancy type, building type, fuel type, and area information for all zones, excluding unconditioned zones
  std = Standard.build('90.1-2019') # delete once space methods refactored
  zones = std.model_zones_with_occ_and_fuel_type(model, nil)

  # Ensure that there is at least one conditioned zone
  if zones.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.prototype.Model', 'The building does not appear to have any conditioned zones. Make sure zones have thermostat with appropriate heating and cooling setpoint schedules.')
    return []
  end

  # Group the zones by building type
  type_to_area = Hash.new { 0.0 }
  zones_grouped_by_bldg_type = zones.group_by { |z| z['bldg_type'] }

  # Determine the dominant building type by area
  zones_grouped_by_bldg_type.each do |bldg_type, zns|
    zns.each do |zn|
      type_to_area[bldg_type] += zn['area']
    end
  end
  dom_bldg_type = type_to_area.sort_by { |k, v| v }.reverse[0][0]

  # Get the dominant building type group
  dom_bldg_type_group = zones_grouped_by_bldg_type[dom_bldg_type]

  # Check the non-dominant building type groups to see if they are big enough to trigger the building exception.
  # If they are, leave the group standing alone.
  # If they are not, add the zones in that group back to the dominant building type group.
  bldg_type_groups = []
  zones_grouped_by_bldg_type.each do |bldg_type, zns|
    # Skip the dominant building type
    next if bldg_type == dom_bldg_type

    # Add up the floor area of the group
    area_m2 = 0
    zns.each do |zn|
      area_m2 += zn['area']
    end
    area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get

    # If the non-dominant group is big enough, preserve that group.
    if area_ft2 > min_area_ft2
      bldg_type_groups << [bldg_type, zns]
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "The portion of the building with a building type of #{bldg_type} is bigger than the minimum area of #{min_area_ft2.round} ft2.  It will be assigned a separate HVAC system type.")
      # Otherwise, add the zones back to the dominant group.
    else
      dom_bldg_type_group += zns
    end
  end
  # Add the dominant building type group to the list
  bldg_type_groups << [dom_bldg_type, dom_bldg_type_group]

  # Calculate the area for each of the final groups
  # and replace the zone hashes with an array of zone objects
  final_groups = []
  bldg_type_groups.each do |bldg_type, zns|
    # Sum the area and put all zones into an array
    area_m2 = 0.0
    gp_zns = []
    zns.each do |zn|
      area_m2 += zn['area']
      gp_zns << zn['zone']
    end
    area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get

    # Determine the number of stories this group spans
    num_stories = OpenstudioStandards::Geometry.thermal_zones_get_number_of_stories_spanned(gp_zns)

    # Create a hash representing this group
    group = {}
    group['area_ft2'] = area_ft2
    group['type'] = bldg_type
    group['stories'] = num_stories
    group['zones'] = gp_zns
    final_groups << group

    # Report out the final grouping
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Final system type group: bldg_type = #{group['type']}, area = #{group['area_ft2'].round} ft2, num stories = #{group['stories']}, zones:")
    group['zones'].sort.each_slice(5) do |zone_list|
      zone_names = []
      zone_list.each do |zone|
        zone_names << zone.name.get.to_s
      end
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "--- #{zone_names.join(', ')}")
    end
  end

  return final_groups
end

.model_group_thermal_zones_by_occupancy_type(model, min_area_m2: 1858.0608) ⇒ Array<Hash>

Split all zones in the model into groups that are big enough to justify their own HVAC system type. Similar to the logic from 90.1 Appendix G, but without regard to the fuel type of the existing HVAC system (because the model may not have one).

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio Model object

  • min_area_m2 (Double) (defaults to: 1858.0608)

    the minimum area required to justify a different system type, default 20,000 ft^2

Returns:

  • (Array<Hash>)

    an array of hashes of area information, with keys area_ft2, type, stories, and zones (an array of zones)



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

def self.model_group_thermal_zones_by_occupancy_type(model, min_area_m2: 1858.0608)
  min_area_ft2 = OpenStudio.convert(min_area_m2, 'm^2', 'ft^2').get

  # Get occupancy type, fuel type, and area information for all zones, excluding unconditioned zones.
  # Occupancy types are:
  # Residential
  # NonResidential
  # Use 90.1-2010 so that retail and publicassembly are not split out
  std = Standard.build('90.1-2019') # delete once space methods refactored
  zones = std.model_zones_with_occ_and_fuel_type(model, nil)

  # Ensure that there is at least one conditioned zone
  if zones.empty?
    OpenStudio.logFree(OpenStudio::Error, 'openstudio.prototype.Model', 'The building does not appear to have any conditioned zones. Make sure zones have thermostat with appropriate heating and cooling setpoint schedules.')
    return []
  end

  # Group the zones by occupancy type
  type_to_area = Hash.new { 0.0 }
  zones_grouped_by_occ = zones.group_by { |z| z['occ'] }

  # Determine the dominant occupancy type by area
  zones_grouped_by_occ.each do |occ_type, zns|
    zns.each do |zn|
      type_to_area[occ_type] += zn['area']
    end
  end
  dom_occ = type_to_area.sort_by { |k, v| v }.reverse[0][0]

  # Get the dominant occupancy type group
  dom_occ_group = zones_grouped_by_occ[dom_occ]

  # Check the non-dominant occupancy type groups to see if they are big enough to trigger the occupancy exception.
  # If they are, leave the group standing alone.
  # If they are not, add the zones in that group back to the dominant occupancy type group.
  occ_groups = []
  zones_grouped_by_occ.each do |occ_type, zns|
    # Skip the dominant occupancy type
    next if occ_type == dom_occ

    # Add up the floor area of the group
    area_m2 = 0
    zns.each do |zn|
      area_m2 += zn['area']
    end
    area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get

    # If the non-dominant group is big enough, preserve that group.
    if area_ft2 > min_area_ft2
      occ_groups << [occ_type, zns]
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "The portion of the building with an occupancy type of #{occ_type} is bigger than the minimum area of #{min_area_ft2.round} ft2.  It will be assigned a separate HVAC system type.")
      # Otherwise, add the zones back to the dominant group.
    else
      dom_occ_group += zns
    end
  end
  # Add the dominant occupancy group to the list
  occ_groups << [dom_occ, dom_occ_group]

  # Calculate the area for each of the final groups
  # and replace the zone hashes with an array of zone objects
  final_groups = []
  occ_groups.each do |occ_type, zns|
    # Sum the area and put all zones into an array
    area_m2 = 0.0
    gp_zns = []
    zns.each do |zn|
      area_m2 += zn['area']
      gp_zns << zn['zone']
    end
    area_ft2 = OpenStudio.convert(area_m2, 'm^2', 'ft^2').get

    # Determine the number of stories this group spans
    num_stories = OpenstudioStandards::Geometry.thermal_zones_get_number_of_stories_spanned(gp_zns)

    # Create a hash representing this group
    group = {}
    group['area_ft2'] = area_ft2
    group['type'] = occ_type
    group['stories'] = num_stories
    group['zones'] = gp_zns
    final_groups << group

    # Report out the final grouping
    OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "Final system type group: occ = #{group['type']}, area = #{group['area_ft2'].round} ft2, num stories = #{group['stories']}, zones:")
    group['zones'].sort.each_slice(5) do |zone_list|
      zone_names = []
      zone_list.each do |zone|
        zone_names << zone.name.get.to_s
      end
      OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.Model', "--- #{zone_names.join(', ')}")
    end
  end

  return final_groups
end

.model_set_building_north_axis(model, north_axis) ⇒ Boolean

Set the model’s north axis (degrees from true North)

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio Model object

  • north_axis (Float)

    Degrees from true North

Returns:

  • (Boolean)

    Returns true if successful, false otherwise



145
146
147
148
149
150
151
152
# File 'lib/openstudio-standards/geometry/modify.rb', line 145

def self.model_set_building_north_axis(model, north_axis)
  return false if north_axis.nil?

  building = model.getBuilding
  building.setNorthAxis(north_axis)

  return true
end

.model_sort_building_stories_and_get_min_multiplier(model) ⇒ Hash

sort building stories

Parameters:

  • model (OpenStudio::Model::Model)

    OpenStudio model object

Returns:

  • (Hash)

    An Hash with key OpenStudio BuildingStory objects and their minimum z value



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/openstudio-standards/geometry/create_bar.rb', line 114

def self.model_sort_building_stories_and_get_min_multiplier(model)
  sorted_building_stories = {}
  # loop through stories
  model.getBuildingStorys.sort.each do |story|
    story_min_z = nil
    # loop through spaces in story.
    story.spaces.sort.each do |space|
      space_z_min = OpenstudioStandards::Geometry.surfaces_get_z_values(space.surfaces.to_a).min + space.zOrigin
      if story_min_z.nil? || (story_min_z > space_z_min)
        story_min_z = space_z_min
      end
    end
    sorted_building_stories[story] = story_min_z
  end

  return sorted_building_stories
end

.space_create_point_at_center_of_floor(space, z_offset_m) ⇒ OpenStudio::Point3d

method to create a point object at the center of a floor

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

  • z_offset_m (Double)

    vertical offset in meters

Returns:

  • (OpenStudio::Point3d)

    point at the center of the space. return nil if point is not on floor in space.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/openstudio-standards/geometry/create.rb', line 12

def self.space_create_point_at_center_of_floor(space, z_offset_m)
  # find floors
  floor_surfaces = []
  space.surfaces.each { |surface| floor_surfaces << surface if surface.surfaceType == 'Floor' }

  # this method only works for flat (non-inclined) floors
  bounding_box = OpenStudio::BoundingBox.new
  floor_surfaces.each { |floor| bounding_box.addPoints(floor.vertices) }
  xmin = bounding_box.minX.get
  ymin = bounding_box.minY.get
  zmin = bounding_box.minZ.get
  xmax = bounding_box.maxX.get
  ymax = bounding_box.maxY.get

  x_pos = (xmin + xmax) / 2
  y_pos = (ymin + ymax) / 2
  z_pos = zmin + z_offset_m
  point_on_floor = OpenstudioStandards::Geometry.surfaces_contain_point?(floor_surfaces, OpenStudio::Point3d.new(x_pos, y_pos, zmin))

  if point_on_floor
    new_point = OpenStudio::Point3d.new(x_pos, y_pos, z_pos)
  else
    # don't make point, it doesn't appear to be inside of the space
    new_point = nil
  end

  return new_point
end

.space_get_adjacent_space_with_most_shared_wall_area(space, same_floor: true) ⇒ OpenStudio::Model::Space

Find the space that has the most wall area touching this space.

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

  • same_floor (Boolean) (defaults to: true)

    only consider spaces on the same floor

Returns:

  • (OpenStudio::Model::Space)

    OpenStudio Space object



480
481
482
483
# File 'lib/openstudio-standards/geometry/information.rb', line 480

def self.space_get_adjacent_space_with_most_shared_wall_area(space, same_floor: true)
  adjacent_space = OpenstudioStandards::Geometry.space_get_adjacent_spaces_with_shared_wall_areas(space, same_floor: same_floor)[0][0]
  return adjacent_space
end

.space_get_adjacent_spaces_with_shared_wall_areas(space, same_floor: true) ⇒ Hash

Get a sorted array of tuples containing a list of spaces and connected area in descending order

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

  • same_floor (Boolean) (defaults to: true)

    only consider spaces on the same floor

Returns:

  • (Hash)

    sorted hash with array of spaces and area



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/openstudio-standards/geometry/information.rb', line 409

def self.space_get_adjacent_spaces_with_shared_wall_areas(space, same_floor: true)
  same_floor_spaces = []
  spaces = []
  space.surfaces.each do |surface|
    adj_surface = surface.adjacentSurface
    unless adj_surface.empty?
      space.model.getSpaces.sort.each do |other_space|
        next if other_space == space

        other_space.surfaces.each do |surf|
          if surf == adj_surface.get
            spaces << other_space
          end
        end
      end
    end
  end
  # If looking for only spaces adjacent on the same floor.
  if same_floor == true
    if space.buildingStory.empty?
      OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Information', "Cannot get adjacent spaces of space #{space.name} since space not set to BuildingStory.")
      return nil
    end

    spaces.each do |other_space|
      if space.buildingStory.empty?
        OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.Geometry.Information', "One or more adjecent spaces to space #{space.name} is not assigned to a BuildingStory. Ensure all spaces are assigned.")
        return nil
      end

      if other_space.buildingStory.get == space.buildingStory.get
        same_floor_spaces << other_space
      end
    end
    spaces = same_floor_spaces
  end

  # now sort by areas.
  area_index = []
  array_hash = {}
  return array_hash if spaces.empty?

  # iterate through each surface in the space
  space.surfaces.each do |surface|
    # get the adjacent surface in another space.
    adj_surface = surface.adjacentSurface
    unless adj_surface.empty?
      # go through each of the adjacent spaces to find the matching surface/space.
      spaces.each_with_index do |other_space, index|
        next if other_space == space

        other_space.surfaces.each do |surf|
          if surf == adj_surface.get
            # initialize array index to zero for first time so += will work.
            area_index[index] = 0 if area_index[index].nil?
            area_index[index] += surf.grossArea
            array_hash[other_space] = area_index[index]
          end
        end
      end
    end
  end
  sorted_spaces = array_hash.sort_by { |_key, value| value }.reverse
  return sorted_spaces
end

.space_get_below_grade_wall_height(space) ⇒ Double

Finds heights of the first below grade walls and returns them as a numeric. Used when defining C Factor walls. Returns nil if the space is above grade.

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

Returns:

  • (Double)

    height in meters, or nil if undefined



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/openstudio-standards/geometry/information.rb', line 490

def self.space_get_below_grade_wall_height(space)
  # find height of first below-grade wall adjacent to the ground
  surface_height = nil
  space.surfaces.each do |surface|
    next unless surface.surfaceType == 'Wall'

    boundary_condition = surface.outsideBoundaryCondition
    next unless boundary_condition == 'OtherSideCoefficients' || boundary_condition.to_s.downcase.include?('ground')

    # calculate wall height as difference of maximum and minimum z values, assuming square, vertical walls
    z_values = []
    surface.vertices.each do |vertex|
      z_values << vertex.z
    end
    surface_height = z_values.max - z_values.min
  end

  return surface_height
end

.space_get_envelope_area(space, multiplier: true) ⇒ Double

Calculate the space envelope area. According to the 90.1 definition, building envelope include:

  1. “the elements of a building that separate conditioned spaces from the exterior”

  2. “the elements of a building that separate conditioned space from unconditioned space or that enclose semiheated spaces through which thermal energy may be transferred to or from the exterior, to or from unconditioned spaces or to or from conditioned spaces.”

Outside boundary conditions currently supported:

  • Adiabatic

  • Surface

  • Outdoors

  • Foundation

  • Ground

  • GroundFCfactorMethod

  • OtherSideCoefficients

  • OtherSideConditionsModel

  • GroundSlabPreprocessorAverage

  • GroundSlabPreprocessorCore

  • GroundSlabPreprocessorPerimeter

  • GroundBasementPreprocessorAverageWall

  • GroundBasementPreprocessorAverageFloor

  • GroundBasementPreprocessorUpperWall

  • GroundBasementPreprocessorLowerWall

Surface type currently supported:

  • Floor

  • Wall

  • RoofCeiling

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

  • multiplier (Boolean) (defaults to: true)

    account for space multiplier

Returns:

  • (Double)

    area in m^2



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/openstudio-standards/geometry/information.rb', line 297

def self.space_get_envelope_area(space, multiplier: true)
  area_m2 = 0.0

  # Get the space conditioning type
  std = Standard.build('90.1-2019') # delete once space methods refactored
  space_cond_type = std.space_conditioning_category(space)

  # Loop through all surfaces in this space
  space.surfaces.sort.each do |surface|
    # Only account for spaces that are conditioned or semi-heated
    next unless space_cond_type != 'Unconditioned'

    surf_cnt = false

    # Conditioned space OR semi-heated space <-> exterior
    # Conditioned space OR semi-heated space <-> ground
    if surface.outsideBoundaryCondition == 'Outdoors' || surface.isGroundSurface
      surf_cnt = true
    end

    # Conditioned space OR semi-heated space <-> unconditioned spaces
    unless surf_cnt
      # @todo add a case for 'Zone' when supported
      if surface.outsideBoundaryCondition == 'Surface'
        adj_space = surface.adjacentSurface.get.space.get
        adj_space_cond_type = std.space_conditioning_category(adj_space)
        surf_cnt = true unless adj_space_cond_type != 'Unconditioned'
      end
    end

    if surf_cnt
      # This surface
      area_m2 += surface.netArea
      # Subsurfaces in this surface
      surface.subSurfaces.sort.each do |subsurface|
        area_m2 += subsurface.netArea
      end
    end
  end

  if multiplier
    area_m2 *= space.multiplier
  end

  return area_m2
end

.space_get_exterior_wall_and_subsurface_and_roof_area(space, multiplier: false) ⇒ Double

Calculate the area of the exterior walls, including the area of the windows and doors on these walls, and the area of roofs.

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

  • multiplier (Boolean) (defaults to: false)

    account for space multiplier, default false

Returns:

  • (Double)

    area in m^2



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/openstudio-standards/geometry/information.rb', line 379

def self.space_get_exterior_wall_and_subsurface_and_roof_area(space, multiplier: false)
  area_m2 = 0.0

  # Loop through all surfaces in this space
  space.surfaces.sort.each do |surface|
    # Skip non-outdoor surfaces
    next unless surface.outsideBoundaryCondition == 'Outdoors'
    # Skip non-walls
    next unless surface.surfaceType == 'Wall' || surface.surfaceType == 'RoofCeiling'

    # This surface
    area_m2 += surface.netArea
    # Subsurfaces in this surface
    surface.subSurfaces.sort.each do |subsurface|
      area_m2 += subsurface.netArea
    end
  end

  if multiplier
    area_m2 *= space.multiplier
  end

  return area_m2
end

.space_get_exterior_wall_and_subsurface_area(space, multiplier: false) ⇒ Double

Calculate the area of the exterior walls, including the area of the windows and doors on these walls.

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

  • multiplier (Boolean) (defaults to: false)

    account for space multiplier, default false

Returns:

  • (Double)

    area in m^2



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/openstudio-standards/geometry/information.rb', line 349

def self.space_get_exterior_wall_and_subsurface_area(space, multiplier: false)
  area_m2 = 0.0

  # Loop through all surfaces in this space
  space.surfaces.sort.each do |surface|
    # Skip non-outdoor surfaces
    next unless surface.outsideBoundaryCondition == 'Outdoors'
    # Skip non-walls
    next unless surface.surfaceType == 'Wall'

    # This surface
    area_m2 += surface.netArea
    # Subsurfaces in this surface
    surface.subSurfaces.sort.each do |subsurface|
      area_m2 += subsurface.netArea
    end
  end

  if multiplier
    area_m2 *= space.multiplier
  end

  return area_m2
end

.space_get_f_floor_area(space) ⇒ Double

This function returns the space’s ground area. Assumes only one floor per space!

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

Returns:

  • (Double)

    area in m^2



550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
# File 'lib/openstudio-standards/geometry/information.rb', line 550

def self.space_get_f_floor_area(space)
  # Find space's floors with ground contact
  floors = []
  space.surfaces.each do |surface|
    if surface.surfaceType == 'Floor' && surface.outsideBoundaryCondition.to_s.downcase.include?('ground')
      floors << surface
    end
  end

  # If this space has no ground contact floors, return 0
  return 0.0 if floors.empty?

  # Raise a warning for any space with more than 1 ground contact floor surface.
  if floors.length > 1
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Information', "Space: #{space.name} has more than one ground contact floor. FFactorGroundFloorConstruction area in this space may be incorrect.")
  end

  # Get floor area
  floor = floors[0]
  area = floor.netArea

  return area
end

.space_get_f_floor_perimeter(space) ⇒ Double

This function returns the space’s ground perimeter length. Assumes only one floor per space!

Parameters:

  • space (OpenStudio::Model::Space)

    OpenStudio Space object

Returns:

  • (Double)

    length in meters



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'lib/openstudio-standards/geometry/information.rb', line 515

def self.space_get_f_floor_perimeter(space)
  # Find space's floors with ground contact
  floors = []
  space.surfaces.each do |surface|
    if surface.surfaceType == 'Floor' && surface.outsideBoundaryCondition.to_s.downcase.include?('ground')
      floors << surface
    end
  end

  # If this space has no ground contact floors, return 0
  return 0.0 if floors.empty?

  # Raise a warning for any space with more than 1 ground contact floor surface.
  if floors.length > 1
    OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.Geometry.Information', "Space: #{space.name} has more than one ground contact floor. FFactorGroundFloorConstruction perimeter in this space may be incorrect.")
  end

  # cycle through surfaces in the space and get adjacency length to the floor
  floor = floors[0]
  perimeter = 0.0
  space.surfaces.each do |surface|
    # find perimeter of floor by finding intersecting outdoor walls and measuring the intersection
    if surface.surfaceType == 'Wall' && surface.outsideBoundaryCondition == 'Outdoors'
      perimeter += OpenstudioStandards::Geometry.wall_and_floor_intersection_length(surface, floor)
    end
  end

  return perimeter
end

.spaces_get_exterior_area(spaces, multiplier: true) ⇒ Double

Get the total exterior area of selected spaces

Parameters:

  • spaces (Array<OpenStudio::Model::Space>)

    array of OpenStudio Space objects

  • multiplier (Boolean) (defaults to: true)

    account for space multiplier

Returns:

  • (Double)

    total exterior area of spaces in square meters



597
598
599
600
601
602
603
604
# File 'lib/openstudio-standards/geometry/information.rb', line 597

def self.spaces_get_exterior_area(spaces, multiplier: true)
  total_area = 0.0
  spaces.each do |space|
    space_multiplier = multiplier ? space.multiplier : 1.0
    total_area += space.exteriorArea * space_multiplier
  end
  return total_area
end

.spaces_get_exterior_wall_area(spaces, multiplier: true) ⇒ Double

Get the total exterior wall area of selected spaces

Parameters:

  • spaces (Array<OpenStudio::Model::Space>)

    array of OpenStudio Space objects

  • multiplier (Boolean) (defaults to: true)

    account for space multiplier

Returns:

  • (Double)

    total exterior wall area of spaces in square meters



611
612
613
614
615
616
617
618
# File 'lib/openstudio-standards/geometry/information.rb', line 611

def self.spaces_get_exterior_wall_area(spaces, multiplier: true)
  total_area = 0.0
  spaces.each do |space|
    space_multiplier = multiplier ? space.multiplier : 1.0
    total_area += space.exteriorWallArea * space_multiplier
  end
  return total_area
end

.spaces_get_floor_area(spaces, multiplier: true) ⇒ Double

Get the total floor area of selected spaces

Parameters:

  • spaces (Array<OpenStudio::Model::Space>)

    array of OpenStudio Space objects

  • multiplier (Boolean) (defaults to: true)

    account for space multiplier

Returns:

  • (Double)

    total floor area of spaces in square meters



583
584
585
586
587
588
589
590
# File 'lib/openstudio-standards/geometry/information.rb', line 583

def self.spaces_get_floor_area(spaces, multiplier: true)
  total_area = 0.0
  spaces.each do |space|
    space_multiplier = multiplier ? space.multiplier : 1.0
    total_area += space.floorArea * space_multiplier
  end
  return total_area
end

.sub_surface_create_point_at_specific_height(sub_surface, reference_floor, distance_from_window_m, height_above_subsurface_bottom_m) ⇒ OpenStudio::Point3d

method to create a point object from a sub surface

Parameters:

  • sub_surface (OpenStudio::Model::SubSurface)

    OpenStudio SubSurface object

  • reference_floor (OpenStudio::Model::SubSurface)

    OpenStudio SubSurface object

  • distance_from_window_m (Double)

    distance in from the window, in meters

  • height_above_subsurface_bottom_m (Double)

    height above the bottom of the subsurface, in meters

Returns:

  • (OpenStudio::Point3d)

    point at the center of the space. return nil if point is not on floor in space.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/openstudio-standards/geometry/create.rb', line 48

def self.sub_surface_create_point_at_specific_height(sub_surface, reference_floor, distance_from_window_m, height_above_subsurface_bottom_m)
  window_outward_normal = sub_surface.outwardNormal
  window_centroid = OpenStudio.getCentroid(sub_surface.vertices).get
  window_outward_normal.setLength(distance_from_window_m)
  vertex = window_centroid + window_outward_normal.reverseVector
  vertex_on_floorplane = reference_floor.plane.project(vertex)
  floor_outward_normal = reference_floor.outwardNormal
  floor_outward_normal.setLength(height_above_subsurface_bottom_m)

  floor_surfaces = []
  space.surfaces.each { |surface| floor_surfaces << surface if surface.surfaceType == 'Floor' }

  point_on_floor = OpenstudioStandards::Geometry.surfaces_contain_point?(floor_surfaces, vertex_on_floorplane)

  if point_on_floor
    new_point = vertex_on_floorplane + floor_outward_normal.reverseVector
  else
    # don't make point, it doesn't appear to be inside of the space
    # nil
    new_point = vertex_on_floorplane + floor_outward_normal.reverseVector
  end

  return new_point
end

.sub_surface_reduce_area_by_percent_by_raising_sill(sub_surface, percent_reduction) ⇒ Boolean

Reduce the area of the subsurface by raising the sill height

Parameters:

  • sub_surface (OpenStudio::Model::SubSurface)

    OpenStudio SubSurface object

  • percent_reduction (Double)

    the fractional amount to reduce the area

Returns:

  • (Boolean)

    returns true if successful, false if not



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/openstudio-standards/geometry/modify.rb', line 51

def self.sub_surface_reduce_area_by_percent_by_raising_sill(sub_surface, percent_reduction)
  # Find the min and max z values
  min_z_val = 99_999
  max_z_val = -99_999
  sub_surface.vertices.each do |vertex|
    # Min z value
    if vertex.z < min_z_val
      min_z_val = vertex.z
    end
    # Max z value
    if vertex.z > max_z_val
      max_z_val = vertex.z
    end
  end

  # Calculate the window height
  height = max_z_val - min_z_val

  # Calculate the new sill height
  z_delta = height * percent_reduction

  # Reset the z value of the lowest points within a certain threshold
  new_vertices = []
  sub_surface.vertices.each do |vertex|
    if (vertex.z - min_z_val).abs < 0.025
      new_vertices << (vertex + OpenStudio::Vector3d.new(0.0, 0.0, z_delta))
    else
      new_vertices << vertex
    end
  end

  # Reset the vertices
  sub_surface.setVertices(new_vertices)

  return true
end

.sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(sub_surface, percent_reduction) ⇒ Boolean

Reduce the area of the subsurface by shrinking it toward the centroid

Parameters:

  • sub_surface (OpenStudio::Model::SubSurface)

    OpenStudio SubSurface object

  • percent_reduction (Double)

    the fractional amount to reduce the area

Returns:

  • (Boolean)

    returns true if successful, false if not

Author:

  • Julien Marrec



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/openstudio-standards/geometry/modify.rb', line 14

def self.sub_surface_reduce_area_by_percent_by_shrinking_toward_centroid(sub_surface, percent_reduction)
  # if percent_reduction > 1=> percent increase instead of reduction
  mult = percent_reduction <= 1 ? 1 - percent_reduction : percent_reduction
  scale_factor = mult**0.5

  # Get the centroid (Point3d)
  g = sub_surface.centroid

  # Create an array to collect the new vertices
  new_vertices = []

  # Loop on vertices (Point3ds)
  sub_surface.vertices.each do |vertex|
    # Point3d - Point3d = Vector3d
    # Vector from centroid to vertex (GA, GB, GC, etc)
    centroid_vector = vertex - g

    # Resize the vector (done in place) according to scale_factor
    centroid_vector.setLength(centroid_vector.length * scale_factor)

    # Move the vertex toward the centroid
    vertex = g + centroid_vector

    new_vertices << vertex
  end

  # Assign the new vertices to the self
  sub_surface.setVertices(new_vertices)

  return true
end

.sub_surface_vertical_rectangle?(sub_surface) ⇒ Boolean

Determine if the sub surface is a vertical rectangle, meaning a rectangle where the bottom is parallel to the ground.

Parameters:

  • sub_surface (OpenStudio::Model::SubSurface)

    OpenStudio SubSurface object

Returns:

  • (Boolean)

    returns true if the surface is a vertical rectangle, false if not



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/openstudio-standards/geometry/information.rb', line 235

def self.sub_surface_vertical_rectangle?(sub_surface)
  # Get the vertices once
  verts = sub_surface.vertices

  # Check for 4 vertices
  return false unless verts.size == 4

  # Check if the 2 lowest z-values
  # are the same
  z_vals = []
  verts.each do |vertex|
    z_vals << vertex.z
  end
  z_vals = z_vals.sort
  return false unless z_vals[0] == z_vals[1]

  # Check if the diagonals are equal length
  diag_a = verts[0] - verts[2]
  diag_b = verts[1] - verts[3]
  return false unless diag_a.length == diag_b.length

  # If here, we have a rectangle
  return true
end

.surface_get_absolute_azimuth(surface) ⇒ Double

Calculate a surface’s absolute azimuth

Parameters:

  • surface (OpenStudio::Model::Surface)

    OpenStudio Surface object

Returns:

  • (Double)

    surface absolute azimuth in degrees



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/openstudio-standards/geometry/information.rb', line 136

def self.surface_get_absolute_azimuth(surface)
  # Get associated space
  space = surface.space.get

  # Get model object
  model = surface.model

  # Calculate azimuth
  surface_azimuth_rel_space = OpenStudio.convert(surface.azimuth, 'rad', 'deg').get
  space_dir_rel_north = space.directionofRelativeNorth
  building_dir_rel_north = model.getBuilding.northAxis
  surface_abs_azimuth = surface_azimuth_rel_space + space_dir_rel_north + building_dir_rel_north
  surface_abs_azimuth -= 360.0 until surface_abs_azimuth < 360.0

  return surface_abs_azimuth
end

.surface_get_cardinal_direction(surface) ⇒ String

Determine a surface absolute cardinal direction

Parameters:

  • surface (OpenStudio::Model::Surface)

    OpenStudio Surface object

Returns:

  • (String)

    surface absolute cardinal direction, ‘N’, ‘E’, ‘S, ’W’



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/openstudio-standards/geometry/information.rb', line 157

def self.surface_get_cardinal_direction(surface)
  # Get the surface's absolute azimuth
  surface_abs_azimuth = OpenstudioStandards::Geometry.surface_get_absolute_azimuth(surface)

  # Determine the surface's cardinal direction
  cardinal_direction = ''
  if (surface_abs_azimuth >= 0 && surface_abs_azimuth <= 45) || (surface_abs_azimuth > 315 && surface_abs_azimuth <= 360)
    cardinal_direction = 'N'
  elsif surface_abs_azimuth > 45 && surface_abs_azimuth <= 135
    cardinal_direction = 'E'
  elsif surface_abs_azimuth > 135 && surface_abs_azimuth <= 225
    cardinal_direction = 'S'
  elsif surface_abs_azimuth > 225 && surface_abs_azimuth <= 315
    cardinal_direction = 'W'
  end

  return cardinal_direction
end

.surface_get_door_to_wall_ratio(surface) ⇒ Double

Calculate the door to wall ratio of a surface

Parameters:

  • surface (OpenStudio::Model::Surface)

    OpenStudio Surface object

Returns:

  • (Double)

    door to wall ratio of a surface



121
122
123
124
125
126
127
128
129
130
# File 'lib/openstudio-standards/geometry/information.rb', line 121

def self.surface_get_door_to_wall_ratio(surface)
  surface_area = surface.grossArea
  surface_door_area = 0.0
  surface.subSurfaces.sort.each do |ss|
    next unless ss.subSurfaceType == 'Door'

    surface_door_area += ss.netArea
  end
  return surface_door_area / surface_area
end

.surface_get_edges(surface) ⇒ Array<Array(OpenStudio::Point3D, OpenStudio::Point3D)>

Returns an array of OpenStudio::Point3D pairs of an OpenStudio::Model::Surface’s edges. Used to calculate surface intersections.

Parameters:

  • surface (OpenStudio::Model::Surface)

    OpenStudio surface object

Returns:

  • (Array<Array(OpenStudio::Point3D, OpenStudio::Point3D)>)

    Array of pair of points describing the line segment of an edge



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/openstudio-standards/geometry/information.rb', line 78

def self.surface_get_edges(surface)
  vertices = surface.vertices
  n_vertices = vertices.length

  # Create edge hash that keeps track of all edges in surface. An edge is defined here as an array of length 2
  # containing two OpenStudio::Point3Ds that define the line segment representing a surface edge.
  # format edge_array[i] = [OpenStudio::Point3D, OpenStudio::Point3D]
  edge_array = []

  # Iterate through each vertex in the surface and construct an edge for it
  for edge_counter in 0..n_vertices - 1

    # If not the last vertex in surface
    if edge_counter < n_vertices - 1
      edge_array << [vertices[edge_counter], vertices[edge_counter + 1]]
    else
      # Make index adjustments for final index in vertices array
      edge_array << [vertices[edge_counter], vertices[0]]
    end
  end

  return edge_array
end

.surface_get_window_to_wall_ratio(surface) ⇒ Double

Calculate the window to wall ratio of a surface

Parameters:

  • surface (OpenStudio::Model::Surface)

    OpenStudio Surface object

Returns:

  • (Double)

    window to wall ratio of a surface



106
107
108
109
110
111
112
113
114
115
# File 'lib/openstudio-standards/geometry/information.rb', line 106

def self.surface_get_window_to_wall_ratio(surface)
  surface_area = surface.grossArea
  surface_fene_area = 0.0
  surface.subSurfaces.sort.each do |ss|
    next unless ss.subSurfaceType == 'FixedWindow' || ss.subSurfaceType == 'OperableWindow' || ss.subSurfaceType == 'GlassDoor'

    surface_fene_area += ss.netArea
  end
  return surface_fene_area / surface_area
end

.surfaces_contain_point?(surfaces, point) ⇒ Boolean

Check if a point is contained on any surface in an array of surfaces

Parameters:

  • surfaces (Array<OpenStudio::Model::Surface>)

    Array of OpenStudio Surface objects

  • point (OpenStudio::Point3d)

    Point3d object

Returns:

  • (Boolean)

    true if on a surface in surface array, false if not



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/openstudio-standards/geometry/information.rb', line 205

def self.surfaces_contain_point?(surfaces, point)
  on_surface = false

  surfaces.each do |surface|
    # Check if sensor is on floor plane (I need to loop through all floors)
    plane = surface.plane
    point_on_plane = plane.project(point)

    face_transform = OpenStudio::Transformation.alignFace(surface.vertices)
    face_vertices = face_transform * surface.vertices
    face_point_on_plane = face_transform * point_on_plane

    if OpenStudio.pointInPolygon(face_point_on_plane, face_vertices.reverse, 0.01)
      # initial_sensor location lands in this surface's polygon
      on_surface = true
    end
  end

  return on_surface
end

.surfaces_get_z_values(surfaces) ⇒ Array<Double>

return an array of z values for surfaces passed in. The values will be relative to the parent origin.

Parameters:

  • surfaces (Array<OpenStudio::Model::Surface>)

    Array of OpenStudio Surface objects

Returns:

  • (Array<Double>)

    array of z values in meters



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/openstudio-standards/geometry/information.rb', line 184

def self.surfaces_get_z_values(surfaces)
  z_values = []

  # loop over all surfaces
  surfaces.each do |surface|
    # get the existing vertices
    vertices = surface.vertices
    vertices.each do |vertex|
      # push z value to array
      z_values << vertex.z
    end
  end

  return z_values
end

.thermal_zone_get_adjacent_zones_with_shared_walls(thermal_zone, same_floor: true) ⇒ Array<OpenStudio::Model::ThermalZone>

Return an array of zones that share a wall with the zone

Parameters:

  • thermal_zone (OpenStudio::Model::ThermalZone)

    OpenStudio ThermalZone object

  • same_floor (Boolean) (defaults to: true)

    only valid option for now is true

Returns:

  • (Array<OpenStudio::Model::ThermalZone>)

    Array of OpenStudio ThermalZone objects



629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
# File 'lib/openstudio-standards/geometry/information.rb', line 629

def self.thermal_zone_get_adjacent_zones_with_shared_walls(thermal_zone, same_floor: true)
  adjacent_zones = []

  thermal_zone.spaces.each do |space|
    adj_spaces = OpenstudioStandards::Geometry.space_get_adjacent_spaces_with_shared_wall_areas(space, same_floor: same_floor)
    adj_spaces.each do |k, v|
      # skip if space is in current thermal zone.
      next unless space.thermalZone.is_initialized
      next if k.thermalZone.get == thermal_zone

      adjacent_zones << k.thermalZone.get
    end
  end

  adjacent_zones = adjacent_zones.uniq

  return adjacent_zones
end

.thermal_zones_get_number_of_stories_spanned(thermal_zones) ⇒ Integer

Determine the number of stories spanned by the supplied thermal zones. If all zones on one of the stories have an identical multiplier, assume that the multiplier is a floor multiplier and increase the number of stories accordingly. Stories do not have to be contiguous.

Parameters:

  • thermal_zones (Array<OpenStudio::Model::ThermalZone>)

    An array of OpenStudio ThermalZone objects

Returns:

  • (Integer)

    The number of stories spanned by the thermal zones



659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
# File 'lib/openstudio-standards/geometry/information.rb', line 659

def self.thermal_zones_get_number_of_stories_spanned(thermal_zones)
  # Get the story object for all zones
  stories = []
  thermal_zones.each do |zone|
    zone.spaces.each do |space|
      story = space.buildingStory
      next if story.empty?

      stories << story.get
    end
  end

  # Reduce down to the unique set of stories
  stories = stories.uniq

  # Tally up stories including multipliers
  num_stories = 0
  stories.each do |story|
    num_stories += OpenstudioStandards::Geometry.building_story_get_floor_multiplier(story)
  end

  return num_stories
end

.wall_and_floor_intersection_length(wall, floor) ⇒ Double

Note:

this calculation has a few assumptions:

This function returns the length of intersection between a wall and floor sharing space. Primarily used for FFactorGroundFloorConstruction exposed perimeter calculations.

  • Floors are flat. This means they have a constant z-axis value.

  • If a wall shares an edge with a floor, it’s assumed that edge intersects with only this floor.

  • The wall and floor share a common space. This space is assumed to only have one floor!

Parameters:

  • wall (OpenStudio::Model::Surface)

    wall surface being compared to the floor of interest

  • floor (OpenStudio::Model::Surface)

    floor occupying same space as wall. Edges checked for interesections with wall

Returns:

  • (Double)

    returns the intersection/overlap length of the wall and floor in meters



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/openstudio-standards/geometry/information.rb', line 31

def self.wall_and_floor_intersection_length(wall, floor)
  # Used for determining if two points are 'equal' if within this length
  tolerance = 0.0001

  # Get floor and wall edges
  wall_edge_array = OpenstudioStandards::Geometry.surface_get_edges(wall)
  floor_edge_array = OpenstudioStandards::Geometry.surface_get_edges(floor)

  # Floor assumed flat and constant in x-y plane (i.e. a single z value)
  floor_z_value = floor_edge_array[0][0].z

  # Iterate through wall edges
  wall_edge_array.each do |wall_edge|
    wall_edge_p1 = wall_edge[0]
    wall_edge_p2 = wall_edge[1]

    # If points representing the wall surface edge have different z-coordinates, this edge is not parallel to the
    # floor and can be skipped

    if tolerance <= (wall_edge_p1.z - wall_edge_p2.z).abs
      next
    end

    # If wall edge is parallel to the floor, ensure it's on the same x-y plane as the floor.
    if tolerance <= (wall_edge_p1.z - floor_z_value).abs
      next
    end

    # If the edge is parallel with the floor and in the same x-y plane as the floor, assume an intersection the
    # length of the wall edge
    intersect_vector = wall_edge_p1 - wall_edge_p2
    edge_vector = OpenStudio::Vector3d.new(intersect_vector.x, intersect_vector.y, intersect_vector.z)
    return(edge_vector.length)
  end

  # If no edges intersected, return 0
  return 0.0
end