# frozen_string_literal: true # ******************************************************************************* # OpenStudio(R), Copyright (c) 2008-2020, Alliance for Sustainable Energy, LLC. # All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # (1) Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # (2) Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # (3) Neither the name of the copyright holder nor the names of any contributors # may be used to endorse or promote products derived from this software without # specific prior written permission from the respective party. # # (4) Other than as required in clauses (1) and (2), distributions in any form # of modifications or other derivative works may not use the "OpenStudio" # trademark, "OS", "os", or any other confusingly similar designation without # specific prior written permission from Alliance for Sustainable Energy, LLC. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE # UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF # THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # ******************************************************************************* # see the URL below for information on how to write OpenStudio measures # http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/ # load OpenStudio measure libraries from openstudio-extension gem require 'openstudio-extension' require 'openstudio/extension/core/os_lib_geometry' # start the measure class ReplaceGeometryByStory < OpenStudio::Measure::ModelMeasure # resource file modules include OsLib_Geometry # human readable name def name return 'Replace Geometry By Story' end # human readable description def description return 'Test measure to throw away spaces and thermal zones of a completed model, adding in custom footprint for each story, and assigning proper space types, story, and fan exhaust. HVAC will be downstream.' end # human readable description of modeling approach def modeler_description return 'This is mockup for UrbanOpt, where footprint shape will come from geojson.' end # define the arguments that the user will input def arguments(model) args = OpenStudio::Measure::OSArgumentVector.new return args end # define what happens when the measure is run def run(model, runner, user_arguments) super(model, runner, user_arguments) # use the built-in error checking if !runner.validateUserArguments(arguments(model), user_arguments) return false end # report initial condition of model runner.registerInitialCondition("The building started with #{model.getSpaces.size} spaces.") # create hash of space type by story story_hash = {} model.getBuildingStorys.each do |story| next if !story.spaces.first.spaceType.is_initialized story_hash[story] = {} story_hash[story][:space_type] = story.spaces.first.spaceType.get # set nominalZCoordinate, as it is used later on minz_spaces = [] sorted_spaces = {} story.spaces.each do |space| # loop through space surfaces to find min z value z_points = [] space.surfaces.each do |surface| surface.vertices.each do |vertex| z_points << vertex.z end end minz_spaces << z_points.min + space.zOrigin end if !minz_spaces.empty? story.setNominalZCoordinate(minz_spaces.min) end end orig_floor_area = model.getBuilding.floorArea # un-assign fan exhaust zone objects model.getFanZoneExhausts.each(&:removeFromThermalZone) # Identity matrix for setting space origins m = OpenStudio::Matrix.new(4, 4, 0) m[0, 0] = 1 m[1, 1] = 1 m[2, 2] = 1 m[3, 3] = 1 # target origin for all spaces m[0, 3] = 0.0 m[1, 3] = 0.0 m[2, 3] = 0.0 # space transformation model.getSpaces.each do |space| space.changeTransformation(OpenStudio::Transformation.new(m)) end # loop through surfaces floor_polygons = [] starting_footprint_area = 0.0 story_hash.each do |story, hash| hash[:basement] = false hash[:multipliers] = [] story.spaces.each do |space| hash[:multipliers] << space.multiplier space.surfaces.each do |surface| next if !(surface.outsideBoundaryCondition == 'Ground' || surface.outsideBoundaryCondition == 'OtherSideCoefficients') if surface.surfaceType == 'Wall' hash[:basement] = true next elsif surface.surfaceType != 'Floor' next end # runner.registerInfo("#{surface.name} is a ground exposed floor") starting_footprint_area += surface.grossArea # add to polygons new_floor_polygon = [] surface.vertices.each do |vertex| new_floor_polygon << OpenStudio::Point3d.new(vertex.x, vertex.y, 0.0) end floor_polygons << new_floor_polygon end end end # report starting footprint area starting_footprint_area_ip = OpenStudio.toNeatString(OpenStudio.convert(starting_footprint_area, 'm^2', 'ft^2').get, 0, true) runner.registerInfo("Model has #{floor_polygons.size} ground exposed floor surfaces, with an area of #{starting_footprint_area_ip} (ft^2).") # Combine the polygons combined_polygons = OpenStudio.joinAll(floor_polygons, 0.01) # temp code to work around bug in joinAll floor_polygons2 = floor_polygons floor_polygons.size.times do |i| floor_polygons2 << combined_polygons.first combined_polygons = OpenStudio.joinAll(floor_polygons2.rotate(i), 0.01) end combined_polygons = combined_polygons.first # get target wwr target_wwr = OsLib_Geometry.getExteriorWindowToWallRatio(model.getSpaces) runner.registerInfo("Initial window to wall ratio is #{target_wwr}") # remove geometry model.getThermalZones.each(&:remove) model.getSpaces.each(&:remove) # add new geometry story_hash.each do |story, hash| space_type = hash[:space_type] options = {} options['name'] = story.name.get options['spaceType'] = space_type options['story'] = story options['makeThermalZone'] = true options['thermalZoneMultiplier'] = hash[:multipliers].min options['floor_to_floor_height'] = story.nominalFloortoFloorHeight.get space = OsLib_Geometry.makeSpaceFromPolygon(model, OpenStudio::Point3d.new(0, 0, 0), combined_polygons, options) space.setZOrigin(story.nominalZCoordinate.get) # make ext walls ground if original space had any ground exposed walls if hash[:basement] space.surfaces.each do |surface| next if surface.surfaceType != 'Wall' surface.setOutsideBoundaryCondition('Ground') end end end # surface match spaces = OpenStudio::Model::SpaceVector.new model.getSpaces.each do |space| spaces << space end OpenStudio::Model.matchSurfaces(spaces) # set window to wall ratio model.getSpaces.each do |space| space.surfaces.each do |surface| next if surface.outsideBoundaryCondition != 'Outdoors' next if surface.surfaceType != 'Wall' surface.setWindowToWallRatio(target_wwr) end end # re-assign fan zone exhaust objets zone_hash = {} # key is zone value is floor area. It excludes zones with non 1 multiplier model.getThermalZones.each do |thermal_zone| next if thermal_zone.multiplier > 1 zone_hash[thermal_zone] = thermal_zone.floorArea end target_zone = zone_hash.key(zone_hash.values.max) model.getFanZoneExhausts.each do |exhaust| exhaust.addToThermalZone(target_zone) end # check that footprint matches expected if model.getSpaces.first.floorArea.round(2) != starting_footprint_area.round(2) runner.registerWarning("Resulting floor area of #{model.getSpaces.first.floorArea} doesn't match expected value of #{starting_footprint_area}") end # report final condition of model runner.registerFinalCondition("The building finished with #{model.getSpaces.size} spaces.") return true end end # register the measure to be used by the application ReplaceGeometryByStory.new.registerWithApplication