require_relative "constants" require_relative "unit_conversions" require_relative "util" class WholeBuildingGeometry def self.has_rear_units(model, runner, units) units.each do |unit| unit.spaces.each do |space| facades = [] space.surfaces.each do |surface| next if surface.surfaceType.downcase != "wall" next if surface.outsideBoundaryCondition.downcase != "outdoors" facade = Geometry.get_facade_for_surface(surface) unless facades.include? facade facades << facade end end next if facades.empty? if facades.include? WholeBuildingConstants.FacadeFront and facades.include? WholeBuildingConstants.FacadeBack elsif facades.include? WholeBuildingConstants.FacadeLeft and facades.include? WholeBuildingConstants.FacadeRight else return true end end end return false end def self.get_abs_azimuth(azimuth_type, relative_azimuth, building_orientation, offset = 180.0) azimuth = nil if azimuth_type == WholeBuildingConstants.CoordRelative azimuth = relative_azimuth + building_orientation + offset elsif azimuth_type == WholeBuildingConstants.CoordAbsolute azimuth = relative_azimuth + offset end # Ensure azimuth is >=0 and <=360 while azimuth < 0.0 azimuth += 360.0 end while azimuth >= 360.0 azimuth -= 360.0 end return azimuth end def self.get_abs_tilt(tilt_type, relative_tilt, roof_tilt, latitude) if tilt_type == WholeBuildingConstants.TiltPitch return relative_tilt + roof_tilt elsif tilt_type == WholeBuildingConstants.TiltLatitude return relative_tilt + latitude elsif tilt_type == WholeBuildingConstants.CoordAbsolute return relative_tilt end end def self.initialize_transformation_matrix(m) m[0, 0] = 1 m[1, 1] = 1 m[2, 2] = 1 m[3, 3] = 1 return m end def self.get_surface_dimensions(surface) least_x = 9e99 greatest_x = -9e99 least_y = 9e99 greatest_y = -9e99 least_z = 9e99 greatest_z = -9e99 surface.vertices.each do |vertex| least_x = [vertex.x, least_x].min greatest_x = [vertex.x, greatest_x].max least_y = [vertex.y, least_y].min greatest_y = [vertex.y, greatest_y].max least_z = [vertex.z, least_z].min greatest_z = [vertex.z, greatest_z].max end l = greatest_x - least_x w = greatest_y - least_y h = greatest_z - least_z return l, w, h end def self.get_building_stories(spaces) space_min_zs = [] spaces.each do |space| next if not self.space_is_finished(space) surfaces_min_zs = [] space.surfaces.each do |surface| zvalues = self.getSurfaceZValues([surface]) surfaces_min_zs << zvalues.min + UnitConversions.convert(space.zOrigin, "m", "ft") end space_min_zs << surfaces_min_zs.min end return space_min_zs.uniq.length end def self.get_above_grade_building_stories(spaces) space_min_zs = [] spaces.each do |space| next if not self.space_is_finished(space) next if not self.space_is_above_grade(space) surfaces_min_zs = [] space.surfaces.each do |surface| zvalues = self.getSurfaceZValues([surface]) surfaces_min_zs << zvalues.min + UnitConversions.convert(space.zOrigin, "m", "ft") end space_min_zs << surfaces_min_zs.min end return space_min_zs.uniq.length end def self.make_one_space_from_multiple_spaces(model, spaces) new_space = OpenStudio::Model::Space.new(model) spaces.each do |space| space.surfaces.each do |surface| if surface.adjacentSurface.is_initialized and surface.surfaceType.downcase == "wall" surface.adjacentSurface.get.remove surface.remove else surface.setSpace(new_space) end end space.remove end return new_space end def self.make_polygon(*pts) p = OpenStudio::Point3dVector.new pts.each do |pt| p << pt end return p end def self.get_building_units(model, runner = nil) if model.getSpaces.size == 0 if !runner.nil? runner.registerError("No building geometry has been defined.") end return nil end return_units = [] model.getBuildingUnits.each do |unit| # Remove any units from list that have no associated spaces or are not residential next if not (unit.spaces.size > 0 and unit.buildingUnitType == WholeBuildingConstants.BuildingUnitTypeResidential) return_units << unit end if return_units.size == 0 # Assume SFD; create single building unit for entire model if !runner.nil? runner.registerWarning("No building units defined; assuming single-family detached building.") end unit = OpenStudio::Model::BuildingUnit.new(model) unit.setBuildingUnitType(WholeBuildingConstants.BuildingUnitTypeResidential) unit.setName(WholeBuildingConstants.ObjectNameBuildingUnit) model.getSpaces.each do |space| space.setBuildingUnit(unit) end model.getBuildingUnits.each do |unit| return_units << unit end end # Sort the building units unit_numbers = {} return_units.each do |unit| unit_number = Float(unit.name.to_s.split(" ")[-1]) unit_numbers[unit] = unit_number end unit_numbers = unit_numbers.sort_by { |k, v| v } return_units = [] unit_numbers.each do |unit, number| return_units << unit end return return_units end def self.get_unit_beds_baths(model, unit, runner = nil) # Returns a list with #beds, #baths, a list of spaces, and the unit name nbeds = unit.additionalProperties.getFeatureAsInteger(WholeBuildingConstants.BuildingUnitFeatureNumBedrooms) nbaths = unit.additionalProperties.getFeatureAsDouble(WholeBuildingConstants.BuildingUnitFeatureNumBathrooms) if not (nbeds.is_initialized or nbaths.is_initialized) if !runner.nil? runner.registerError("Could not determine number of bedrooms or bathrooms.") end return [nil, nil] else nbeds = nbeds.get.to_f nbaths = nbaths.get end return [nbeds, nbaths] end def self.get_unit_occupants(model, unit, runner = nil) noccupants = unit.additionalProperties.getFeatureAsDouble(WholeBuildingConstants.BuildingUnitFeatureNumOccupants) if not noccupants.is_initialized if !runner.nil? runner.registerError("Could not determine number of occupants.") end return nil else noccupants = noccupants.get.to_f end return noccupants end def self.get_unit_adjacent_common_spaces(unit) # Returns a list of spaces adjacent to the unit that are not assigned # to a building unit. spaces = [] unit.spaces.each do |space| space.surfaces.each do |surface| next if not surface.adjacentSurface.is_initialized adjacent_surface = surface.adjacentSurface.get next if not adjacent_surface.space.is_initialized adjacent_space = adjacent_surface.space.get next if adjacent_space.buildingUnit.is_initialized spaces << adjacent_space end end return spaces.uniq end def self.get_common_spaces(model) spaces = [] model.getSpaces.each do |space| next if space.buildingUnit.is_initialized spaces << space end return spaces end def self.get_floor_area_from_spaces(spaces, runner = nil) floor_area = 0 spaces.each do |space| floor_area += UnitConversions.convert(space.floorArea, "m^2", "ft^2") end if floor_area == 0 and not runner.nil? runner.registerError("Could not find any floor area.") return nil end return floor_area end def self.get_zone_volume(zone, runner = nil) if zone.isVolumeAutocalculated or not zone.volume.is_initialized # Calculate volume from spaces volume = 0 zone.spaces.each do |space| volume += UnitConversions.convert(space.volume, "m^3", "ft^3") end else volume = UnitConversions.convert(zone.volume.get, "m^3", "ft^3") end if volume <= 0 and not runner.nil? runner.registerError("Could not find any volume.") return nil end return volume end def self.get_finished_floor_area_from_spaces(spaces, runner = nil, apply_mult = false) floor_area = 0 spaces.each do |space| next if not self.space_is_finished(space) mult = 1.0 if apply_mult mult = space.multiplier.to_f end floor_area += UnitConversions.convert(space.floorArea * mult, "m^2", "ft^2") end if floor_area == 0 and not runner.nil? runner.registerError("Could not find any finished floor area.") return nil end return floor_area end def self.get_above_grade_finished_floor_area_from_spaces(spaces, runner = nil) floor_area = 0 spaces.each do |space| next if not (self.space_is_finished(space) and self.space_is_above_grade(space)) floor_area += UnitConversions.convert(space.floorArea, "m^2", "ft^2") end if floor_area == 0 and not runner.nil? runner.registerError("Could not find any above-grade finished floor area.") return nil end return floor_area end def self.get_above_grade_finished_volume(model, runner = nil) volume = 0 model.getThermalZones.each do |zone| next if not (self.zone_is_finished(zone) and self.zone_is_above_grade(zone)) volume += self.get_zone_volume(zone, runner) end if volume == 0 and not runner.nil? runner.registerError("Could not find any above-grade finished volume.") return nil end return volume end def self.get_window_area_from_spaces(spaces) window_area = 0 spaces.each do |space| space.surfaces.each do |surface| surface.subSurfaces.each do |subsurface| next if subsurface.subSurfaceType.downcase != "fixedwindow" window_area += UnitConversions.convert(subsurface.grossArea, "m^2", "ft^2") end end end return window_area end def self.space_height(space) return Geometry.get_height_of_spaces([space]) end # Calculates space heights as the max z coordinate minus the min z coordinate def self.get_height_of_spaces(spaces) minzs = [] maxzs = [] spaces.each do |space| zvalues = self.getSurfaceZValues(space.surfaces) minzs << zvalues.min + UnitConversions.convert(space.zOrigin, "m", "ft") maxzs << zvalues.max + UnitConversions.convert(space.zOrigin, "m", "ft") end return maxzs.max - minzs.min end # Calculates the surface height as the max z coordinate minus the min z coordinate def self.surface_height(surface) zvalues = self.getSurfaceZValues([surface]) minz = zvalues.min maxz = zvalues.max return maxz - minz end def self.zone_is_finished(zone) zone.spaces.each do |space| unless self.space_is_finished(space) return false end end end # Returns true if all spaces in zone are fully above grade def self.zone_is_above_grade(zone) spaces_are_above_grade = [] zone.spaces.each do |space| spaces_are_above_grade << self.space_is_above_grade(space) end if spaces_are_above_grade.all? return true end return false end # Returns true if all spaces in zone are either fully or partially below grade def self.zone_is_below_grade(zone) return !self.zone_is_above_grade(zone) end def self.get_finished_above_and_below_grade_zones(thermal_zones) finished_living_zones = [] finished_basement_zones = [] thermal_zones.each do |thermal_zone| next unless self.zone_is_finished(thermal_zone) if self.zone_is_above_grade(thermal_zone) finished_living_zones << thermal_zone elsif self.zone_is_below_grade(thermal_zone) finished_basement_zones << thermal_zone end end return finished_living_zones, finished_basement_zones end def self.get_thermal_zones_from_spaces(spaces) thermal_zones = [] spaces.each do |space| next unless space.thermalZone.is_initialized unless thermal_zones.include? space.thermalZone.get thermal_zones << space.thermalZone.get end end return thermal_zones end def self.get_building_type(model) building_type = nil unless model.getBuilding.standardsBuildingType.empty? building_type = model.getBuilding.standardsBuildingType.get.downcase end return building_type end def self.space_is_unfinished(space) return !self.space_is_finished(space) end def self.space_is_finished(space) unless space.isPlenum if space.spaceType.is_initialized if space.spaceType.get.standardsSpaceType.is_initialized return self.is_living_space_type(space.spaceType.get.standardsSpaceType.get) end end end return false end def self.is_living_space_type(space_type) if [WholeBuildingConstants.SpaceTypeLiving, WholeBuildingConstants.SpaceTypeFinishedBasement, WholeBuildingConstants.SpaceTypeKitchen, WholeBuildingConstants.SpaceTypeBedroom, WholeBuildingConstants.SpaceTypeBathroom, WholeBuildingConstants.SpaceTypeLaundryRoom].include? space_type return true end return false end # Returns true if space is fully above grade def self.space_is_above_grade(space) return !self.space_is_below_grade(space) end # Returns true if space is either fully or partially below grade def self.space_is_below_grade(space) space.surfaces.each do |surface| next if surface.surfaceType.downcase != "wall" if surface.outsideBoundaryCondition.downcase == "foundation" return true end end return false end def self.space_has_roof(space) space.surfaces.each do |surface| next if surface.surfaceType.downcase != "roofceiling" next if surface.outsideBoundaryCondition.downcase != "outdoors" next if surface.tilt == 0 return true end return false end def self.space_below_is_finished(space) space.surfaces.each do |surface| next if surface.surfaceType.downcase != "floor" next if not surface.adjacentSurface.is_initialized next if not surface.adjacentSurface.get.space.is_initialized adjacent_space = surface.adjacentSurface.get.space.get next if not self.space_is_finished(adjacent_space) return true end return false end def self.get_model_locations(model) locations = [] model.getSpaceTypes.each do |spaceType| next if not spaceType.standardsSpaceType.is_initialized locations << spaceType.standardsSpaceType.get end return locations end def self.get_space_from_location(unit, location, location_hierarchy) spaces = unit.spaces + self.get_unit_adjacent_common_spaces(unit) if location == WholeBuildingConstants.Auto location_hierarchy.each do |space_type| spaces.each do |space| next if not self.space_is_of_type(space, space_type) return space end end else spaces.each do |space| next if not space.spaceType.is_initialized next if not space.spaceType.get.standardsSpaceType.is_initialized next if space.spaceType.get.standardsSpaceType.get != location return space end end return nil end # Return an array of x values for surfaces passed in. The values will be relative to the parent origin. This was intended for spaces. def self.getSurfaceXValues(surfaceArray) xValueArray = [] surfaceArray.each do |surface| surface.vertices.each do |vertex| xValueArray << UnitConversions.convert(vertex.x, "m", "ft") end end return xValueArray end # Return an array of y values for surfaces passed in. The values will be relative to the parent origin. This was intended for spaces. def self.getSurfaceYValues(surfaceArray) yValueArray = [] surfaceArray.each do |surface| surface.vertices.each do |vertex| yValueArray << UnitConversions.convert(vertex.y, "m", "ft") end end return yValueArray end # Return an array of z values for surfaces passed in. The values will be relative to the parent origin. This was intended for spaces. def self.getSurfaceZValues(surfaceArray) zValueArray = [] surfaceArray.each do |surface| surface.vertices.each do |vertex| zValueArray << UnitConversions.convert(vertex.z, "m", "ft") end end return zValueArray end def self.get_space_floor_z(space) space.surfaces.each do |surface| next unless surface.surfaceType.downcase == "floor" return self.getSurfaceZValues([surface])[0] end end def self.get_z_origin_for_zone(zone) z_origins = [] zone.spaces.each do |space| z_origins << UnitConversions.convert(space.zOrigin, "m", "ft") end return z_origins.min end # Takes in a list of spaces and returns the average space height def self.spaces_avg_height(spaces) return nil if spaces.size == 0 sum_height = 0 spaces.each do |space| sum_height += self.space_height(space) end return sum_height / spaces.size end # Takes in a list of surfaces and returns the total gross area def self.calculate_total_area_from_surfaces(surfaces) total_area = 0 surfaces.each do |surface| total_area += UnitConversions.convert(surface.grossArea, "m^2", "ft^2") end return total_area end # Takes in a list of spaces and returns the total above grade wall area def self.calculate_above_grade_wall_area(spaces) wall_area = 0 spaces.each do |space| space.surfaces.each do |surface| next if surface.surfaceType.downcase != "wall" next if surface.outsideBoundaryCondition.downcase == "foundation" wall_area += UnitConversions.convert(surface.grossArea, "m^2", "ft^2") end end return wall_area end def self.calculate_above_grade_exterior_wall_area(spaces) wall_area = 0 spaces.each do |space| space.surfaces.each do |surface| next if surface.surfaceType.downcase != "wall" next if surface.outsideBoundaryCondition.downcase != "outdoors" next if surface.outsideBoundaryCondition.downcase == "foundation" next unless self.space_is_finished(surface.space.get) wall_area += UnitConversions.convert(surface.grossArea, "m^2", "ft^2") end end return wall_area end def self.get_roof_pitch(surfaces) tilts = [] surfaces.each do |surface| next if surface.surfaceType.downcase != "roofceiling" next if surface.outsideBoundaryCondition.downcase != "outdoors" and surface.outsideBoundaryCondition.downcase != "adiabatic" tilts << surface.tilt end return UnitConversions.convert(tilts.max, "rad", "deg") end # Checks if the surface is between finished space and outside def self.is_exterior_surface(surface) if surface.outsideBoundaryCondition.downcase != "outdoors" or not surface.space.is_initialized return false end if not self.space_is_finished(surface.space.get) return false end return true end # Checks if the surface is between finished and unfinished space def self.is_interzonal_surface(surface) if surface.outsideBoundaryCondition.downcase != "surface" or not surface.space.is_initialized or not surface.adjacentSurface.is_initialized return false end adjacent_surface = surface.adjacentSurface.get if not adjacent_surface.space.is_initialized return false end if self.space_is_finished(surface.space.get) == self.space_is_finished(adjacent_surface.space.get) return false end return true end def self.is_pier_beam_surface(surface) if not surface.space.is_initialized return false end if not Geometry.is_pier_beam(surface.space.get) return false end return true end # Takes in a list of floor surfaces for which to calculate the exposed perimeter. # Returns the total exposed perimeter. # NOTE: Does not work for buildings with non-orthogonal walls. def self.calculate_exposed_perimeter(model, ground_floor_surfaces, has_foundation_walls = false) perimeter = 0 # Get ground edges if not has_foundation_walls # Use edges from floor surface ground_edges = self.get_edges_for_surfaces(ground_floor_surfaces, false) else # Use top edges from foundation walls instead surfaces = [] ground_floor_surfaces.each do |ground_floor_surface| next if not ground_floor_surface.space.is_initialized foundation_space = ground_floor_surface.space.get wall_surfaces = [] foundation_space.surfaces.each do |surface| next if not surface.surfaceType.downcase == "wall" next if surface.adjacentSurface.is_initialized wall_surfaces << surface end self.get_walls_connected_to_floor(wall_surfaces, ground_floor_surface).each do |surface| next if surfaces.include? surface surfaces << surface end end ground_edges = self.get_edges_for_surfaces(surfaces, true) end # Get bottom edges of exterior walls (building footprint) surfaces = [] model.getSurfaces.each do |surface| next if not surface.surfaceType.downcase == "wall" next if surface.outsideBoundaryCondition.downcase != "outdoors" surfaces << surface end model_edges = self.get_edges_for_surfaces(surfaces, false) # compare edges for overlap ground_edges.each do |e1| model_edges.each do |e2| next if not self.is_point_between(e2[0], e1[0], e1[1]) next if not self.is_point_between(e2[1], e1[0], e1[1]) point_one = OpenStudio::Point3d.new(e2[0][0], e2[0][1], e2[0][2]) point_two = OpenStudio::Point3d.new(e2[1][0], e2[1][1], e2[1][2]) length = OpenStudio::Vector3d.new(point_one - point_two).length perimeter += length end end return UnitConversions.convert(perimeter, "m", "ft") end def self.is_point_between(p, v1, v2) # Checks if point p is between points v1 and v2 is_between = false tol = 0.001 if (p[2] - v1[2]).abs <= tol and (p[2] - v2[2]).abs <= tol # equal z if (p[0] - v1[0]).abs <= tol and (p[0] - v2[0]).abs <= tol # equal x; vertical if p[1] >= v1[1] - tol and p[1] <= v2[1] + tol is_between = true elsif p[1] <= v1[1] + tol and p[1] >= v2[1] - tol is_between = true end elsif (p[1] - v1[1]).abs <= tol and (p[1] - v2[1]).abs <= tol # equal y; horizontal if p[0] >= v1[0] - tol and p[0] <= v2[0] + tol is_between = true elsif p[0] <= v1[0] + tol and p[0] >= v2[0] - tol is_between = true end end end return is_between end def self.get_edges_for_surfaces(surfaces, use_top_edge) top_z = -99999 bottom_z = 99999 surfaces.each do |surface| top_z = [self.getSurfaceZValues([surface]).max, top_z].max bottom_z = [self.getSurfaceZValues([surface]).min, bottom_z].min end edges = [] edge_counter = 0 surfaces.each do |surface| if use_top_edge matchz = top_z else matchz = bottom_z end # get vertices vertex_hash = {} vertex_counter = 0 surface.vertices.each do |vertex| next if (UnitConversions.convert(vertex.z, "m", "ft") - matchz).abs > 0.0001 # ensure we only process bottom/top edge of wall surfaces vertex_counter += 1 vertex_hash[vertex_counter] = [vertex.x + surface.space.get.xOrigin, vertex.y + surface.space.get.yOrigin, vertex.z + surface.space.get.zOrigin] end # make edges counter = 0 vertex_hash.each do |k, v| edge_counter += 1 counter += 1 if vertex_hash.size != counter edges << [v, vertex_hash[counter + 1], self.get_facade_for_surface(surface)] elsif vertex_hash.size > 2 # different code for wrap around vertex (if > 2 vertices) edges << [v, vertex_hash[1], self.get_facade_for_surface(surface)] end end end return edges end def self.equal_vertices(v1, v2) tol = 0.001 return false if (v1[0] - v2[0]).abs > tol return false if (v1[1] - v2[1]).abs > tol return false if (v1[2] - v2[2]).abs > tol return true end def self.get_walls_connected_to_floor(wall_surfaces, floor_surface, same_space = true) adjacent_wall_surfaces = [] wall_surfaces.each do |wall_surface| if same_space next if wall_surface.space.get != floor_surface.space.get else next if wall_surface.space.get == floor_surface.space.get end wall_vertices = wall_surface.vertices wall_vertices.each_with_index do |wv1, widx| wv2 = wall_vertices[widx - 1] floor_vertices = floor_surface.vertices floor_vertices.each_with_index do |fv1, fidx| fv2 = floor_vertices[fidx - 1] # Wall within floor edge? if self.is_point_between([wv1.x, wv1.y, wv1.z], [fv1.x, fv1.y, fv1.z], [fv2.x, fv2.y, fv2.z]) and self.is_point_between([wv2.x, wv2.y, wv2.z], [fv1.x, fv1.y, fv1.z], [fv2.x, fv2.y, fv2.z]) if not adjacent_wall_surfaces.include? wall_surface adjacent_wall_surfaces << wall_surface end end end end end return adjacent_wall_surfaces end def self.is_living(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeLiving) end def self.is_pier_beam(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypePierBeam) end def self.is_crawl(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeCrawl) end def self.is_finished_basement(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeFinishedBasement) end def self.is_unfinished_basement(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeUnfinishedBasement) end def self.is_unfinished_attic(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeUnfinishedAttic) end def self.is_garage(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeGarage) end def self.is_corridor(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeCorridor) end def self.is_bedroom(space_or_zone) return self.space_or_zone_is_of_type(space_or_zone, WholeBuildingConstants.SpaceTypeBedroom) end def self.space_or_zone_is_of_type(space_or_zone, space_type) if space_or_zone.is_a? OpenStudio::Model::Space return self.space_is_of_type(space_or_zone, space_type) elsif space_or_zone.is_a? OpenStudio::Model::ThermalZone return self.zone_is_of_type(space_or_zone, space_type) end end def self.space_is_of_type(space, space_type) unless space.isPlenum if space.spaceType.is_initialized if space.spaceType.get.standardsSpaceType.is_initialized return true if space.spaceType.get.standardsSpaceType.get == space_type end end end return false end def self.zone_is_of_type(zone, space_type) zone.spaces.each do |space| return self.space_is_of_type(space, space_type) end end def self.is_basement(space_or_zone) if space_or_zone.is_a? OpenStudio::Model::Space return self.space_is_below_grade(space_or_zone) elsif space_or_zone.is_a? OpenStudio::Model::ThermalZone return self.zone_is_below_grade(space_or_zone) end end def self.is_attic(space_or_zone) if space_or_zone.is_a? OpenStudio::Model::Space space_or_zone.surfaces.each do |surface| next unless surface.surfaceType.downcase.to_s == "roofceiling" unless surface.outsideBoundaryCondition.downcase.to_s == "outdoors" return false end end space_or_zone.surfaces.each do |surface| next unless surface.surfaceType.downcase.to_s == "floor" surface.vertices.each do |vertex| unless vertex.z + space_or_zone.zOrigin > 0 # not an attic if it isn't above grade return false end end end elsif space_or_zone.is_a? OpenStudio::Model::ThermalZone space_or_zone.spaces.each do |space| space.surfaces.each do |surface| next unless surface.surfaceType.downcase.to_s == "roofceiling" if not surface.outsideBoundaryCondition.downcase.to_s == "outdoors" return false end end space.surfaces.each do |surface| next unless surface.surfaceType.downcase.to_s == "floor" surface.vertices.each do |vertex| unless vertex.z + space.zOrigin > 0 # not an attic if it isn't above grade return false end end end end end end def self.is_foundation(space_or_zone) return true if self.is_pier_beam(space_or_zone) or self.is_crawl(space_or_zone) or self.is_finished_basement(space_or_zone) or self.is_unfinished_basement(space_or_zone) end def self.get_crawl_spaces(spaces) crawl_spaces = [] spaces.each do |space| next if not self.is_crawl(space) crawl_spaces << space end return crawl_spaces end def self.get_pier_beam_spaces(spaces) pb_spaces = [] spaces.each do |space| next if not self.is_pier_beam(space) pb_spaces << space end return pb_spaces end def self.get_finished_spaces(spaces) finished_spaces = [] spaces.each do |space| next if self.space_is_unfinished(space) finished_spaces << space end return finished_spaces end def self.get_bedroom_spaces(spaces) bedroom_spaces = [] spaces.each do |space| next if not self.is_bedroom(space) end return bedroom_spaces end def self.get_finished_basement_spaces(spaces) finished_basement_spaces = [] spaces.each do |space| next if not self.is_finished_basement(space) finished_basement_spaces << space end return finished_basement_spaces end def self.get_unfinished_basement_spaces(spaces) unfinished_basement_spaces = [] spaces.each do |space| next if not self.is_unfinished_basement(space) unfinished_basement_spaces << space end return unfinished_basement_spaces end def self.get_unfinished_attic_spaces(spaces) unfinished_attic_spaces = [] spaces.each do |space| next if not self.is_unfinished_attic(space) unfinished_attic_spaces << space end return unfinished_attic_spaces end def self.get_garage_spaces(spaces) garage_spaces = [] spaces.each do |space| next if not self.is_garage(space) garage_spaces << space end return garage_spaces end def self.get_facade_for_surface(surface) tol = 0.001 n = surface.outwardNormal facade = nil if (n.z).abs < tol if (n.x).abs < tol and (n.y + 1).abs < tol facade = WholeBuildingConstants.FacadeFront elsif (n.x - 1).abs < tol and (n.y).abs < tol facade = WholeBuildingConstants.FacadeRight elsif (n.x).abs < tol and (n.y - 1).abs < tol facade = WholeBuildingConstants.FacadeBack elsif (n.x + 1).abs < tol and (n.y).abs < tol facade = WholeBuildingConstants.FacadeLeft end else if (n.x).abs < tol and n.y < 0 facade = WholeBuildingConstants.FacadeFront elsif n.x > 0 and (n.y).abs < tol facade = WholeBuildingConstants.FacadeRight elsif (n.x).abs < tol and n.y > 0 facade = WholeBuildingConstants.FacadeBack elsif n.x < 0 and (n.y).abs < tol facade = WholeBuildingConstants.FacadeLeft end end return facade end def self.get_surface_length(surface) xvalues = self.getSurfaceXValues([surface]) yvalues = self.getSurfaceYValues([surface]) xrange = xvalues.max - xvalues.min yrange = yvalues.max - yvalues.min if xrange > yrange return xrange end return yrange end def self.get_surface_height(surface) zvalues = self.getSurfaceZValues([surface]) zrange = zvalues.max - zvalues.min return zrange end def self.is_gable_wall(surface) if (surface.surfaceType.downcase != "wall" or surface.outsideBoundaryCondition.downcase != "outdoors") return false end if surface.vertices.size != 3 return false end if not surface.space.is_initialized return false end space = surface.space.get if not self.space_has_roof(space) return false end return true end def self.is_rectangular_wall(surface) if (surface.surfaceType.downcase != "wall" or surface.outsideBoundaryCondition.downcase != "outdoors") return false end if surface.vertices.size != 4 return false end xvalues = self.getSurfaceXValues([surface]) yvalues = self.getSurfaceYValues([surface]) zvalues = self.getSurfaceZValues([surface]) if not ((xvalues.uniq.size == 1 and yvalues.uniq.size == 2) or (xvalues.uniq.size == 2 and yvalues.uniq.size == 1)) return false end if not zvalues.uniq.size == 2 return false end return true end def self.get_closest_neighbor_distance(model) house_points = [] neighbor_points = [] model.getSurfaces.each do |surface| next unless surface.surfaceType.downcase == "wall" surface.vertices.each do |vertex| house_points << OpenStudio::Point3d.new(vertex) end end model.getShadingSurfaces.each do |shading_surface| next unless shading_surface.name.to_s.downcase.include? "neighbor" shading_surface.vertices.each do |vertex| neighbor_points << OpenStudio::Point3d.new(vertex) end end neighbor_offsets = [] house_points.each do |house_point| neighbor_points.each do |neighbor_point| neighbor_offsets << OpenStudio::getDistance(house_point, neighbor_point) end end if neighbor_offsets.empty? return 0 end return UnitConversions.convert(neighbor_offsets.min, "m", "ft") end def self.get_spaces_above_grade_exterior_walls(spaces) above_grade_exterior_walls = [] spaces.each do |space| next if not Geometry.space_is_finished(space) next if not Geometry.space_is_above_grade(space) space.surfaces.each do |surface| next if above_grade_exterior_walls.include?(surface) next if surface.surfaceType.downcase != "wall" next if surface.outsideBoundaryCondition.downcase != "outdoors" above_grade_exterior_walls << surface end end return above_grade_exterior_walls end def self.get_spaces_above_grade_exterior_floors(spaces) above_grade_exterior_floors = [] spaces.each do |space| next if not Geometry.space_is_finished(space) next if not Geometry.space_is_above_grade(space) space.surfaces.each do |surface| next if above_grade_exterior_floors.include?(surface) next if surface.surfaceType.downcase != "floor" next if surface.outsideBoundaryCondition.downcase != "outdoors" above_grade_exterior_floors << surface end end return above_grade_exterior_floors end def self.get_spaces_above_grade_ground_floors(spaces) above_grade_ground_floors = [] spaces.each do |space| next if not Geometry.space_is_finished(space) next if not Geometry.space_is_above_grade(space) space.surfaces.each do |surface| next if above_grade_ground_floors.include?(surface) next if surface.surfaceType.downcase != "floor" next if surface.outsideBoundaryCondition.downcase != "foundation" above_grade_ground_floors << surface end end return above_grade_ground_floors end def self.get_spaces_above_grade_exterior_roofs(spaces) above_grade_exterior_roofs = [] spaces.each do |space| next if not Geometry.space_is_finished(space) next if not Geometry.space_is_above_grade(space) space.surfaces.each do |surface| next if above_grade_exterior_roofs.include?(surface) next if surface.surfaceType.downcase != "roofceiling" next if surface.outsideBoundaryCondition.downcase != "outdoors" above_grade_exterior_roofs << surface end end return above_grade_exterior_roofs end def self.get_spaces_interzonal_walls(spaces) interzonal_walls = [] spaces.each do |space| space.surfaces.each do |surface| next if interzonal_walls.include?(surface) next if surface.surfaceType.downcase != "wall" next if not self.is_interzonal_surface(surface) interzonal_walls << surface end end return interzonal_walls end def self.get_spaces_interzonal_floors_and_ceilings(spaces) interzonal_floors = [] spaces.each do |space| space.surfaces.each do |surface| next if interzonal_floors.include?(surface) next if surface.surfaceType.downcase != "floor" and surface.surfaceType.downcase != "roofceiling" next if not self.is_interzonal_surface(surface) interzonal_floors << surface end end return interzonal_floors end def self.get_spaces_below_grade_exterior_walls(spaces) below_grade_exterior_walls = [] spaces.each do |space| next if not Geometry.space_is_finished(space) next if not Geometry.space_is_below_grade(space) space.surfaces.each do |surface| next if below_grade_exterior_walls.include?(surface) next if surface.surfaceType.downcase != "wall" next if surface.outsideBoundaryCondition.downcase != "foundation" below_grade_exterior_walls << surface end end return below_grade_exterior_walls end def self.get_spaces_below_grade_exterior_floors(spaces) below_grade_exterior_floors = [] spaces.each do |space| next if not Geometry.space_is_finished(space) next if not Geometry.space_is_below_grade(space) space.surfaces.each do |surface| next if below_grade_exterior_floors.include?(surface) next if surface.surfaceType.downcase != "floor" next if surface.outsideBoundaryCondition.downcase != "foundation" below_grade_exterior_floors << surface end end return below_grade_exterior_floors end def self.process_overhangs(model, runner, depth, offset, facade_bools_hash) # Error checking if depth < 0 runner.registerError("Overhang depth must be greater than or equal to 0.") return false end if offset < 0 runner.registerError("Overhang offset must be greater than or equal to 0.") return false end # if width_extension < 0 # runner.registerError("Overhang width extension must be greater than or equal to 0.") # return false # end sub_surfaces = self.get_window_sub_surfaces(model) # Remove existing overhangs num_removed = 0 model.getShadingSurfaceGroups.each do |shading_surface_group| remove_group = false shading_surface_group.shadingSurfaces.each do |shading_surface| next unless shading_surface.name.to_s.downcase.include? WholeBuildingConstants.ObjectNameOverhangs num_removed += 1 remove_group = true end if remove_group shading_surface_group.remove end end if num_removed > 0 runner.registerInfo("Removed #{num_removed} #{WholeBuildingConstants.ObjectNameOverhangs}.") end # No overhangs to add? Exit here. if depth == 0 runner.registerInfo("No #{WholeBuildingConstants.ObjectNameOverhangs} to be added.") return true end num_added = 0 sub_surfaces.each do |sub_surface| facade = self.get_facade_for_surface(sub_surface) next if facade.nil? next if !facade_bools_hash["#{facade} Facade"] overhang = sub_surface.addOverhang(depth, offset) overhang.get.setName("#{sub_surface.name} - #{WholeBuildingConstants.ObjectNameOverhangs}") num_added += 1 sub_surface.additionalProperties.setFeature(WholeBuildingConstants.SizingInfoWindowOverhangDepth, depth) sub_surface.additionalProperties.setFeature(WholeBuildingConstants.SizingInfoWindowOverhangOffset, offset) end unless num_added > 0 runner.registerInfo("No windows found for adding #{WholeBuildingConstants.ObjectNameOverhangs}.") return true end runner.registerInfo("Added #{num_added} #{WholeBuildingConstants.ObjectNameOverhangs}.") return true end def self.get_window_sub_surfaces(model) sub_surfaces = [] model.getSubSurfaces.each do |sub_surface| next unless sub_surface.subSurfaceType.downcase.include? "window" next if (90 - sub_surface.tilt * 180 / Math::PI).abs > 0.01 # not a vertical subsurface sub_surfaces << sub_surface end return sub_surfaces end def self.process_beds_and_baths(model, runner, num_br, num_ba) # Error checking if not num_br.all? { |x| MathTools.valid_float?(x) } runner.registerError("Number of bedrooms must be a numerical value.") return false else num_br = num_br.map(&:to_f) end if not num_ba.all? { |x| MathTools.valid_float?(x) } runner.registerError("Number of bathrooms must be a numerical value.") return false else num_ba = num_ba.map(&:to_f) end if num_br.any? { |x| x < 0 or x % 1 != 0 } runner.registerError("Number of bedrooms must be a non-negative integer.") return false end if num_ba.any? { |x| x <= 0 or x % 0.25 != 0 } runner.registerError("Number of bathrooms must be a positive multiple of 0.25.") return false end if num_br.length > 1 and num_ba.length > 1 and num_br.length != num_ba.length runner.registerError("Number of bedroom elements specified inconsistent with number of bathroom elements specified.") return false end # Get building units units = self.get_building_units(model, runner) if units.nil? return false end # error checking if num_br.length > 1 and num_br.length != units.size runner.registerError("Number of bedroom elements specified inconsistent with number of multifamily units defined in the model.") return false end if num_ba.length > 1 and num_ba.length != units.size runner.registerError("Number of bathroom elements specified inconsistent with number of multifamily units defined in the model.") return false end if units.size > 1 if num_br.length == 1 num_br = Array.new(units.size, num_br[0]) end if num_ba.length == 1 num_ba = Array.new(units.size, num_ba[0]) end end # Update number of bedrooms/bathrooms total_num_br = 0 total_num_ba = 0 units.each_with_index do |unit, unit_index| num_br[unit_index] = num_br[unit_index].to_i num_ba[unit_index] = num_ba[unit_index].to_f unit.additionalProperties.setFeature(WholeBuildingConstants.BuildingUnitFeatureNumBedrooms, num_br[unit_index]) unit.additionalProperties.setFeature(WholeBuildingConstants.BuildingUnitFeatureNumBathrooms, num_ba[unit_index]) if units.size > 1 runner.registerInfo("Unit '#{unit_index}' has been assigned #{num_br[unit_index].to_s} bedroom(s) and #{num_ba[unit_index].round(2).to_s} bathroom(s).") end total_num_br += num_br[unit_index] total_num_ba += num_ba[unit_index] end runner.registerInfo("The building has been assigned #{total_num_br.to_s} bedroom(s) and #{total_num_ba.round(2).to_s} bathroom(s) across #{units.size} unit(s).") return true end def self.process_occupants(model, runner, num_occ, occ_gain, sens_frac, lat_frac, schedules_file) num_occ = num_occ.split(",").map(&:strip) # Error checking if occ_gain < 0 runner.registerError("Internal gains cannot be negative.") return false end if sens_frac < 0 or sens_frac > 1 runner.registerError("Sensible fraction must be greater than or equal to 0 and less than or equal to 1.") return false end if lat_frac < 0 or lat_frac > 1 runner.registerError("Latent fraction must be greater than or equal to 0 and less than or equal to 1.") return false end if lat_frac + sens_frac > 1 runner.registerError("Sum of sensible and latent fractions must be less than or equal to 1.") return false end # Get building units units = self.get_building_units(model, runner) if units.nil? return false end # Error checking if num_occ.length > 1 and num_occ.length != units.size runner.registerError("Number of occupant elements specified inconsistent with number of multifamily units defined in the model.") return false end if units.size > 1 and num_occ.length == 1 num_occ = Array.new(units.size, num_occ[0]) end activity_per_person = UnitConversions.convert(occ_gain, "Btu/hr", "W") # Hard-coded convective, radiative, latent, and lost fractions occ_lat = lat_frac occ_sens = sens_frac occ_conv = 0.442 * occ_sens occ_rad = 0.558 * occ_sens occ_lost = 1 - occ_lat - occ_conv - occ_rad # Update number of occupants total_num_occ = 0 people_sch = nil activity_sch = nil units.each_with_index do |unit, unit_index| unit_occ = num_occ[unit_index] if unit_occ != WholeBuildingConstants.Auto if not MathTools.valid_float?(unit_occ) or unit_occ.to_f < 0 runner.registerError("Number of Occupants must be either '#{WholeBuildingConstants.Auto}' or a number greater than or equal to 0.") return false end end # Get number of beds nbeds, nbaths = self.get_unit_beds_baths(model, unit, runner) if nbeds.nil? return false end # Calculate number of occupants for this unit if unit_occ == WholeBuildingConstants.Auto if [WholeBuildingConstants.BuildingTypeMultifamily, WholeBuildingConstants.BuildingTypeSingleFamilyAttached].include? get_building_type(model) # multifamily equation unit_occ = 0.63 + 0.92 * nbeds # nbeds = -0.68 + 1.09 * unit_occ elsif [WholeBuildingConstants.BuildingTypeSingleFamilyDetached].include? get_building_type(model) # single-family equation unit_occ = 0.87 + 0.59 * nbeds # nbeds = -1.47 + 1.69 * unit_occ end else unit_occ = unit_occ.to_f end unit.additionalProperties.setFeature(WholeBuildingConstants.BuildingUnitFeatureNumOccupants, unit_occ) # Get spaces bedroom_ffa_spaces = self.get_bedroom_spaces(unit.spaces) non_bedroom_ffa_spaces = self.get_finished_spaces(unit.spaces) - bedroom_ffa_spaces # Get FFA non_bedroom_ffa = self.get_finished_floor_area_from_spaces(non_bedroom_ffa_spaces, runner) bedroom_ffa = self.get_finished_floor_area_from_spaces(bedroom_ffa_spaces) bedroom_ffa = 0 if bedroom_ffa.nil? ffa = non_bedroom_ffa + bedroom_ffa # Design day schedules used when autosizing winter_design_day_sch = OpenStudio::Model::ScheduleDay.new(model) winter_design_day_sch.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0) summer_design_day_sch = OpenStudio::Model::ScheduleDay.new(model) summer_design_day_sch.addValue(OpenStudio::Time.new(0, 24, 0, 0), 1) # Assign occupants to each space of the unit spaces = non_bedroom_ffa_spaces if not bedroom_ffa_spaces.empty? spaces = bedroom_ffa_spaces end spaces.each do |space| space_obj_name = "#{WholeBuildingConstants.ObjectNameOccupants(unit.name.to_s)}|#{space.name.to_s}" # Remove any existing people objects_to_remove = [] space.people.each do |people| objects_to_remove << people objects_to_remove << people.peopleDefinition if people.numberofPeopleSchedule.is_initialized objects_to_remove << people.numberofPeopleSchedule.get end if people.activityLevelSchedule.is_initialized objects_to_remove << people.activityLevelSchedule.get end end if objects_to_remove.size > 0 runner.registerInfo("Removed existing people from space '#{space.name.to_s}'.") end objects_to_remove.uniq.each do |object| begin object.remove rescue # no op end end space_num_occ = unit_occ * UnitConversions.convert(space.floorArea, "m^2", "ft^2") / ffa if space_num_occ > 0 if people_sch.nil? people_sch = schedules_file.create_schedule_file(col_name: "occupants") end if activity_sch.nil? # Create schedule activity_sch = OpenStudio::Model::ScheduleRuleset.new(model, activity_per_person) end # Add people definition for the occ occ_def = OpenStudio::Model::PeopleDefinition.new(model) occ = OpenStudio::Model::People.new(occ_def) occ.setName(space_obj_name) occ.setSpace(space) occ_def.setName(space_obj_name) occ_def.setNumberOfPeopleCalculationMethod("People", 1) occ_def.setNumberofPeople(space_num_occ) occ_def.setFractionRadiant(occ_rad) occ_def.setSensibleHeatFraction(occ_sens) occ_def.setMeanRadiantTemperatureCalculationType("ZoneAveraged") occ_def.setCarbonDioxideGenerationRate(0) occ_def.setEnableASHRAE55ComfortWarnings(false) occ.setActivityLevelSchedule(activity_sch) occ.setNumberofPeopleSchedule(people_sch) total_num_occ += space_num_occ runner.registerInfo("#{unit.name.to_s} has been assigned #{space_num_occ.round(2)} occupant(s) for space '#{space.name}'.") end end end schedules_file.set_vacancy(col_name: "occupants") runner.registerInfo("The building has been assigned #{total_num_occ.round(2)} occupant(s) across #{units.size} unit(s).") return true end def self.get_occupancy_default_num(nbeds) return Float(nbeds) end def self.get_occupancy_default_values() # Table 4.2.2(3). Internal Gains for Reference Homes hrs_per_day = 16.5 # hrs/day sens_gains = 3716.0 # Btu/person/day lat_gains = 2884.0 # Btu/person/day tot_gains = sens_gains + lat_gains heat_gain = tot_gains / hrs_per_day # Btu/person/hr sens = sens_gains / tot_gains lat = lat_gains / tot_gains return heat_gain, hrs_per_day, sens, lat end def self.process_eaves(model, runner, eaves_depth, roof_structure) # Error checking if eaves_depth < 0 runner.registerError("Eaves depth must be greater than or equal to 0.") return false end # Remove existing eaves num_removed = 0 existing_eaves_depth = nil model.getShadingSurfaceGroups.each do |shading_surface_group| next unless shading_surface_group.name.to_s == WholeBuildingConstants.ObjectNameEaves shading_surface_group.shadingSurfaces.each do |shading_surface| num_removed += 1 next unless existing_eaves_depth.nil? existing_eaves_depth = self.get_existing_eaves_depth(shading_surface) end shading_surface_group.remove end if num_removed > 0 runner.registerInfo("#{num_removed} #{WholeBuildingConstants.ObjectNameEaves} removed.") end # No eaves to add? Exit here. if eaves_depth == 0 and runner.registerInfo("No #{WholeBuildingConstants.ObjectNameEaves} were added.") return true end if existing_eaves_depth.nil? existing_eaves_depth = 0 end surfaces_modified = false shading_surface_group = OpenStudio::Model::ShadingSurfaceGroup.new(model) shading_surface_group.setName(WholeBuildingConstants.ObjectNameEaves) model.getSurfaces.each do |roof_surface| next unless roof_surface.surfaceType.downcase == "roofceiling" next unless roof_surface.outsideBoundaryCondition.downcase == "outdoors" if roof_structure == WholeBuildingConstants.RoofStructureTrussCantilever l, w, h = self.get_surface_dimensions(roof_surface) lift = (h / [l, w].min) * eaves_depth m = self.initialize_transformation_matrix(OpenStudio::Matrix.new(4, 4, 0)) m[2, 3] = lift transformation = OpenStudio::Transformation.new(m) new_vertices = transformation * roof_surface.vertices roof_surface.setVertices(new_vertices) end surfaces_modified = true if roof_surface.vertices.length > 3 vertex_dir_backup = roof_surface.vertices[-3] vertex_dir = roof_surface.vertices[-2] vertex_1 = roof_surface.vertices[-1] roof_surface.vertices[0..-1].each do |vertex| vertex_2 = vertex dir_vector = OpenStudio::Vector3d.new(vertex_1.x - vertex_dir.x, vertex_1.y - vertex_dir.y, vertex_1.z - vertex_dir.z) # works if angles are right angles if (dir_vector.dot(OpenStudio::Vector3d.new(vertex_1.x - vertex_2.x, vertex_1.y - vertex_2.y, vertex_1.z - vertex_2.z))).abs > 0.0001 # ensure perpendicular dir_vector = OpenStudio::Vector3d.new(0, vertex_1.y - vertex_dir.y, vertex_1.z - vertex_dir.z) end if (dir_vector.dot(OpenStudio::Vector3d.new(vertex_1.x - vertex_2.x, vertex_1.y - vertex_2.y, vertex_1.z - vertex_2.z))).abs > 0.0001 # ensure perpendicular dir_vector = OpenStudio::Vector3d.new(vertex_1.x - vertex_dir.x, 0, vertex_1.z - vertex_dir.z) end if (dir_vector.dot(OpenStudio::Vector3d.new(vertex_1.x - vertex_2.x, vertex_1.y - vertex_2.y, vertex_1.z - vertex_2.z))).abs > 0.0001 # ensure perpendicular dir_vector = OpenStudio::Vector3d.new(0, vertex_1.y - vertex_dir.y, vertex_1.z - vertex_dir.z) end if (dir_vector.dot(OpenStudio::Vector3d.new(vertex_1.x - vertex_2.x, vertex_1.y - vertex_2.y, vertex_1.z - vertex_2.z))).abs > 0.0001 # ensure perpendicular dir_vector = OpenStudio::Vector3d.new(vertex_1.x - vertex_dir_backup.x, vertex_1.y - vertex_dir_backup.y, vertex_1.z - vertex_dir_backup.z) end dir_vector_n = OpenStudio::Vector3d.new(dir_vector.x / dir_vector.length, dir_vector.y / dir_vector.length, dir_vector.z / dir_vector.length) # normalize l, w, h = self.get_surface_dimensions(roof_surface) tilt = Math.atan(h / [l, w].min) z = eaves_depth / Math.cos(tilt) if dir_vector_n.z == 0 scale = 1 else scale = z / eaves_depth end m = self.initialize_transformation_matrix(OpenStudio::Matrix.new(4, 4, 0)) m[0, 3] = dir_vector_n.x * eaves_depth * scale m[1, 3] = dir_vector_n.y * eaves_depth * scale m[2, 3] = dir_vector_n.z * eaves_depth * scale new_vertices = OpenStudio::Point3dVector.new new_vertices << OpenStudio::Transformation.new(m) * vertex_1 new_vertices << OpenStudio::Transformation.new(m) * vertex_2 new_vertices << vertex_2 new_vertices << vertex_1 vertex_dir_backup = vertex_dir vertex_dir = vertex_1 vertex_1 = vertex_2 next if dir_vector.length == 0 next if dir_vector_n.z > 0 if OpenStudio::getOutwardNormal(new_vertices).get.z < 0 transformation = OpenStudio::Transformation.rotation(new_vertices[2], OpenStudio::Vector3d.new(new_vertices[2].x - new_vertices[3].x, new_vertices[2].y - new_vertices[3].y, new_vertices[2].z - new_vertices[3].z), 3.14159) new_vertices = transformation * new_vertices end m = self.initialize_transformation_matrix(OpenStudio::Matrix.new(4, 4, 0)) m[2, 3] = roof_surface.space.get.zOrigin new_vertices = OpenStudio::Transformation.new(m) * new_vertices shading_surface = OpenStudio::Model::ShadingSurface.new(new_vertices, model) shading_surface.setName("#{roof_surface.name} - #{WholeBuildingConstants.ObjectNameEaves}") shading_surface.setShadingSurfaceGroup(shading_surface_group) end elsif roof_surface.vertices.length == 3 zmin = 9e99 roof_surface.vertices.each do |vertex| zmin = [vertex.z, zmin].min end vertex_1 = nil vertex_2 = nil vertex_dir = nil roof_surface.vertices.each do |vertex| if vertex.z == zmin if vertex_1.nil? vertex_1 = vertex end end if vertex.z == zmin vertex_2 = vertex end if vertex.z != zmin vertex_dir = vertex end end l, w, h = self.get_surface_dimensions(roof_surface) tilt = Math.atan(h / [l, w].min) z = eaves_depth / Math.cos(tilt) scale = z / eaves_depth dir_vector = OpenStudio::Vector3d.new(vertex_1.x - vertex_dir.x, vertex_1.y - vertex_dir.y, vertex_1.z - vertex_dir.z) if dir_vector.dot(OpenStudio::Vector3d.new(vertex_1.x - vertex_2.x, vertex_1.y - vertex_2.y, vertex_1.z - vertex_2.z)) != 0 # ensure perpendicular dir_vector = OpenStudio::Vector3d.new(vertex_1.x - vertex_dir.x, 0, vertex_1.z - vertex_dir.z) end if dir_vector.dot(OpenStudio::Vector3d.new(vertex_1.x - vertex_2.x, vertex_1.y - vertex_2.y, vertex_1.z - vertex_2.z)) != 0 # ensure perpendicular dir_vector = OpenStudio::Vector3d.new(0, vertex_1.y - vertex_dir.y, vertex_1.z - vertex_dir.z) end dir_vector_n = OpenStudio::Vector3d.new(dir_vector.x / dir_vector.length, dir_vector.y / dir_vector.length, dir_vector.z / dir_vector.length) # normalize m = self.initialize_transformation_matrix(OpenStudio::Matrix.new(4, 4, 0)) m[0, 3] = dir_vector_n.x * eaves_depth * scale m[1, 3] = dir_vector_n.y * eaves_depth * scale m[2, 3] = dir_vector_n.z * eaves_depth * scale new_vertices = OpenStudio::Point3dVector.new new_vertices << OpenStudio::Transformation.new(m) * vertex_1 new_vertices << OpenStudio::Transformation.new(m) * vertex_2 new_vertices << vertex_2 new_vertices << vertex_1 next if dir_vector.length == 0 next if dir_vector_n.z > 0 if OpenStudio::getOutwardNormal(new_vertices).get.z < 0 transformation = OpenStudio::Transformation.rotation(new_vertices[2], OpenStudio::Vector3d.new(new_vertices[2].x - new_vertices[3].x, new_vertices[2].y - new_vertices[3].y, new_vertices[2].z - new_vertices[3].z), 3.14159) new_vertices = transformation * new_vertices end m = self.initialize_transformation_matrix(OpenStudio::Matrix.new(4, 4, 0)) m[2, 3] = roof_surface.space.get.zOrigin new_vertices = OpenStudio::Transformation.new(m) * new_vertices shading_surface = OpenStudio::Model::ShadingSurface.new(new_vertices, model) shading_surface.setName("#{roof_surface.name} - #{WholeBuildingConstants.ObjectNameEaves}") shading_surface.setShadingSurfaceGroup(shading_surface_group) end end # Remove eaves overlapping roofceiling shading_surfaces_to_remove = [] model.getShadingSurfaces.each do |shading_surface| next unless shading_surface.name.to_s.include? WholeBuildingConstants.ObjectNameEaves new_shading_vertices = [] shading_surface.vertices.reverse.each do |vertex| new_shading_vertices << OpenStudio::Point3d.new(vertex.x, vertex.y, 0) end model.getSurfaces.each do |roof_surface| next unless roof_surface.surfaceType.downcase == "roofceiling" next unless roof_surface.outsideBoundaryCondition.downcase == "outdoors" or roof_surface.outsideBoundaryCondition.downcase == "adiabatic" roof_surface_vertices = [] roof_surface.vertices.reverse.each do |vertex| roof_surface_vertices << OpenStudio::Point3d.new(vertex.x, vertex.y, 0) end polygon = OpenStudio::subtract(roof_surface_vertices, [new_shading_vertices], 0.001)[0] if OpenStudio::getArea(roof_surface_vertices).get - OpenStudio::getArea(polygon).get > 0.001 shading_surfaces_to_remove << shading_surface end end end shading_surfaces_to_remove.uniq.each do |shading_surface| shading_surface.remove end unless surfaces_modified runner.registerInfo("No surfaces found for adding #{WholeBuildingConstants.ObjectNameEaves}.") return true end num_added = shading_surface_group.shadingSurfaces.length runner.registerInfo("Added #{num_added} #{WholeBuildingConstants.ObjectNameEaves}.") return true end def self.get_existing_eaves_depth(shading_surface) existing_eaves_depth = 0 min_xs = [] (0..3).to_a.each do |i| if (shading_surface.vertices[0].x - shading_surface.vertices[i].x).abs > existing_eaves_depth min_xs << (shading_surface.vertices[0].x - shading_surface.vertices[i].x).abs end end unless min_xs.empty? return min_xs.min end return 0 end def self.process_neighbors(model, runner, left_neighbor_offset, right_neighbor_offset, back_neighbor_offset, front_neighbor_offset) # Error checking if left_neighbor_offset < 0 or right_neighbor_offset < 0 or back_neighbor_offset < 0 or front_neighbor_offset < 0 runner.registerError("Neighbor offsets must be greater than or equal to 0.") return false end surfaces = model.getSurfaces if surfaces.size == 0 runner.registerInfo("No surfaces found to copy for neighboring buildings.") return true end # Remove existing neighbors num_removed = 0 model.getShadingSurfaceGroups.each do |shading_surface_group| next unless shading_surface_group.name.to_s == WholeBuildingConstants.ObjectNameNeighbors shading_surface_group.remove num_removed += 1 end if num_removed > 0 runner.registerInfo("Removed #{num_removed} #{WholeBuildingConstants.ObjectNameNeighbors} shading surfaces.") end # No neighbor shading surfaces to add? Exit here. if [left_neighbor_offset, right_neighbor_offset, back_neighbor_offset, front_neighbor_offset].all? { |offset| offset == 0 } runner.registerInfo("No #{WholeBuildingConstants.ObjectNameNeighbors} shading surfaces to be added.") return true end # Get x, y, z minima and maxima of wall surfaces least_x = 9e99 greatest_x = -9e99 least_y = 9e99 greatest_y = -9e99 greatest_z = -9e99 surfaces.each do |surface| next unless surface.surfaceType.downcase == "wall" space = surface.space.get surface.vertices.each do |vertex| if vertex.x > greatest_x greatest_x = vertex.x end if vertex.x < least_x least_x = vertex.x end if vertex.y > greatest_y greatest_y = vertex.y end if vertex.y < least_y least_y = vertex.y end if vertex.z + space.zOrigin > greatest_z greatest_z = vertex.z + space.zOrigin end end end directions = [[WholeBuildingConstants.FacadeLeft, left_neighbor_offset], [WholeBuildingConstants.FacadeRight, right_neighbor_offset], [WholeBuildingConstants.FacadeBack, back_neighbor_offset], [WholeBuildingConstants.FacadeFront, front_neighbor_offset]] shading_surface_group = OpenStudio::Model::ShadingSurfaceGroup.new(model) shading_surface_group.setName(WholeBuildingConstants.ObjectNameNeighbors) num_added = 0 directions.each do |facade, neighbor_offset| next unless neighbor_offset > 0 vertices = OpenStudio::Point3dVector.new m = Geometry.initialize_transformation_matrix(OpenStudio::Matrix.new(4, 4, 0)) transformation = OpenStudio::Transformation.new(m) if facade == WholeBuildingConstants.FacadeLeft vertices << OpenStudio::Point3d.new(least_x - neighbor_offset, least_y, 0) vertices << OpenStudio::Point3d.new(least_x - neighbor_offset, least_y, greatest_z) vertices << OpenStudio::Point3d.new(least_x - neighbor_offset, greatest_y, greatest_z) vertices << OpenStudio::Point3d.new(least_x - neighbor_offset, greatest_y, 0) elsif facade == WholeBuildingConstants.FacadeRight vertices << OpenStudio::Point3d.new(greatest_x + neighbor_offset, greatest_y, 0) vertices << OpenStudio::Point3d.new(greatest_x + neighbor_offset, greatest_y, greatest_z) vertices << OpenStudio::Point3d.new(greatest_x + neighbor_offset, least_y, greatest_z) vertices << OpenStudio::Point3d.new(greatest_x + neighbor_offset, least_y, 0) elsif facade == WholeBuildingConstants.FacadeFront vertices << OpenStudio::Point3d.new(greatest_x, least_y - neighbor_offset, 0) vertices << OpenStudio::Point3d.new(greatest_x, least_y - neighbor_offset, greatest_z) vertices << OpenStudio::Point3d.new(least_x, least_y - neighbor_offset, greatest_z) vertices << OpenStudio::Point3d.new(least_x, least_y - neighbor_offset, 0) elsif facade == WholeBuildingConstants.FacadeBack vertices << OpenStudio::Point3d.new(least_x, greatest_y + neighbor_offset, 0) vertices << OpenStudio::Point3d.new(least_x, greatest_y + neighbor_offset, greatest_z) vertices << OpenStudio::Point3d.new(greatest_x, greatest_y + neighbor_offset, greatest_z) vertices << OpenStudio::Point3d.new(greatest_x, greatest_y + neighbor_offset, 0) end vertices = transformation * vertices shading_surface = OpenStudio::Model::ShadingSurface.new(vertices, model) shading_surface.setName(WholeBuildingConstants.ObjectNameNeighbors(facade)) shading_surface.setShadingSurfaceGroup(shading_surface_group) num_added += 1 end runner.registerInfo("Added #{num_added} #{WholeBuildingConstants.ObjectNameNeighbors} shading surfaces.") return true end def self.process_orientation(model, runner, orientation) if orientation > 360 or orientation < 0 runner.registerError("Invalid orientation entered.") return false end building = model.getBuilding unless building.northAxis == orientation runner.registerInfo("The building's initial orientation was #{building.northAxis} azimuth.") end building.setNorthAxis(orientation) # the shading surfaces representing neighbors have ShadingSurfaceType=Building, and so are oriented along with the building runner.registerInfo("The building's final orientation was #{building.northAxis} azimuth.") return true end end