lib/openstudio-standards/geometry/information.rb in openstudio-standards-0.5.0 vs lib/openstudio-standards/geometry/information.rb in openstudio-standards-0.6.0.rc1
- old
+ new
@@ -1,11 +1,12 @@
-# Methods to get information about model geometry
-# Many of these methods may be moved to core OpenStudio
module OpenstudioStandards
+ # This Module provides methods to create, modify, and get information about model geometry
module Geometry
- # @!group Information
+ # Methods to get information about model geometry
+ # @!group Information:Calculations
+
# calculate aspect ratio from area and perimeter
#
# @param area [Double] area
# @param perimeter [Double] perimeter
# @return [Double] aspect ratio
@@ -15,17 +16,174 @@
aspect_ratio = length / width
return aspect_ratio
end
- # @!endgroup Information
+ # This function returns the length of intersection between a wall and floor sharing space. Primarily used for
+ # FFactorGroundFloorConstruction exposed perimeter calculations.
+ # @note this calculation has a few assumptions:
+ # - 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!
+ #
+ # @param wall[OpenStudio::Model::Surface] wall surface being compared to the floor of interest
+ # @param floor[OpenStudio::Model::Surface] floor occupying same space as wall. Edges checked for interesections with wall
+ # @return [Double] returns the intersection/overlap length of the wall and floor in meters
+ 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
+
+ # @!endgroup Information:Calculations
+
+ # @!group Information:Surface
+
+ # Returns an array of OpenStudio::Point3D pairs of an OpenStudio::Model::Surface's edges. Used to calculate surface intersections.
+ #
+ # @param surface[OpenStudio::Model::Surface] OpenStudio surface object
+ # @return [Array<Array(OpenStudio::Point3D, OpenStudio::Point3D)>] Array of pair of points describing the line segment of an edge
+ 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
+
+ # Calculate the window to wall ratio of a surface
+ #
+ # @param surface [OpenStudio::Model::Surface] OpenStudio Surface object
+ # @return [Double] window to wall ratio of a surface
+ 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
+
+ # Calculate the door to wall ratio of a surface
+ #
+ # @param surface [OpenStudio::Model::Surface] OpenStudio Surface object
+ # @return [Double] door to wall ratio of a surface
+ 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
+
+ # Calculate a surface's absolute azimuth
+ #
+ # @param surface [OpenStudio::Model::Surface] OpenStudio Surface object
+ # @return [Double] surface absolute azimuth in degrees
+ 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
+
+ # Determine a surface absolute cardinal direction
+ #
+ # @param surface [OpenStudio::Model::Surface] OpenStudio Surface object
+ # @return [String] surface absolute cardinal direction, 'N', 'E', 'S, 'W'
+ 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
+ cardinal_direction = 'N'
+ elsif 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
+
+ # @!endgroup Information:Surface
+
# @!group Information:Surfaces
# return an array of z values for surfaces passed in. The values will be relative to the parent origin.
#
- # @param surfaces [Array<OpenStudio::Model::Surface>] array of Surface objects
+ # @param surfaces [Array<OpenStudio::Model::Surface>] Array of OpenStudio Surface objects
# @return [Array<Double>] array of z values in meters
def self.surfaces_get_z_values(surfaces)
z_values = []
# loop over all surfaces
@@ -41,11 +199,11 @@
return z_values
end
# Check if a point is contained on any surface in an array of surfaces
#
- # @param surfaces [Array<OpenStudio::Model::Surface>] array of Surface objects
+ # @param surfaces [Array<OpenStudio::Model::Surface>] Array of OpenStudio Surface objects
# @param point [OpenStudio::Point3d] Point3d object
# @return [Boolean] true if on a surface in surface array, false if not
def self.surfaces_contain_point?(surfaces, point)
on_surface = false
@@ -67,19 +225,363 @@
return on_surface
end
# @!endgroup Information:Surfaces
+ # @!group Information:SubSurface
+
+ # Determine if the sub surface is a vertical rectangle,
+ # meaning a rectangle where the bottom is parallel to the ground.
+ #
+ # @param sub_surface [OpenStudio::Model::SubSurface] OpenStudio SubSurface object
+ # @return [Boolean] returns true if the surface is a vertical rectangle, false if not
+ 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
+
+ # @!endgroup Information:SubSurface
+
# @!group Information:Space
+ # 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
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @param multiplier [Boolean] account for space multiplier
+ # @return [Double] area in m^2
+ 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
+
+ # Calculate the area of the exterior walls, including the area of the windows and doors on these walls.
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @param multiplier [Boolean] account for space multiplier, default false
+ # @return [Double] area in m^2
+ 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
+
+ # Calculate the area of the exterior walls, including the area of the windows and doors on these walls, and the area of roofs.
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @param multiplier [Boolean] account for space multiplier, default false
+ # @return [Double] area in m^2
+ 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
+
+ # Get a sorted array of tuples containing a list of spaces and connected area in descending order
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @param same_floor [Boolean] only consider spaces on the same floor
+ # @return [Hash] sorted hash with array of spaces and area
+ 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.size.zero?
+
+ # 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
+
+ # Find the space that has the most wall area touching this space.
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @param same_floor [Boolean] only consider spaces on the same floor
+ # @return [OpenStudio::Model::Space] OpenStudio Space object
+ 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
+
+ # 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.
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @return [Double] height in meters, or nil if undefined
+ 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
+
+ # This function returns the space's ground perimeter length.
+ # Assumes only one floor per space!
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @return [Double] length in meters
+ 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
+
+ # This function returns the space's ground area.
+ # Assumes only one floor per space!
+ #
+ # @param space [OpenStudio::Model::Space] OpenStudio Space object
+ # @return [Double] area in m^2
+ 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
+
# @!endgroup Information:Space
# @!group Information:Spaces
# Get the total floor area of selected spaces
#
- # @param spaces [Array<OpenStudio::Model::Space>] array of Space objects
+ # @param spaces [Array<OpenStudio::Model::Space>] array of OpenStudio Space objects
# @param multiplier [Boolean] account for space multiplier
# @return [Double] total floor area of spaces in square meters
def self.spaces_get_floor_area(spaces, multiplier: true)
total_area = 0.0
spaces.each do |space|
@@ -89,11 +591,11 @@
return total_area
end
# Get the total exterior area of selected spaces
#
- # @param spaces [Array<OpenStudio::Model::Space>] array of Space objects
+ # @param spaces [Array<OpenStudio::Model::Space>] array of OpenStudio Space objects
# @param multiplier [Boolean] account for space multiplier
# @return [Double] total exterior area of spaces in square meters
def self.spaces_get_exterior_area(spaces, multiplier: true)
total_area = 0.0
spaces.each do |space|
@@ -103,11 +605,11 @@
return total_area
end
# Get the total exterior wall area of selected spaces
#
- # @param spaces [Array<OpenStudio::Model::Space>] array of Space objects
+ # @param spaces [Array<OpenStudio::Model::Space>] array of OpenStudio Space objects
# @param multiplier [Boolean] account for space multiplier
# @return [Double] total exterior wall area of spaces in square meters
def self.spaces_get_exterior_wall_area(spaces, multiplier: true)
total_area = 0.0
spaces.each do |space|
@@ -117,24 +619,87 @@
return total_area
end
# @!endgroup Information:Spaces
+ # @!group Information:ThermalZone
+
+ # Return an array of zones that share a wall with the zone
+ #
+ # @param thermal_zone [OpenStudio::Model::ThermalZone] OpenStudio ThermalZone object
+ # @param same_floor [Boolean] only valid option for now is true
+ # @return [Array<OpenStudio::Model::ThermalZone>] Array of OpenStudio ThermalZone objects
+ 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
+
+ # @!endgroup Information:ThermalZone
+
+ # @!group Information:ThermalZones
+
+ # 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.
+ #
+ # @param thermal_zones [Array<OpenStudio::Model::ThermalZone>] An array of OpenStudio ThermalZone objects
+ # @return [Integer] The number of stories spanned by the thermal zones
+ 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
+
+ # @!endgroup Information:ThermalZones
+
# @!group Information:Story
# Calculate the story exterior wall perimeter. Selected story should have above grade walls. If not perimeter may return zero.
#
# @param story [OpenStudio::Model::BuildingStory]
# @param multiplier_adjustment [Double] 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.
# @param exterior_boundary_conditions [Array<String>] Array of strings of exterior boundary conditions.
# @param bounding_box [OpenStudio::BoundingBox] bounding box to determine which spaces are included
# @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
- def self.story_get_exterior_wall_perimeter(story,
- multiplier_adjustment: nil,
- exterior_boundary_conditions: ['Outdoors', 'Ground'],
- bounding_box: nil)
+ 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 = {}
@@ -174,11 +739,11 @@
# 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 tollerance for each array entry
+ # 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
@@ -218,16 +783,16 @@
# 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.Create', " * #{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.")
+ 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.Create', " * #{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.")
+ 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
@@ -246,64 +811,218 @@
end
return { perimeter: perimeter, party_walls: party_walls }
end
+ # 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.
+ #
+ # @param building_story [OpenStudio::Model::BuildingStory] OpenStudio BuildingStory object
+ # @return [Integer] return the floor multiplier for this story, returning 1 if no floor multiplier.
+ 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.size.zero?
+ 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
+
+ # 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.
+ #
+ # @param building_story [OpenStudio::Model::BuildingStory] OpenStudio BuildingStory object
+ # @return [Double] the minimum height in meters
+ 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?
+ z = z_heights.min
+ else
+ 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.")
+ end
+
+ return z
+ end
+
+ # Get an array of OpenStudio ThermalZone objects for an OpenStudio BuildingStory
+ #
+ # @param building_story [OpenStudio::Model::BuildingStory] OpenStudio BuildingStory object
+ # @return [Array<OpenStudio::Model::ThermalZone>] Array of OpenStudio ThermalZone objects, empty array if none
+ 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
+
# @!endgroup Information:Story
# @!group Information:Model
+ # 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.
+ #
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
+ # @param minimum_height [Double] The base height of the desired story, in meters.
+ # @param tolerance [Double] tolerance for comparison, in m. Default is 0.3 m ~1ft
+ # @return [OpenStudio::Model::BuildingStory] OpenStudio BuildingStory object, nil if none matching
+ 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
+
+ # Returns an array of the above ground building stories in the model.
+ #
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
+ # @return [Array<OpenStudio::Model::BuildingStory>] Array of OpenStudio BuildingStory objects, empty array if none
+ def self.model_get_building_stories_above_ground(model)
+ above_ground_stories = []
+ model.getBuildingStorys.sort.each do |story|
+ z = story.nominalZCoordinate
+ unless z.empty?
+ above_ground_stories << story if z.to_f >= 0
+ end
+ end
+ return above_ground_stories
+ end
+
+ # Returns an array of the below ground building stories in the model.
+ #
+ # @param model [OpenStudio::Model::Model] OpenStudio model object
+ # @return [Array<OpenStudio::Model::BuildingStory>] Array of OpenStudio BuildingStory objects, empty array if none
+ def self.model_get_building_stories_below_ground(model)
+ below_ground_stories = []
+ model.getBuildingStorys.sort.each do |story|
+ z = story.nominalZCoordinate
+ unless z.empty?
+ below_ground_stories << story if z.to_f < 0
+ end
+ end
+ return below_ground_stories
+ end
+
# Returns the window to wall ratio
#
# @param model [OpenStudio::Model::Model] OpenStudio model object
# @param spaces [Array<OpenStudio::Model::Space>] optional array of Space objects.
- # If provided, the return will report for only those spaces.
+ # If provided, the return will report for only those spaces.
+ # @param cardinal_direction [String] Cardinal direction 'N', 'E', 'S', 'W'
+ # If provided, the return will report for only the provided cardinal direction
# @return [Double] window to wall ratio
- def self.model_get_exterior_window_to_wall_ratio(model, spaces: [])
+ def self.model_get_exterior_window_to_wall_ratio(model,
+ spaces: [],
+ cardinal_direction: nil)
# counters
- total_gross_ext_wall_area = 0
- total_ext_window_area = 0
+ 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?
zone_multiplier = zone.get.multiplier
if zone_multiplier > 1
end
else
- zone_multiplier = 1 # space is not in a thermal zone
+ # space is not in a thermal zone
+ zone_multiplier = 1
end
- space.surfaces.each do |s|
- next if s.surfaceType != 'Wall'
- next if s.outsideBoundaryCondition != 'Outdoors'
+ # 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'
- surface_gross_area = s.grossArea * zone_multiplier
+ # 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
- s.subSurfaces.each do |sub_surface|
+ 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
- result = total_ext_window_area / total_gross_ext_wall_area
- else
- result = 0.0
+ if total_gross_ext_wall_area > 0.0
+ window_to_wall_ratio = total_ext_window_area / total_gross_ext_wall_area
end
- return result
+ return window_to_wall_ratio
end
# Returns the wall area and window area by orientation
#
# @param model [OpenStudio::Model::Model] OpenStudio model object
@@ -387,11 +1106,11 @@
#
# @param model [OpenStudio::Model::Model] OpenStudio model object
# @return [Double] perimeter length in meters
# @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
- def self.model_get_perimeter_length(model)
+ def self.model_get_perimeter(model)
perimeter = 0.0
model.getSpaces.sort.each do |space|
# counter to use later
edge_hash = {}
edge_counter = 0
@@ -428,10 +1147,10 @@
# 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 tollerance for each array entry
+ # 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