# ******************************************************************************* # OpenStudio(R), Copyright (c) 2008-2021, 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. # ******************************************************************************* module OsLib_ModelSimplification # get all loads for a space_or_space_type and place in hash by type def gather_internal_loads(space_or_space_type) internal_load_hash = {} # gather different load types (all vectors except dsoa which will be turned into an array) internal_load_hash[:internal_mass] = space_or_space_type.internalMass internal_load_hash[:people] = space_or_space_type.people internal_load_hash[:lights] = space_or_space_type.lights internal_load_hash[:luminaires] = space_or_space_type.luminaires internal_load_hash[:electric_equipment] = space_or_space_type.electricEquipment internal_load_hash[:gas_equipment] = space_or_space_type.gasEquipment internal_load_hash[:hot_water_equipment] = space_or_space_type.hotWaterEquipment internal_load_hash[:steam_equipment] = space_or_space_type.steamEquipment internal_load_hash[:other_equipment] = space_or_space_type.otherEquipment internal_load_hash[:space_infiltration_design_flow_rates] = space_or_space_type.spaceInfiltrationDesignFlowRates internal_load_hash[:space_infiltration_effective_leakage_areas] = space_or_space_type.spaceInfiltrationEffectiveLeakageAreas if space_or_space_type.designSpecificationOutdoorAir.nil? internal_load_hash[:design_specification_outdoor_air] = [] else internal_load_hash[:design_specification_outdoor_air] = [space_or_space_type.designSpecificationOutdoorAir] end if space_or_space_type.class.to_s == 'OpenStudio::Model::Space' internal_load_hash[:water_use_equipment] = space_or_space_type.waterUseEquipment # don't think this reports internal_load_hash[:daylighting_controls] = space_or_space_type.daylightingControls end # TODO: - warn if daylighting controls in spaces (should I alter fraction controled based on lighting per area ratio) return internal_load_hash end # blend_space_types_from_floor_area_ratio used when working from space type ratio and un-assigned space types def blend_space_types_from_floor_area_ratio(runner, model, space_type_ratio_hash) # create stub blended space type blended_space_type = OpenStudio::Model::SpaceType.new(model) blended_space_type.setName('Blended Space Type') # TODO: - inspect people instances and see if any defs are not normalized per area. If find any issue warning # gather inputs sum_of_num_people_per_m_2 = 0.0 space_type_ratio_hash.each do |space_type, ratios| # get number of peple per m 2 for space type. Can do this without looking at instances sum_of_num_people_per_m_2 += space_type.getPeoplePerFloorArea(1.0) end # raw num_people_ratios sum_area_adj_num_people_ratio = 0.0 space_type_ratio_hash.each do |space_type, ratios| # calculate num_people_ratios area_adj_num_people_ratio = (space_type.getPeoplePerFloorArea(1.0) / sum_of_num_people_per_m_2) * ratios[:floor_area_ratio] sum_area_adj_num_people_ratio += area_adj_num_people_ratio end # set ratios largest_space_type = nil largest_space_type_ratio = 0.00 space_type_ratio_hash.each do |space_type, ratios| # calculate num_people_ratios area_adj_num_people_ratio = (space_type.getPeoplePerFloorArea(1.0) / sum_of_num_people_per_m_2) * ratios[:floor_area_ratio] normalized_area_adj_num_people_ratio = area_adj_num_people_ratio / sum_area_adj_num_people_ratio # ratios[:floor_area_ratio] is already defined ratios[:num_people_ratio] = normalized_area_adj_num_people_ratio.round(4) ratios[:ext_surface_area_ratio] = ratios[:floor_area_ratio] ratios[:ext_wall_area_ratio] = ratios[:floor_area_ratio] ratios[:volume_ratio] = ratios[:floor_area_ratio] # update largest space type values if largest_space_type.nil? largest_space_type = space_type largest_space_type_ratio = ratios[:floor_area_ratio] elsif ratios[:floor_area_ratio] > largest_space_type_ratio largest_space_type = space_type largest_space_type_ratio = ratios[:floor_area_ratio] end end if largest_space_type.nil? runner.registerError("Didn't find any space types in model matching user argument string.") return nil end # set standards info for space type based on largest ratio (for use to apply HVAC system) standards_building_type = largest_space_type.standardsBuildingType standards_space_type = largest_space_type.standardsSpaceType if standards_building_type.is_initialized blended_space_type.setStandardsBuildingType(standards_building_type.get) end if standards_space_type.is_initialized blended_space_type.setStandardsSpaceType(standards_space_type.get) end # loop therough space types to get instances from and then remove space_type_ratio_hash.each do |space_type, ratios| # blend internal loads (nil is space_hash) space_type_load_instances = blend_internal_loads(runner, model, space_type, blended_space_type, ratios, model.getBuilding.floorArea, nil) runner.registerInfo("Blending #{space_type.name.get} with floor area ratio of #{ratios[:floor_area_ratio]} and number of people ratio of #{ratios[:num_people_ratio]}.") # delete space type. Don't want to leave in model since internal loads have been removed from it space_type.remove end return blended_space_type end # takes in space type hash where each hash value is a colleciton of space types. Each collection is blended into it's own space type # If key for any collection is "Building" it will also opererate on spaces that don't have space type assigned # where a space assigned to a space type from a collection has space loads, those space loads are normalized and added to the blended space type # load instances are maintained so that they can haave unique schedules, and can have EE measures selectivly applied. def blend_space_type_collections(runner, model, space_type_hash) # loop through building type hash to create multiple blends space_type_hash.each do |collection_name, space_types| if collection_name == 'Building' space_array = model.getSpaces.sort # use all space types, not just space types passed in else space_array = [] space_types.each do |space_type| space_array.concat(space_type.spaces) end end # calculate metrics for all spaces included in building area to pass into space_type and space hash # note: in the future this may be a subset of spaces if blending into multiple space types vs. just one. collection_totals = {} collection_totals[:floor_area] = 0.0 collection_totals[:num_people] = 0.0 collection_totals[:ext_surface_area] = 0.0 collection_totals[:ext_wall_area] = 0.0 collection_totals[:volume] = 0.0 space_array.each do |space| next if !space.partofTotalFloorArea collection_totals[:floor_area] += space.floorArea * space.multiplier collection_totals[:num_people] += space.numberOfPeople * space.multiplier collection_totals[:ext_surface_area] += space.exteriorArea * space.multiplier collection_totals[:ext_wall_area] += space.exteriorWallArea * space.multiplier collection_totals[:volume] += space.volume * space.multiplier end area_ip = OpenStudio.convert(collection_totals[:floor_area], 'm^2', 'ft^2').get area_ip_neat = OpenStudio.toNeatString(area_ip, 2, true) runner.registerInfo("#{collection_name} area is #{area_ip_neat} ft^2, number of people is #{collection_totals[:num_people].round(0)}.") # create hash of space types and floor area for all space types with area > 0 when spaces included in floor area # code to gather space type areas came from openstudio_results measure. space_type_hash = {} largest_space_type = nil largest_space_type_ratio = 0.00 space_types.each do |space_type| next if space_type.floorArea == 0 space_type_totals = {} space_type_totals[:floor_area] = 0.0 space_type_totals[:num_people] = 0.0 space_type_totals[:ext_surface_area] = 0.0 space_type_totals[:ext_wall_area] = 0.0 space_type_totals[:volume] = 0.0 # loop through spaces so I can skip if not included in floor area space_type.spaces.each do |space| next if !space.partofTotalFloorArea space_type_totals[:floor_area] += space.floorArea * space.multiplier space_type_totals[:num_people] += space.numberOfPeople * space.multiplier space_type_totals[:ext_surface_area] += space.exteriorArea * space.multiplier space_type_totals[:ext_wall_area] += space.exteriorWallArea * space.multiplier space_type_totals[:volume] += space.volume * space.multiplier end # update largest space type values if largest_space_type.nil? largest_space_type = space_type largest_space_type_ratio = space_type_totals[:floor_area] elsif space_type_totals[:floor_area] > largest_space_type_ratio largest_space_type = space_type largest_space_type_ratio = space_type_totals[:floor_area] end # gather internal loads space_type_loads_hash = gather_internal_loads(space_type) # don't add to hash if no spaces used for space type are included in building area (e.g. plenum and attic) # todo - log these and decide what to do for them. Leave loads alone or remove, do they add to blend at all? next if space_type_totals[:floor_area] == 0 if !space_type_totals[:floor_area] = space_type.floorArea # TODO: - not sure if these would ever show as different runner.registerWarning("Some but not all spaces of #{space_type.name} space type are not included in the building floor area. May have unexpected results") end # populate space type hash space_type_hash[space_type] = { int_loads: space_type_loads_hash, totals: space_type_totals } end # report initial condition of model runner.registerInfo("#{collection_name} accounts for #{space_type_hash.size} space types.") if collection_name == 'Building' # count area of spaces that have no space type no_space_type_area_counter = 0 model.getSpaces.sort.each do |space| if space.spaceType.empty? next if !space.partofTotalFloorArea no_space_type_area_counter += space.floorArea * space.multiplier end end floor_area_ratio = no_space_type_area_counter / collection_totals[:floor_area] if floor_area_ratio > 0 runner.registerInfo("#{floor_area_ratio} fraction of building area is composed of spaces without space type assignments.") end end # report the space ratio for hard spaces space_hash = {} space_array.each do |space| next if !space.partofTotalFloorArea space_loads_hash = gather_internal_loads(space) space_totals = {} space_totals[:floor_area] = space.floorArea * space.multiplier space_totals[:num_people] = space.numberOfPeople * space.multiplier space_totals[:ext_surface_area] = space.exteriorArea * space.multiplier space_totals[:ext_wall_area] = space.exteriorWallArea * space.multiplier space_totals[:volume] = space.volume * space.multiplier if !space_loads_hash[:daylighting_controls].empty? runner.registerWarning("#{space.name} has one or more daylighting controls. Lighting loads from blended space type may affect lighting reduction from daylighting controls.") end if !space_loads_hash[:water_use_equipment].empty? runner.registerInfo("One ore more water use equipment objects are associated with space #{space.name}. This can't be moved to a space type.") end # note: If generating ratios without geometry can calculate people_ratio given space_types floor_area_ratio space_hash[space] = { int_loads: space_loads_hash, totals: space_totals } end # create stub blended space type blended_space_type = OpenStudio::Model::SpaceType.new(model) blended_space_type.setName("#{collection_name} Blended Space Type") # set standards info for space type based on largest ratio (for use to apply HVAC system) standards_building_type = largest_space_type.standardsBuildingType standards_space_type = largest_space_type.standardsSpaceType if standards_building_type.is_initialized blended_space_type.setStandardsBuildingType(standards_building_type.get) end if standards_space_type.is_initialized blended_space_type.setStandardsSpaceType(standards_space_type.get) end # values from collection hash collection_floor_area = collection_totals[:floor_area] collection_num_people = collection_totals[:num_people] collection_ext_surface_area = collection_totals[:ext_surface_area] collection_ext_wall_area = collection_totals[:ext_wall_area] collection_volume = collection_totals[:volume] # loop through space that have one or more spaces included in the building area space_type_hash.each do |space_type, hash| # hard assign space load schedules before re-assign instances to blended space type space_type.hardApplySpaceLoadSchedules # vaules from space or space_type floor_area = hash[:totals][:floor_area] num_people = hash[:totals][:num_people] ext_surface_area = hash[:totals][:ext_surface_area] ext_wall_area = hash[:totals][:ext_wall_area] volume = hash[:totals][:volume] # ratios ratios = {} if collection_floor_area > 0 ratios[:floor_area_ratio] = floor_area / collection_floor_area else ratios[:floor_area_ratio] = 0.0 end if collection_num_people > 0 ratios[:num_people_ratio] = num_people / collection_num_people else ratios[:num_people_ratio] = 0.0 end if collection_ext_surface_area > 0 ratios[:ext_surface_area_ratio] = ext_surface_area / collection_ext_surface_area else ratios[:ext_surface_area_ratio] = 0.0 end if collection_ext_wall_area > 0 ratios[:ext_wall_area_ratio] = ext_wall_area / collection_ext_wall_area else ratios[:ext_wall_area_ratio] = 0.0 end if collection_volume > 0 ratios[:volume_ratio] = volume / collection_volume else ratios[:volume_ratio] = 0.0 end # populate blended space type with space type loads space_type_load_instances = blend_internal_loads(runner, model, space_type, blended_space_type, ratios, collection_floor_area, space_hash) runner.registerInfo("Blending space type #{space_type.name}. Floor area ratio is #{(hash[:totals][:floor_area] / collection_totals[:floor_area]).round(3)}. People ratio is #{(hash[:totals][:num_people] / collection_totals[:num_people]).round(3)}") # hard assign any constructions assigned by space types, except for space not included in the building area if space_type.defaultConstructionSet.is_initialized runner.registerInfo("Hard assigning constructions for #{space_type.name}.") space_type.spaces.each(&:hardApplyConstructions) end # remove all space type assignments, except for spaces not included in building area. space_type.spaces.each do |space| next if !space.partofTotalFloorArea space.resetSpaceType end # delete space type. Don't want to leave in model since internal loads have been removed from it space_type.remove end # loop through spaces that are included in building area space_hash.each do |space, hash| # hard assign space load schedules before re-assign instances to blended space type space.hardApplySpaceLoadSchedules # vaules from space or space_type floor_area = hash[:totals][:floor_area] num_people = hash[:totals][:num_people] ext_surface_area = hash[:totals][:ext_surface_area] ext_wall_area = hash[:totals][:ext_wall_area] volume = hash[:totals][:volume] # ratios ratios = {} if collection_floor_area > 0 ratios[:floor_area_ratio] = floor_area / collection_floor_area else ratios[:floor_area_ratio] = 0.0 end if collection_num_people > 0 ratios[:num_people_ratio] = num_people / collection_num_people else ratios[:num_people_ratio] = 0.0 end if collection_ext_surface_area > 0 ratios[:ext_surface_area_ratio] = ext_surface_area / collection_ext_surface_area else ratios[:ext_surface_area_ratio] = 0.0 end if collection_ext_wall_area > 0 ratios[:ext_wall_area_ratio] = ext_wall_area / collection_ext_wall_area else ratios[:ext_wall_area_ratio] = 0.0 end if collection_volume > 0 ratios[:volume_ratio] = volume / collection_volume else ratios[:volume_ratio] = 0.0 end # populate blended space type with space loads space_load_instances = blend_internal_loads(runner, model, space, blended_space_type, ratios, collection_floor_area, space_hash) next if space_load_instances.empty? runner.registerInfo("Blending space #{space.name}. Floor area ratio is #{(hash[:totals][:floor_area] / collection_totals[:floor_area]).round(3)}. People ratio is #{(hash[:totals][:num_people] / collection_totals[:num_people]).round(3)}") end if collection_name == 'Building' # assign blended space type to building model.getBuilding.setSpaceType(blended_space_type) building_space_type = model.getBuilding.spaceType else space_array.each do |space| space.setSpaceType(blended_space_type) end end end return model.getSpaceTypes.sort end # blend internal loads used when working from existing model def blend_internal_loads(runner, model, source_space_or_space_type, target_space_type, ratios, collection_floor_area, space_hash) # ratios floor_area_ratio = ratios[:floor_area_ratio] num_people_ratio = ratios[:num_people_ratio] ext_surface_area_ratio = ratios[:ext_surface_area_ratio] ext_wall_area_ratio = ratios[:ext_wall_area_ratio] volume_ratio = ratios[:volume_ratio] # for normalizing design level loads I need to know effective number of spaces instance is applied to if source_space_or_space_type.to_Space.is_initialized eff_num_spaces = source_space_or_space_type.multiplier else eff_num_spaces = 0 source_space_or_space_type.spaces.each do |space| eff_num_spaces += space.multiplier end end # array of load instacnes re-assigned to blended space instances_array = [] # internal_mass source_space_or_space_type.internalMass.each do |load_inst| load_def = load_inst.definition.to_InternalMassDefinition.get if load_def.surfaceArea.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_InternalMass.get orig_design_level = cloned_load_def.surfaceArea.get cloned_load_def.setSurfaceAreaperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} m^2.") load_inst.setInternalMassDefinition(cloned_load_def) end elsif load_def.surfaceAreaperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.surfaceAreaperPerson.is_initialized if num_people_ratio.nil? runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in") return false else load_inst.setMultiplier(load_inst.multiplier * num_people_ratio) end else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # people source_space_or_space_type.people.each do |load_inst| load_def = load_inst.definition.to_PeopleDefinition.get if load_def.numberofPeople.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_PeopleDefinition.get orig_design_level = cloned_load_def.numberofPeople.get cloned_load_def.setPeopleperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} people.") load_inst.setPeopleDefinition(cloned_load_def) end elsif load_def.peopleperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.spaceFloorAreaperPerson.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # lights source_space_or_space_type.lights.each do |load_inst| load_def = load_inst.definition.to_LightsDefinition.get if load_def.lightingLevel.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_LightsDefinition.get orig_design_level = cloned_load_def.lightingLevel.get cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.") load_inst.setLightsDefinition(cloned_load_def) end elsif load_def.wattsperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.wattsperPerson.is_initialized if num_people_ratio.nil? runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in") return false else load_inst.setMultiplier(load_inst.multiplier * num_people_ratio) end else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # luminaires source_space_or_space_type.luminaires.each do |load_inst| # TODO: - can't normalize luminaire. Replace it with similar normalized lights def and instance runner.registerWarning("Can't area normalize luminaire. Instance will be applied to every space using the blended space type") instances_array << load_inst end # electric_equipment source_space_or_space_type.electricEquipment.each do |load_inst| load_def = load_inst.definition.to_ElectricEquipmentDefinition.get if load_def.designLevel.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_ElectricEquipmentDefinition.get orig_design_level = cloned_load_def.designLevel.get cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.") load_inst.setElectricEquipmentDefinition(cloned_load_def) end elsif load_def.wattsperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.wattsperPerson.is_initialized if num_people_ratio.nil? runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in") return false else load_inst.setMultiplier(load_inst.multiplier * num_people_ratio) end else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # gas_equipment source_space_or_space_type.gasEquipment.each do |load_inst| load_def = load_inst.definition.to_GasEquipmentDefinition.get if load_def.designLevel.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_GasEquipmentDefinition.get orig_design_level = cloned_load_def.designLevel.get cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.") load_inst.setGasEquipmentDefinition(cloned_load_def) end elsif load_def.wattsperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.wattsperPerson.is_initialized if num_people_ratio.nil? runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in") return false else load_inst.setMultiplier(load_inst.multiplier * num_people_ratio) end else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # hot_water_equipment source_space_or_space_type.hotWaterEquipment.each do |load_inst| load_def = load_inst.definition.to_HotWaterDefinition.get if load_def.designLevel.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_HotWaterEquipmentDefinition.get orig_design_level = cloned_load_def.designLevel.get cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.") load_inst.setHotWaterEquipmentDefinition(cloned_load_def) end elsif load_def.wattsperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.wattsperPerson.is_initialized if num_people_ratio.nil? runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in") return false else load_inst.setMultiplier(load_inst.multiplier * num_people_ratio) end else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # steam_equipment source_space_or_space_type.steamEquipment.each do |load_inst| load_def = load_inst.definition.to_SteamDefinition.get if load_def.designLevel.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_SteamEquipmentDefinition.get orig_design_level = cloned_load_def.designLevel.get cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.") load_inst.setSteamEquipmentDefinition(cloned_load_def) end elsif load_def.wattsperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.wattsperPerson.is_initialized if num_people_ratio.nil? runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in") return false else load_inst.setMultiplier(load_inst.multiplier * num_people_ratio) end else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # other_equipment source_space_or_space_type.otherEquipment.each do |load_inst| load_def = load_inst.definition.to_OtherDefinition.get if load_def.designLevel.is_initialized # edit and assign a clone of definition and normalize per area based on floor area ratio if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else cloned_load_def = load_def.clone(model).to_OtherEquipmentDefinition.get orig_design_level = cloned_load_def.designLevel.get cloned_load_def.setWattsperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) cloned_load_def.setName("#{cloned_load_def.name} - pre-normalized value was #{orig_design_level.round} W.") load_inst.setOtherEquipmentDefinition(cloned_load_def) end elsif load_def.wattsperSpaceFloorArea.is_initialized load_inst.setMultiplier(load_inst.multiplier * floor_area_ratio) elsif load_def.wattsperPerson.is_initialized if num_people_ratio.nil? runner.registerError("#{load_def} has value defined per person, but people ratio wasn't passed in") return false else load_inst.setMultiplier(load_inst.multiplier * num_people_ratio) end else runner.registerError("Unexpected value type for #{load_def.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # space_infiltration_design_flow_rates source_space_or_space_type.spaceInfiltrationDesignFlowRates.each do |load_inst| if load_inst.designFlowRateCalculationMethod == 'Flow/Space' # edit load so normalized for building area if collection_floor_area == 0 runner.registerWarning("Can't determine building floor area to normalize #{load_def}. #{load_inst} will be asigned the the blended space without altering its values.") else orig_design_level = load_inst.designFlowRate.get load_inst.setFlowperSpaceFloorArea(eff_num_spaces * orig_design_level / collection_floor_area) load_inst.setName("#{load_inst.name} - pre-normalized value was #{orig_design_level} m^3/sec") end elsif load_inst.designFlowRateCalculationMethod == 'Flow/Area' load_inst.setFlowperSpaceFloorArea(load_inst.flowperSpaceFloorArea.get * floor_area_ratio) elsif load_inst.designFlowRateCalculationMethod == 'Flow/ExteriorArea' load_inst.setFlowperExteriorSurfaceArea(load_inst.flowperExteriorSurfaceArea.get * ext_surface_area_ratio) elsif load_inst.designFlowRateCalculationMethod == 'Flow/ExteriorWallArea' load_inst.setFlowperExteriorWallArea(load_inst.flowperExteriorWallArea.get * ext_wall_area_ratio) elsif load_inst.designFlowRateCalculationMethod == 'AirChanges/Hour' load_inst.setAirChangesperHour (load_inst.airChangesperHour.get * volume_ratio) else runner.registerError("Unexpected value type for #{load_inst.name}") return false end load_inst.setSpaceType(target_space_type) instances_array << load_inst end # space_infiltration_effective_leakage_areas source_space_or_space_type.spaceInfiltrationEffectiveLeakageAreas.each do |load| # TODO: - can't normalize space_infiltration_effective_leakage_areas. Come up with logic to address this runner.registerWarning("Can't area normalize space_infiltration_effective_leakage_areas. It will be applied to every space using the blended space type") load.setSpaceType(target_space_type) instances_array << load end # add OA object if it doesn't already exist if target_space_type.designSpecificationOutdoorAir.is_initialized blended_oa = target_space_type.designSpecificationOutdoorAir.get else blended_oa = OpenStudio::Model::DesignSpecificationOutdoorAir.new(model) blended_oa.setName('Blended OA') blended_oa.setOutdoorAirMethod('Sum') target_space_type.setDesignSpecificationOutdoorAir(blended_oa) instances_array << blended_oa end # update OA object if source_space_or_space_type.designSpecificationOutdoorAir.is_initialized oa = source_space_or_space_type.designSpecificationOutdoorAir.get oa_sch = nil if oa.outdoorAirFlowRateFractionSchedule.is_initialized # TODO: - improve logic to address multiple schedules runner.registerWarning("Schedule #{oa.outdoorAirFlowRateFractionSchedule.get.name} assigned to #{oa.name} will be ignored. New OA object will not have a schedule assigned") end if oa.outdoorAirMethod == 'Maximum' # TODO: - see if way to address this by pre-calculating the max and only entering that value for space type runner.registerWarning("Outdoor air method of Maximum will be ignored for #{oa.name}. New OA object will have outdoor air method of Sum.") end # adjusted ratios for oa (lowered for space type if there is hard assigned oa load for one or more spaces) oa_floor_area_ratio = floor_area_ratio oa_num_people_ratio = num_people_ratio if source_space_or_space_type.class.to_s == 'OpenStudio::Model::SpaceType' source_space_or_space_type.spaces.each do |space| if !space.isDesignSpecificationOutdoorAirDefaulted if space_hash.nil? runner.registerWarning('No space_hash passed in and model has OA designed at space level.') else oa_floor_area_ratio -= space_hash[space][:floor_area_ratio] oa_num_people_ratio -= space_hash[space][:num_people_ratio] end end end end # add to values of blended OA load if oa.outdoorAirFlowperPerson > 0 blended_oa.setOutdoorAirFlowperPerson(blended_oa.outdoorAirFlowperPerson + oa.outdoorAirFlowperPerson * oa_num_people_ratio) end if oa.outdoorAirFlowperFloorArea > 0 blended_oa.setOutdoorAirFlowperFloorArea(blended_oa.outdoorAirFlowperFloorArea + oa.outdoorAirFlowperFloorArea * oa_floor_area_ratio) end if oa.outdoorAirFlowRate > 0 # calculate quantity for instance (doesn't exist as a method in api) if source_space_or_space_type.class.to_s == 'OpenStudio::Model::SpaceType' quantity = 0 source_space_or_space_type.spaces.each do |space| if !space.isDesignSpecificationOutdoorAirDefaulted quantity += space.multiplier end end else quantity = source_space_or_space_type.multiplier end # can't normalize air flow rate, convert to air flow rate per floor area blended_oa.setOutdoorAirFlowperFloorArea(blended_oa.outdoorAirFlowperFloorArea + quantity * oa.outdoorAirFlowRate / collection_floor_area) end if oa.outdoorAirFlowAirChangesperHour > 0 # floor area should be good approximation of area for multiplier blended_oa.setOutdoorAirFlowAirChangesperHour(blended_oa.outdoorAirFlowAirChangesperHour + oa.outdoorAirFlowAirChangesperHour * oa_floor_area_ratio) end end # note: water_use_equipment can't be assigned to a space type. Leave it as is, if assigned to space type # todo - if we use this measure with new geometry need to find a way to pull water use equipment loads into new model return instances_array end # sort building stories def sort_building_stories_and_get_min_multiplier(model) sorted_building_stories = {} # loop through stories model.getBuildingStorys.sort.each do |story| story_min_z = nil # loop through spaces in story. story.spaces.sort.each do |space| space_z_min = OsLib_Geometry.getSurfaceZValues(space.surfaces.to_a).min + space.zOrigin if story_min_z.nil? || (story_min_z > space_z_min) story_min_z = space_z_min end end sorted_building_stories[story] = story_min_z end return sorted_building_stories end # gather_envelope_data for envelope simplification def gather_envelope_data(runner, model) runner.registerInfo('Gathering envelope data.') # hash to contain envelope data envelope_data_hash = {} # used for overhang and party wall orientation catigorization facade_options = { 'northEast' => 45, 'southEast' => 125, 'southWest' => 225, 'northWest' => 315 } # get building level inputs envelope_data_hash[:north_axis] = model.getBuilding.northAxis envelope_data_hash[:building_floor_area] = model.getBuilding.floorArea envelope_data_hash[:building_exterior_surface_area] = model.getBuilding.exteriorSurfaceArea envelope_data_hash[:building_exterior_wall_area] = model.getBuilding.exteriorWallArea envelope_data_hash[:building_exterior_roof_area] = envelope_data_hash[:building_exterior_surface_area] - envelope_data_hash[:building_exterior_wall_area] envelope_data_hash[:building_air_volume] = model.getBuilding.airVolume envelope_data_hash[:building_perimeter] = nil # will be applied for first story without ground walls # get bounding_box bounding_box = OpenStudio::BoundingBox.new model.getSpaces.sort.each do |space| space.surfaces.sort.each do |spaceSurface| bounding_box.addPoints(space.transformation * spaceSurface.vertices) end end min_x = bounding_box.minX.get min_y = bounding_box.minY.get min_z = bounding_box.minZ.get max_x = bounding_box.maxX.get max_y = bounding_box.maxY.get max_z = bounding_box.maxZ.get envelope_data_hash[:building_min_xyz] = [min_x, min_y, min_z] envelope_data_hash[:building_max_xyz] = [max_x, max_y, max_z] # add orientation specific wwr ext_surfaces_hash = OsLib_Geometry.getExteriorWindowAndWllAreaByOrientation(model, model.getSpaces.sort.to_a) envelope_data_hash[:building_wwr_n] = ext_surfaces_hash['northWindow'] / ext_surfaces_hash['northWall'] envelope_data_hash[:building_wwr_s] = ext_surfaces_hash['southWindow'] / ext_surfaces_hash['southWall'] envelope_data_hash[:building_wwr_e] = ext_surfaces_hash['eastWindow'] / ext_surfaces_hash['eastWall'] envelope_data_hash[:building_wwr_w] = ext_surfaces_hash['westWindow'] / ext_surfaces_hash['westWall'] envelope_data_hash[:stories] = {} # each entry will be hash with buildingStory as key and attributes has values envelope_data_hash[:space_types] = {} # each entry will be hash with spaceType as key and attributes has values # as rough estimate overhang area / glazing area should be close to projection factor assuming overhang is same width as windows # will only add building shading surfaces assoicated with a sub-surface. building_overhang_area_n = 0.0 building_overhang_area_s = 0.0 building_overhang_area_e = 0.0 building_overhang_area_w = 0.0 # loop through stories based on mine z height of surfaces. sorted_stories = sort_building_stories_and_get_min_multiplier(model).sort_by { |k, v| v } sorted_stories.each do |story, story_min_z| story_min_multiplier = nil story_footprint = nil story_multiplied_floor_area = OsLib_HelperMethods.getAreaOfSpacesInArray(model, story.spaces, 'floorArea')['totalArea'] # goal of footprint calc is to count multiplier for hotel room on facade,but not to count what is intended as a story multiplier story_multiplied_exterior_surface_area = OsLib_HelperMethods.getAreaOfSpacesInArray(model, story.spaces, 'exteriorArea')['totalArea'] story_multiplied_exterior_wall_area = OsLib_HelperMethods.getAreaOfSpacesInArray(model, story.spaces, 'exteriorWallArea')['totalArea'] story_multiplied_exterior_roof_area = story_multiplied_exterior_surface_area - story_multiplied_exterior_wall_area story_has_ground_walls = [] story_has_adiabatic_walls = [] story_included_in_building_area = false # will be true if any spaces on story are inclued in building area story_max_z = nil # loop through spaces for story gathering information story.spaces.each do |space| # get min multiplier value multiplier = space.multiplier if story_min_multiplier.nil? || (story_min_multiplier > multiplier) story_min_multiplier = multiplier end # calculate footprint story_footprint = story_multiplied_floor_area / story_min_multiplier # see if part of floor area if space.partofTotalFloorArea story_included_in_building_area = true # add to space type ratio hash when space is included in building floor area if space.spaceType.is_initialized space_type = space.spaceType.get space_floor_area = space.floorArea * space.multiplier if envelope_data_hash[:space_types].key?(space_type) envelope_data_hash[:space_types][space_type][:floor_area] += space_floor_area else envelope_data_hash[:space_types][space_type] = {} envelope_data_hash[:space_types][space_type][:floor_area] = space_floor_area # make hash for heating and cooling setpoints envelope_data_hash[:space_types][space_type][:htg_setpoint] = {} envelope_data_hash[:space_types][space_type][:clg_setpoint] = {} end # add heating and cooling setpoints if space.thermalZone.is_initialized && space.thermalZone.get.thermostatSetpointDualSetpoint.is_initialized thermostat = space.thermalZone.get.thermostatSetpointDualSetpoint.get # log heating schedule if thermostat.heatingSetpointTemperatureSchedule.is_initialized htg_sch = thermostat.heatingSetpointTemperatureSchedule.get if envelope_data_hash[:space_types][space_type][:htg_setpoint].key?(htg_sch) envelope_data_hash[:space_types][space_type][:htg_setpoint][htg_sch] += space_floor_area else envelope_data_hash[:space_types][space_type][:htg_setpoint][htg_sch] = space_floor_area end else runner.registerWarning("#{space.thermalZone.get.name} containing #{space.name} doesn't have a heating setpoint schedule.") end # log cooling schedule if thermostat.coolingSetpointTemperatureSchedule.is_initialized clg_sch = thermostat.coolingSetpointTemperatureSchedule.get if envelope_data_hash[:space_types][space_type][:clg_setpoint].key?(clg_sch) envelope_data_hash[:space_types][space_type][:clg_setpoint][clg_sch] += space_floor_area else envelope_data_hash[:space_types][space_type][:clg_setpoint][clg_sch] = space_floor_area end else runner.registerWarning("#{space.thermalZone.get.name} containing #{space.name} doesn't have a heating setpoint schedule.") end else runner.registerWarning("#{space.name} either isn't in a thermal zone or doesn't have a thermostat assigned") end else runner.regsiterWarning("#{space.name} is included in the building floor area but isn't assigned a space type.") end end # check for walls with adiabatic and ground boundary condition space.surfaces.each do |surface| next if surface.surfaceType != 'Wall' if surface.outsideBoundaryCondition == 'Ground' story_has_ground_walls << surface elsif surface.outsideBoundaryCondition == 'Adiabatic' story_has_adiabatic_walls << surface end end # populate overhang values space.surfaces.each do |surface| surface.subSurfaces.each do |sub_surface| sub_surface.shadingSurfaceGroups.each do |shading_surface_group| shading_surface_group.shadingSurfaces.each do |shading_surface| absoluteAzimuth = OpenStudio.convert(sub_surface.azimuth, 'rad', 'deg').get + sub_surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis absoluteAzimuth -= 360.0 until absoluteAzimuth < 360.0 # add to hash based on orientation if (facade_options['northEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southEast']) # East overhang building_overhang_area_e += shading_surface.grossArea * space.multiplier elsif (facade_options['southEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southWest']) # South overhang building_overhang_area_s += shading_surface.grossArea * space.multiplier elsif (facade_options['southWest'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['northWest']) # West overhang building_overhang_area_w += shading_surface.grossArea * space.multiplier else # North overhang building_overhang_area_n += shading_surface.grossArea * space.multiplier end end end end end # get max z space_z_max = OsLib_Geometry.getSurfaceZValues(space.surfaces.to_a).max + space.zOrigin if story_max_z.nil? || (story_max_z > space_z_max) story_max_z = space_z_max end end # populate hash for story data envelope_data_hash[:stories][story] = {} envelope_data_hash[:stories][story][:story_min_height] = story_min_z envelope_data_hash[:stories][story][:story_max_height] = story_max_z envelope_data_hash[:stories][story][:story_min_multiplier] = story_min_multiplier envelope_data_hash[:stories][story][:story_has_ground_walls] = story_has_ground_walls envelope_data_hash[:stories][story][:story_has_adiabatic_walls] = story_has_adiabatic_walls envelope_data_hash[:stories][story][:story_included_in_building_area] = story_included_in_building_area envelope_data_hash[:stories][story][:story_footprint] = story_footprint envelope_data_hash[:stories][story][:story_multiplied_floor_area] = story_multiplied_floor_area envelope_data_hash[:stories][story][:story_exterior_surface_area] = story_multiplied_exterior_surface_area envelope_data_hash[:stories][story][:story_multiplied_exterior_wall_area] = story_multiplied_exterior_wall_area envelope_data_hash[:stories][story][:story_multiplied_exterior_roof_area] = story_multiplied_exterior_roof_area # get perimeter and adiabatic walls that appear to be party walls perimeter_and_party_walls = OsLib_Geometry.calculate_story_exterior_wall_perimeter(runner, story, story_min_multiplier, ['Outdoors', 'Ground', 'Adiabatic'], bounding_box) envelope_data_hash[:stories][story][:story_perimeter] = perimeter_and_party_walls[:perimeter] envelope_data_hash[:stories][story][:story_party_walls] = [] east = false south = false west = false north = false perimeter_and_party_walls[:party_walls].each do |surface| absoluteAzimuth = OpenStudio.convert(surface.azimuth, 'rad', 'deg').get + surface.space.get.directionofRelativeNorth + model.getBuilding.northAxis absoluteAzimuth -= 360.0 until absoluteAzimuth < 360.0 # add to hash based on orientation (initially added array of sourfaces, but swtiched to just true/false flag) if (facade_options['northEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southEast']) # East party walls east = true elsif (facade_options['southEast'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['southWest']) # South party walls south = true elsif (facade_options['southWest'] <= absoluteAzimuth) && (absoluteAzimuth < facade_options['northWest']) # West party walls west = true else # North party walls north = true end end if east then envelope_data_hash[:stories][story][:story_party_walls] << 'east' end if south then envelope_data_hash[:stories][story][:story_party_walls] << 'south' end if west then envelope_data_hash[:stories][story][:story_party_walls] << 'west' end if north then envelope_data_hash[:stories][story][:story_party_walls] << 'north' end # store perimeter from first story that doesn't have ground walls if story_has_ground_walls.empty? && envelope_data_hash[:building_perimeter].nil? envelope_data_hash[:building_perimeter] = envelope_data_hash[:stories][story][:story_perimeter] runner.registerInfo(" * #{story.name} is the first above grade story and will be used for the building perimeter.") end end envelope_data_hash[:building_overhang_proj_factor_n] = building_overhang_area_n / ext_surfaces_hash['northWindow'] envelope_data_hash[:building_overhang_proj_factor_s] = building_overhang_area_s / ext_surfaces_hash['southWindow'] envelope_data_hash[:building_overhang_proj_factor_e] = building_overhang_area_e / ext_surfaces_hash['eastWindow'] envelope_data_hash[:building_overhang_proj_factor_w] = building_overhang_area_w / ext_surfaces_hash['westWindow'] # warn for spaces that are not on a story (in future could infer stories for these) model.getSpaces.sort.each do |space| if !space.buildingStory.is_initialized runner.registerWarning("#{space.name} is not on a building story, may have unexpected results.") end end return envelope_data_hash end end