module OpenstudioStandards # The CreateTypical module provides methods to create and modify an entire building energy model of a typical building module CreateTypical # @!group CreateTypical # Methods to create typical models # create typical building from model # creates a complete energy model from model with defined geometry and standards space type assignments # # @param template [String] standard template # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A'. # @param add_hvac [Boolean] Add HVAC systems to the model # @param hvac_system_type [String] HVAC system type # @param hvac_delivery_type [String] HVAC delivery type, how the system delivers heating or cooling to zones. # Options are 'Forced Air' or 'Hydronic'. # @param heating_fuel [String] The primary HVAC heating fuel type. # Options are 'Electricity', 'NaturalGas', 'DistrictHeating', 'DistrictHeatingWater', 'DistrictHeatingSteam', 'DistrictAmbient' # @param service_water_heating_fuel [String] The primary service water heating fuel type. # Options are 'Inferred', 'Electricity', 'NaturalGas', 'DistrictHeating', 'DistrictHeatingWater', 'DistrictHeatingSteam', 'HeatPump' # @param cooling_fuel [String] The primary HVAC cooling fuel type # Options are 'Electricity', 'DistrictCooling', 'DistrictAmbient' # @param kitchen_makeup [String] Source of makeup air for kitchen exhaust # Options are 'None', 'Largest Zone', 'Adjacent' # @param exterior_lighting_zone [String] The exterior lighting zone for exterior lighting allowance. # Options are '0 - Undeveloped Areas Parks', '1 - Developed Areas Parks', '2 - Neighborhood', '3 - All Other Areas', '4 - High Activity' # @param add_constructions [Boolean] Create and apply default construction set # @param wall_construction_type [String] wall construction type. # Options are 'Inferred', 'Mass', 'Metal Building', 'WoodFramed', 'SteelFramed' # @param add_space_type_loads [Boolean] Populate existing standards space types in the model with internal loads # @param add_daylighting_controls [Boolean] Add daylighting controls # @param add_elevators [Boolean] Apply elevators directly to a space in the model instead of to a space type # @param add_internal_mass [Boolean] Add internal mass to each space # @param add_exterior_lights [Boolean] Add exterior lightings objects to parking, canopies, and facades # @param onsite_parking_fraction [Double] Fraction of allowable exterior parking lighting applied. Set to 0 to add no parking lighting. # @param add_exhaust [Boolean] Add exhaust fans to the models. Primarly kitchen exhaust fans. # @param add_swh [Boolean] Add service water heating supply and demand objects # @param add_thermostat [Boolean] Add thermostats to thermal zones based on the standards space type # @param add_refrigeration [Boolean] Add refrigerated cases and walkin refrigeration # @param modify_wkdy_op_hrs [Boolean] Modify the default weekday hours of operation # @param wkdy_op_hrs_start_time [Double] Weekday operating hours start time. Enter as a fractional value, e.g. 5:15pm is 17.25. Only used if modify_wkdy_op_hrs is true. # @param wkdy_op_hrs_duration [Double] Weekday operating hours duration from start time. Enter as a fractional value, e.g. 5:15pm is 17.25. Only used if modify_wkdy_op_hrs is true. # @param modify_wknd_op_hrs [Boolean] Modify the default weekend hours of operation # @param wknd_op_hrs_start_time [Double] Weekend operation hours start time. Enter as a fractional value, e.g. 5:15pm is 17.25. Only used if modify_wknd_op_hrs is true. # @param wknd_op_hrs_duration [Double] Weekend operating hours duration from start time. Enter as a fractional value, e.g. 5:15pm is 17.25. Only used if modify_wknd_op_hrs is true. # @param hoo_var_method [String] hours of operation variable method. Options are 'hours' or 'fractional'. # @param enable_dst [Boolean] Enable daylight savings # @param unmet_hours_tolerance_r [Double] Thermostat setpoint tolerance for unmet hours in degrees Rankine # @param remove_objects [Boolean] Clean model of non-geometry objects. Only removes the same objects types as those added to the model. # @param user_hvac_mapping [Hash] Hash defining a mapping of system types to zones. # Structure is: # ['systems'][N]['system_type'] = 'MY_CBECS_HVAC_TYPE' as defined in lib/openstudio-standards/hvac/cbecs_hvac.rb # ['systems'][N]['thermal_zones'] = ['Zone 1', 'Zone 2', ...] # @return [Boolean] returns true if successful, false if not def self.create_typical_building_from_model(model, template, climate_zone: 'Lookup From Model', add_hvac: true, hvac_system_type: 'Inferred', hvac_delivery_type: 'Forced Air', heating_fuel: 'NaturalGas', service_water_heating_fuel: 'NaturalGas', cooling_fuel: 'Electricity', kitchen_makeup: 'Adjacent', exterior_lighting_zone: '3 - All Other Areas', add_constructions: true, wall_construction_type: 'Inferred', add_space_type_loads: true, add_daylighting_controls: true, add_elevators: true, add_internal_mass: true, add_exterior_lights: true, onsite_parking_fraction: 1.0, add_exhaust: true, add_swh: true, add_thermostat: true, add_refrigeration: true, modify_wkdy_op_hrs: false, wkdy_op_hrs_start_time: 8.0, wkdy_op_hrs_duration: 8.0, modify_wknd_op_hrs: false, wknd_op_hrs_start_time: 8.0, wknd_op_hrs_duration: 8.0, hoo_var_method: 'hours', enable_dst: true, unmet_hours_tolerance_r: 1.0, remove_objects: true, user_hvac_mapping: nil, sizing_run_directory: nil) # sizing run directory sizing_run_directory = Dir.pwd if sizing_run_directory.nil? # report initial condition of model initial_object_size = model.getModelObjects.size OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "The building started with #{initial_object_size} objects.") # create a new standard class standard = Standard.build(template) # validate climate zone if climate_zone == 'Lookup From Model' || climate_zone.nil? climate_zone = standard.model_get_building_properties(model)['climate_zone'] OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Using climate zone #{climate_zone} from model") else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Using climate zone #{climate_zone} from user arguments") end if climate_zone == '' OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', 'Could not determine climate zone from measure arguments or model.') return false end # validate weekday hours of operation wkdy_op_hrs_start_time_hr = nil wkdy_op_hrs_start_time_min = nil wkdy_op_hrs_duration_hr = nil wkdy_op_hrs_duration_min = nil if modify_wkdy_op_hrs # weekday start time hr wkdy_op_hrs_start_time_hr = wkdy_op_hrs_start_time.floor if wkdy_op_hrs_start_time_hr < 0 || wkdy_op_hrs_start_time_hr > 24 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekday operating hours start time hrs must be between 0 and 24. #{wkdy_op_hrs_start_time} was entered.") return false end # weekday start time min wkdy_op_hrs_start_time_min = (60.0 * (wkdy_op_hrs_start_time - wkdy_op_hrs_start_time.floor)).floor if wkdy_op_hrs_start_time_min < 0 || wkdy_op_hrs_start_time_min > 59 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekday operating hours start time mins must be between 0 and 59. #{wkdy_op_hrs_start_time} was entered.") return false end # weekday duration hr wkdy_op_hrs_duration_hr = wkdy_op_hrs_duration.floor if wkdy_op_hrs_duration_hr < 0 || wkdy_op_hrs_duration_hr > 24 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekday operating hours duration hrs must be between 0 and 24. #{wkdy_op_hrs_duration} was entered.") return false end # weekday duration min wkdy_op_hrs_duration_min = (60.0 * (wkdy_op_hrs_duration - wkdy_op_hrs_duration.floor)).floor if wkdy_op_hrs_duration_min < 0 || wkdy_op_hrs_duration_min > 59 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekday operating hours duration mins must be between 0 and 59. #{wkdy_op_hrs_duration} was entered.") return false end # check that weekday start time plus duration does not exceed 24 hrs if (wkdy_op_hrs_start_time_hr + wkdy_op_hrs_duration_hr + ((wkdy_op_hrs_start_time_min + wkdy_op_hrs_duration_min) / 60.0)) > 24.0 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Weekday start time of #{wkdy_op_hrs_start_time} plus duration of #{wkdy_op_hrs_duration} is more than 24 hrs, hours of operation overlap midnight.") end end # validate weekend hours of operation wknd_op_hrs_start_time_hr = nil wknd_op_hrs_start_time_min = nil wknd_op_hrs_duration_hr = nil wknd_op_hrs_duration_min = nil if modify_wknd_op_hrs # weekend start time hr wknd_op_hrs_start_time_hr = wknd_op_hrs_start_time.floor if wknd_op_hrs_start_time_hr < 0 || wknd_op_hrs_start_time_hr > 24 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekend operating hours start time hrs must be between 0 and 24. #{wknd_op_hrs_start_time} was entered.") return false end # weekend start time min wknd_op_hrs_start_time_min = (60.0 * (wknd_op_hrs_start_time - wknd_op_hrs_start_time.floor)).floor if wknd_op_hrs_start_time_min < 0 || wknd_op_hrs_start_time_min > 59 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekend operating hours start time mins must be between 0 and 59. #{wknd_op_hrs_start_time} was entered.") return false end # weekend duration hr wknd_op_hrs_duration_hr = wknd_op_hrs_duration.floor if wknd_op_hrs_duration_hr < 0 || wknd_op_hrs_duration_hr > 24 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekend operating hours duration hrs must be between 0 and 24. #{wknd_op_hrs_duration} was entered.") return false end # weekend duration min wknd_op_hrs_duration_min = (60.0 * (wknd_op_hrs_duration - wknd_op_hrs_duration.floor)).floor if wknd_op_hrs_duration_min < 0 || wknd_op_hrs_duration_min > 59 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Weekend operating hours duration min smust be between 0 and 59. #{wknd_op_hrs_duration} was entered.") return false end # check that weekend start time plus duration does not exceed 24 hrs if (wknd_op_hrs_start_time_hr + wknd_op_hrs_duration_hr + ((wknd_op_hrs_start_time_min + wknd_op_hrs_duration_min) / 60.0)) > 24.0 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Weekend start time of #{wknd_op_hrs_start} plus duration of #{wknd_op_hrs_duration} is more than 24 hrs, hours of operation overlap midnight.") end end # validate unmet hours tolerance if unmet_hours_tolerance_r < 0 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', 'unmet_hours_tolerance_r must be greater than or equal to 0 Rankine.') return false elsif unmet_hours_tolerance_r > 5.0 OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', 'unmet_hours_tolerance_r must be less than or equal to 5 Rankine.') return false end # make sure daylight savings is turned on up prior to any sizing runs being done. if enable_dst start_date = '2nd Sunday in March' end_date = '1st Sunday in November' runperiodctrl_daylightsaving = model.getRunPeriodControlDaylightSavingTime runperiodctrl_daylightsaving.setStartDate(start_date) runperiodctrl_daylightsaving.setEndDate(end_date) end # add internal loads to space types if add_space_type_loads # remove internal loads if remove_objects model.getSpaceLoads.sort.each do |instance| # most prototype building types model exterior elevators with name Elevator next if instance.name.to_s.include?('Elevator') next if instance.to_InternalMass.is_initialized next if instance.to_WaterUseEquipment.is_initialized instance.remove end model.getDesignSpecificationOutdoorAirs.each(&:remove) model.getDefaultScheduleSets.each(&:remove) end model.getSpaceTypes.sort.each do |space_type| # Don't add infiltration here; will be added later in the script test = standard.space_type_apply_internal_loads(space_type, true, true, true, true, true, false) if test == false OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CreateTypical', "Could not add loads for #{space_type.name}. Not expected for #{template}") next end # apply internal load schedules # the last bool test it to make thermostat schedules. They are now added in HVAC section instead of here standard.space_type_apply_internal_load_schedules(space_type, true, true, true, true, true, true, false) # extend space type name to include the template. Consider this as well for load defs space_type.setName("#{space_type.name} - #{template}") OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding loads to space type named #{space_type.name}") end # warn if spaces in model without space type spaces_without_space_types = [] model.getSpaces.sort.each do |space| next if space.spaceType.is_initialized spaces_without_space_types << space end if !spaces_without_space_types.empty? OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CreateTypical', "#{spaces_without_space_types.size} spaces do not have space types assigned, and wont' receive internal loads from standards space type lookups.") end end # identify primary building type (used for construction, and ideally HVAC as well) building_types = {} model.getSpaceTypes.sort.each do |space_type| # populate hash of building types if space_type.standardsBuildingType.is_initialized bldg_type = space_type.standardsBuildingType.get if building_types.key?(bldg_type) building_types[bldg_type] += space_type.floorArea else building_types[bldg_type] = space_type.floorArea end else OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CreateTypical', "Can't identify building type for #{space_type.name}") end end # @todo this fails if no space types, or maybe just no space types with standards primary_bldg_type = building_types.key(building_types.values.max) # Used for some lookups in the standards gem lookup_building_type = standard.model_get_lookup_name(primary_bldg_type) model.getBuilding.setStandardsBuildingType(primary_bldg_type) # set FC factor constructions before adding other constructions standard.model_set_below_grade_wall_constructions(model, lookup_building_type, climate_zone) standard.model_set_floor_constructions(model, lookup_building_type, climate_zone) if model.getFFactorGroundFloorConstructions.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', 'Unable to determine FC factor value to use. Using default ground construction instead.') else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', 'Set FC factor constructions for slab and below grade walls.') end # adjust F factor constructions to avoid simulation errors model.getFFactorGroundFloorConstructions.each do |cons| # Rfilm_in = 0.135, Rfilm_out = 0.03, Rcons for 6" heavy concrete = 0.15m / 1.95 W/mK, 0.001 minimum resistance of Rfic resistive layer if cons.area <= (0.135 + 0.03 + (0.15 / 1.95) + 0.001) * cons.perimeterExposed * cons.fFactor # set minimum Rfic to ~ R1 = 0.18 m^2K/W new_area = 0.422 * cons.perimeterExposed * cons.fFactor OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "F-factor fictitious resistance for #{cons.name.get} with Area=#{cons.area.round(2)}, Exposed Perimeter=#{cons.perimeterExposed.round(2)}, and F-factor=#{cons.fFactor.round(2)} will result in a negative value and a failed simulation. Construction area is adjusted to be #{new_area.round(2)} m2.") cons.setArea(new_area) end end # make construction set and apply to building if add_constructions # remove default construction sets if remove_objects model.getDefaultConstructionSets.each(&:remove) end if ['SmallHotel', 'LargeHotel', 'MidriseApartment', 'HighriseApartment'].include?(primary_bldg_type) is_residential = 'Yes' occ_type = 'Residential' else is_residential = 'No' occ_type = 'Nonresidential' end bldg_def_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, is_residential) if bldg_def_const_set.is_initialized bldg_def_const_set = bldg_def_const_set.get if is_residential == 'Yes' bldg_def_const_set.setName("Res #{bldg_def_const_set.name}") end model.getBuilding.setDefaultConstructionSet(bldg_def_const_set) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding default construction set named #{bldg_def_const_set.name}") else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "Could not create default construction set for the building type #{lookup_building_type} in climate zone #{climate_zone} with template #{template}.") return false end # Replace the construction of exterior walls with user-specified wall construction type unless wall_construction_type == 'Inferred' # Check that a default exterior construction set is defined if bldg_def_const_set.defaultExteriorSurfaceConstructions.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', 'Default construction set has no default exterior surface constructions.') return false end ext_surf_consts = bldg_def_const_set.defaultExteriorSurfaceConstructions.get # Check that a default exterior wall is defined if ext_surf_consts.wallConstruction.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', 'Default construction set has no default exterior wall construction.') return false end old_construction = ext_surf_consts.wallConstruction.get standards_info = old_construction.standardsInformation # Get the old wall construction type if standards_info.standardsConstructionType.empty? old_wall_construction_type = 'Not defined' else old_wall_construction_type = standards_info.standardsConstructionType.get end # Modify the default wall construction if different from measure input if old_wall_construction_type == wall_construction_type # Don't modify if the default matches the user-specified wall construction type OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Exterior wall construction type #{wall_construction_type} is the default for this building type.") else climate_zone_set = standard.model_find_climate_zone_set(model, climate_zone) new_construction = standard.model_find_and_add_construction(model, climate_zone_set, 'ExteriorWall', wall_construction_type, occ_type) ext_surf_consts.setWallConstruction(new_construction) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Set exterior wall construction to #{new_construction.name}, replacing building type default #{old_construction.name}.") end end # Replace the construction of any outdoor-facing "AtticFloor" surfaces # with the "ExteriorRoof" - "IEAD" construction for the specific climate zone and template. # This prevents creation of buildings where the DOE Prototype building construction set # assumes an attic but the supplied geometry used does not have an attic. new_construction = nil climate_zone_set = standard.model_find_climate_zone_set(model, climate_zone) model.getSurfaces.sort.each do |surf| next unless surf.outsideBoundaryCondition == 'Outdoors' next unless surf.surfaceType == 'RoofCeiling' next if surf.construction.empty? construction = surf.construction.get standards_info = construction.standardsInformation next if standards_info.intendedSurfaceType.empty? next unless standards_info.intendedSurfaceType.get == 'AtticFloor' if new_construction.nil? new_construction = standard.model_find_and_add_construction(model, climate_zone_set, 'ExteriorRoof', 'IEAD', occ_type) end surf.setConstruction(new_construction) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Changed the construction for #{surf.name} from #{construction.name} to #{new_construction.name} to avoid outdoor-facing attic floor constructions in buildings with no attic space.") end # address any adiabatic surfaces that don't have hard assigned constructions model.getSurfaces.sort.each do |surface| next if surface.outsideBoundaryCondition != 'Adiabatic' next if surface.construction.is_initialized surface.setAdjacentSurface(surface) surface.setConstruction(surface.construction.get) surface.setOutsideBoundaryCondition('Adiabatic') end # modify the infiltration rates if remove_objects model.getSpaceInfiltrationDesignFlowRates.each(&:remove) end standard.model_apply_infiltration_standard(model) standard.model_modify_infiltration_coefficients(model, primary_bldg_type, climate_zone) # set ground temperatures from DOE prototype buildings OpenstudioStandards::Weather.model_set_ground_temperatures(model, climate_zone: climate_zone) end # add elevators (returns ElectricEquipment object) if add_elevators # remove elevators as spaceLoads or exteriorLights model.getSpaceLoads.sort.each do |instance| next if !instance.name.to_s.include?('Elevator') # most prototype building types model exterior elevators with name Elevator instance.remove end model.getExteriorLightss.sort.each do |ext_light| next if !ext_light.name.to_s.include?('Fuel equipment') # some prototype building types model exterior elevators by this name ext_light.remove end elevators = standard.model_add_elevators(model) if elevators.nil? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', 'No elevators added to the building.') else elevator_def = elevators.electricEquipmentDefinition design_level = elevator_def.designLevel.get OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding #{elevators.multiplier.round(1)} elevators each with power of #{OpenStudio.toNeatString(design_level, 0, true)} (W), plus lights and fans.") elevator_def.setFractionLatent(0.0) elevator_def.setFractionRadiant(0.0) elevator_def.setFractionLost(1.0) end end # add exterior lights (returns a hash where key is lighting type and value is exteriorLights object) if add_exterior_lights if remove_objects model.getExteriorLightss.sort.each do |ext_light| next if ext_light.name.to_s.include?('Fuel equipment') # some prototype building types model exterior elevators by this name ext_light.remove end end exterior_lights = standard.model_add_typical_exterior_lights(model, exterior_lighting_zone.chars[0].to_i, onsite_parking_fraction) exterior_lights.each do |k, v| OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding Exterior Lights named #{v.exteriorLightsDefinition.name} with design level of #{v.exteriorLightsDefinition.designLevel} * #{OpenStudio.toNeatString(v.multiplier, 0, true)}.") end end # add_exhaust if add_exhaust # remove exhaust objects if remove_objects model.getFanZoneExhausts.each(&:remove) end zone_exhaust_fans = standard.model_add_exhaust(model, kitchen_makeup) # second argument is strategy for finding makeup zones for exhaust zones zone_exhaust_fans.each do |k, v| max_flow_rate_ip = OpenStudio.convert(k.maximumFlowRate.get, 'm^3/s', 'cfm').get if v.key?(:zone_mixing) zone_mixing = v[:zone_mixing] mixing_source_zone_name = zone_mixing.sourceZone.get.name mixing_design_flow_rate_ip = OpenStudio.convert(zone_mixing.designFlowRate.get, 'm^3/s', 'cfm').get OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding #{OpenStudio.toNeatString(max_flow_rate_ip, 0, true)} (cfm) of exhaust to #{k.thermalZone.get.name}, with #{OpenStudio.toNeatString(mixing_design_flow_rate_ip, 0, true)} (cfm) of makeup air from #{mixing_source_zone_name}") else OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding #{OpenStudio.toNeatString(max_flow_rate_ip, 0, true)} (cfm) of exhaust to #{k.thermalZone.get.name}") end end end # add service water heating demand and supply if add_swh # remove water use equipment and water use connections if remove_objects # @todo remove plant loops used for service water heating model.getWaterUseEquipments.each(&:remove) model.getWaterUseConnectionss.each(&:remove) end # Infer the SWH type if service_water_heating_fuel == 'Inferred' if heating_fuel == 'NaturalGas' || heating_fuel.include?('DistrictHeating') # If building has gas service, probably uses natural gas for SWH service_water_heating_fuel = 'NaturalGas' elsif heating_fuel == 'Electricity' # If building is doing space heating with electricity, probably used for SWH service_water_heating_fuel = 'Electricity' elsif heating_fuel == 'DistrictAmbient' # If building has district ambient loop, it is fancy and probably uses HPs for SWH service_water_heating_fuel = 'HeatPump' else # Use inferences built into OpenStudio Standards for each building and space type service_water_heating_fuel = nil end end typical_swh = standard.model_add_typical_swh(model, water_heater_fuel: service_water_heating_fuel) midrise_swh_loops = [] stripmall_swh_loops = [] typical_swh.each do |loop| if loop.name.get.include?('MidriseApartment') midrise_swh_loops << loop elsif loop.name.get.include?('RetailStripmall') stripmall_swh_loops << loop else water_use_connections = [] loop.demandComponents.each do |component| next if !component.to_WaterUseConnections.is_initialized water_use_connections << component end OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding #{loop.name} to the building. It has #{water_use_connections.size} water use connections.") end end if !midrise_swh_loops.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding #{midrise_swh_loops.size} MidriseApartment service water heating loops.") end if !stripmall_swh_loops.empty? OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Adding #{stripmall_swh_loops.size} RetailStripmall service water heating loops.") end end # add_daylighting_controls if add_daylighting_controls # remove add_daylighting_controls objects if remove_objects model.getDaylightingControls.each(&:remove) end # add daylight controls, need to perform a sizing run for 2010 if (template == '90.1-2010' || template == 'ComStock 90.1-2010') && (standard.model_run_sizing_run(model, "#{sizing_run_directory}/create_typical_building_from_model_SR0") == false) return false end standard.model_add_daylighting_controls(model) end # add refrigeration if add_refrigeration # remove refrigeration equipment if remove_objects model.getRefrigerationSystems.each(&:remove) end # Add refrigerated cases and walkins standard.model_add_typical_refrigeration(model, primary_bldg_type) end # @todo add slab modeling and slab insulation # @todo fuel customization for cooking and laundry # works by switching some fraction of electric loads to gas if requested (assuming base load is electric) # add thermostats if add_thermostat # remove thermostats if remove_objects model.getThermostatSetpointDualSetpoints.each(&:remove) end model.getSpaceTypes.sort.each do |space_type| # create thermostat schedules # skip un-recognized space types next if standard.space_type_get_standards_data(space_type).empty? # the last bool test it to make thermostat schedules. They are added to the model but not assigned standard.space_type_apply_internal_load_schedules(space_type, false, false, false, false, false, false, true) # identify thermal thermostat and apply to zones (apply_internal_load_schedules names ) model.getThermostatSetpointDualSetpoints.sort.each do |thermostat| next if thermostat.name.to_s != "#{space_type.name} Thermostat" next if !thermostat.coolingSetpointTemperatureSchedule.is_initialized next if !thermostat.heatingSetpointTemperatureSchedule.is_initialized OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Assigning #{thermostat.name} to thermal zones with #{space_type.name} assigned.") space_type.spaces.sort.each do |space| next if !space.thermalZone.is_initialized space.thermalZone.get.setThermostatSetpointDualSetpoint(thermostat) end end end end # add internal mass if add_internal_mass if remove_objects model.getSpaceLoads.sort.each do |instance| next unless instance.to_InternalMass.is_initialized instance.remove end end # add internal mass to conditioned spaces; needs to happen after thermostats are applied standard.model_add_internal_mass(model, primary_bldg_type) end # add hvac system if add_hvac # remove HVAC objects if remove_objects standard.model_remove_prm_hvac(model) end # If user does not map HVAC types to zones with a JSON file, run conventional approach to HVAC assignment if user_hvac_mapping.nil? case hvac_system_type when 'Inferred' # Get the hvac delivery type enum hvac_delivery = case hvac_delivery_type when 'Forced Air' 'air' when 'Hydronic' 'hydronic' end # Group the zones by occupancy type. Only split out non-dominant groups if their total area exceeds the limit. min_area_m2 = OpenStudio.convert(20_000, 'ft^2', 'm^2').get sys_groups = OpenstudioStandards::Geometry.model_group_thermal_zones_by_occupancy_type(model, min_area_m2: min_area_m2) # For each group, infer the HVAC system type. sys_groups.each do |sys_group| # Infer the primary system type sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel = standard.model_typical_hvac_system_type(model, climate_zone, sys_group['type'], hvac_delivery, heating_fuel, cooling_fuel, OpenStudio.convert(sys_group['area_ft2'], 'ft^2', 'm^2').get, sys_group['stories']) # Infer the secondary system type for multizone systems sec_sys_type = case sys_type when 'PVAV Reheat', 'VAV Reheat' 'PSZ-AC' when 'PVAV PFP Boxes', 'VAV PFP Boxes' 'PSZ-HP' else sys_type # same as primary system type end # group zones story_zone_lists = OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_story(model, sys_group['zones']) # On each story, add the primary system to the primary zones # and add the secondary system to any zones that are different. story_zone_lists.each do |story_group| # Differentiate primary and secondary zones, based on # operating hours and internal loads (same as 90.1 PRM) pri_sec_zone_lists = standard.model_differentiate_primary_secondary_thermal_zones(model, story_group) system_zones = pri_sec_zone_lists['primary'] # if the primary system type is PTAC, filter to cooled zones to prevent sizing error if no cooling if sys_type == 'PTAC' heated_and_cooled_zones = system_zones.select { |zone| OpenstudioStandards::ThermalZone.thermal_zone_heated?(zone) && OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zone) } cooled_only_zones = system_zones.select { |zone| !OpenstudioStandards::ThermalZone.thermal_zone_heated?(zone) && OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zone) } system_zones = heated_and_cooled_zones + cooled_only_zones end # Add the primary system to the primary zones unless standard.model_add_hvac_system(model, sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel, system_zones) OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "HVAC system type '#{sys_type}' not recognized. Check input system type argument against Model.hvac.rb for valid hvac system type names.") return false end # Add the secondary system to the secondary zones (if any) if !pri_sec_zone_lists['secondary'].empty? system_zones = pri_sec_zone_lists['secondary'] if (sec_sys_type == 'PTAC') || (sec_sys_type == 'PSZ-AC') heated_and_cooled_zones = system_zones.select { |zone| OpenstudioStandards::ThermalZone.thermal_zone_heated?(zone) && OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zone) } cooled_only_zones = system_zones.select { |zone| !OpenstudioStandards::ThermalZone.thermal_zone_heated?(zone) && OpenstudioStandards::ThermalZone.thermal_zone_cooled?(zone) } system_zones = heated_and_cooled_zones + cooled_only_zones end unless standard.model_add_hvac_system(model, sec_sys_type, central_htg_fuel, zone_htg_fuel, clg_fuel, system_zones) OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "HVAC system type '#{sys_type}' not recognized. Check input system type argument against Model.hvac.rb for valid hvac system type names.") return false end end end end else # HVAC system_type specified # Group the zones by occupancy type. Only split out non-dominant groups if their total area exceeds the limit. min_area_m2 = OpenStudio.convert(20_000, 'ft^2', 'm^2').get sys_groups = OpenstudioStandards::Geometry.model_group_thermal_zones_by_occupancy_type(model, min_area_m2: min_area_m2) sys_groups.each do |sys_group| # group zones story_zone_groups = OpenstudioStandards::Geometry.model_group_thermal_zones_by_building_story(model, sys_group['zones']) # Add the user specified HVAC system for each story. # Single-zone systems will get one per zone. story_zone_groups.each do |zones| unless OpenstudioStandards::HVAC.add_cbecs_hvac_system(model, standard, hvac_system_type, zones) OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "HVAC system type '#{hvac_system_type}' not recognized. Check input system type argument against cbecs_hvac.rb in the HVAC module for valid HVAC system type names.") return false end end end end else # If user specified a mapping of HVAC systems to zones user_hvac_mapping['systems'].each do |system_hash| hvac_system_type = system_hash['system_type'] zone_names = system_hash['thermal_zones'] # Get OS:ThermalZone objects zones = zone_names.map do |zone_name| model.getThermalZoneByName(zone_name).get end puts "Adding #{hvac_system_type} to #{zone_names.join(', ')}" unless OpenstudioStandards::HVAC.add_cbecs_hvac_system(model, standard, hvac_system_type, zones) OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "HVAC system type '#{hvac_system_type}' not recognized. Check input system type argument against cbecs_hvac.rb in the HVAC module for valid HVAC system type names.") return false end end end end # hours of operation if modify_wkdy_op_hrs || modify_wknd_op_hrs # Infer the current hours of operation schedule for the building op_sch = OpenstudioStandards::Schedules.model_infer_hours_of_operation_building(model) # Convert existing schedules in the model to parametric schedules based on current hours of operation OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Generating parametric schedules from ruleset schedules using #{hoo_var_method} variable method for hours of operation formula.") OpenstudioStandards::Schedules.model_setup_parametric_schedules(model, hoo_var_method: hoo_var_method) # Create start and end times from start time and duration supplied wkdy_start_time = nil wkdy_end_time = nil wknd_start_time = nil wknd_end_time = nil # weekdays if modify_wkdy_op_hrs wkdy_start_time = OpenStudio::Time.new(0, wkdy_op_hrs_start_time_hr, wkdy_op_hrs_start_time_min, 0) wkdy_end_time = wkdy_start_time + OpenStudio::Time.new(0, wkdy_op_hrs_duration_hr, wkdy_op_hrs_duration_min, 0) end # weekends if modify_wknd_op_hrs wknd_start_time = OpenStudio::Time.new(0, wknd_op_hrs_start_time_hr, wknd_op_hrs_start_time_min, 0) wknd_end_time = wknd_start_time + OpenStudio::Time.new(0, wknd_op_hrs_duration_hr, wknd_op_hrs_duration_min, 0) end # Modify hours of operation, using weekdays values for all weekdays and weekend values for Saturday and Sunday OpenstudioStandards::Schedules.schedule_ruleset_set_hours_of_operation(op_sch, wkdy_start_time: wkdy_start_time, wkdy_end_time: wkdy_end_time, sat_start_time: wknd_start_time, sat_end_time: wknd_end_time, sun_start_time: wknd_start_time, sun_end_time: wknd_end_time) # Apply new operating hours to parametric schedules to make schedules in model reflect modified hours of operation parametric_schedules = OpenstudioStandards::Schedules.model_apply_parametric_schedules(model, error_on_out_of_order: false) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Updated #{parametric_schedules.size} schedules with new hours of operation.") end # set hvac controls and efficiencies (this should be last model articulation element) if add_hvac # set additional properties for building props = model.getBuilding.additionalProperties props.setFeature('hvac_system_type', hvac_system_type) case hvac_system_type when 'Ideal Air Loads' else # Set the heating and cooling sizing parameters standard.model_apply_prm_sizing_parameters(model) # Perform a sizing run if standard.model_run_sizing_run(model, "#{sizing_run_directory}/create_typical_building_from_model_SR1") == false return false end # If there are any multizone systems, reset damper positions # to achieve a 60% ventilation effectiveness minimum for the system # following the ventilation rate procedure from 62.1 standard.model_apply_multizone_vav_outdoor_air_sizing(model) # Apply the prototype HVAC assumptions standard.model_apply_prototype_hvac_assumptions(model, primary_bldg_type, climate_zone) # Apply the HVAC efficiency standard standard.model_apply_hvac_efficiency_standard(model, climate_zone) end end # set unmet hours tolerance unmet_hrs_tol_k = OpenStudio.convert(unmet_hours_tolerance_r, 'R', 'K').get tolerances = model.getOutputControlReportingTolerances tolerances.setToleranceforTimeHeatingSetpointNotMet(unmet_hrs_tol_k) tolerances.setToleranceforTimeCoolingSetpointNotMet(unmet_hrs_tol_k) # remove everything but spaces, zones, and stub space types (extend as needed for additional objects, may make bool arg for this) if remove_objects model.purgeUnusedResourceObjects objects_after_cleanup = initial_object_size - model.getModelObjects.size if objects_after_cleanup > 0 OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Removing #{objects_after_cleanup} objects from model") end end # change night cycling control to "Thermostat" cycling and increase thermostat tolerance to 1.99999 manager_night_cycles = model.getAvailabilityManagerNightCycles OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Changing thermostat tolerance to 1.99999 for #{manager_night_cycles.size} night cycle manager objects.") manager_night_cycles.each do |night_cycle| night_cycle.setThermostatTolerance(1.9999) night_cycle.setCyclingRunTimeControlType('Thermostat') end # disable HVAC Sizing Simulation for Sizing Periods, not used for the type of PlantLoop sizing used in ComStock if model.version >= OpenStudio::VersionString.new('3.0.0') sim_control = model.getSimulationControl sim_control.setDoHVACSizingSimulationforSizingPeriodsNoFail(false) end # report final condition of model OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "The building finished with #{model.getModelObjects.size} objects.") return true end # creates spaces types and construction objects in the model for the given # building type, template, and climate zone # # @param building_type [String] standard building type # @param template [String] standard template # @param climate_zone [String] ASHRAE climate zone, e.g. 'ASHRAE 169-2013-4A' # @param create_space_types [Boolean] Create space types # @param create_construction_set [Boolean] Create the construction set # @param set_building_defaults [Boolean] Set the climate zone, newly generated construction set, # and first newly generated space type as the building default # @return [Boolean] returns true if successful, false if not def self.create_space_types_and_constructions(model, building_type, template, climate_zone, create_space_types: true, create_construction_set: true, set_building_defaults: true) # reporting initial condition of model starting_space_types = model.getSpaceTypes.sort starting_construction_sets = model.getDefaultConstructionSets.sort OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "The building started with #{starting_space_types.size} space types and #{starting_construction_sets.size} construction sets.") # lookup space types for specified building type (false indicates not to use whole building type only) space_type_hash = OpenstudioStandards::CreateTypical.get_space_types_from_building_type(building_type, template: template, whole_building: false) if space_type_hash == false OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', "#{building_type} is an unexpected building type.") return false end # create space_type_map from array space_type_map = {} default_space_type_name = nil space_type_hash.each do |space_type_name, hash| # skip space types like undeveloped and basement next if hash[:space_type_gen] == false # no spaces to pass in space_type_map[space_type_name] = [] if hash[:default] default_space_type_name = space_type_name end end # Make the standard applier standard = Standard.build(template) # mapping building_type name is needed for a few methods lookup_building_type = standard.model_get_lookup_name(building_type) # remap small medium and large office to office if building_type.include?('Office') building_type = 'Office' end # get array of new space types space_types_new = [] # create_space_types if create_space_types # array of starting space types space_types_starting = model.getSpaceTypes.sort # create stub space types space_type_hash.each do |space_type_name, hash| # skip space types like undeveloped and basement next if hash[:space_type_gen] == false # create space type space_type = OpenStudio::Model::SpaceType.new(model) space_type.setStandardsBuildingType(lookup_building_type) space_type.setStandardsSpaceType(space_type_name) space_type.setName("#{lookup_building_type} #{space_type_name}") # add to array of new space types space_types_new << space_type # add internal loads (the nil check isn't necessary, but I will keep it in as a warning instad of an error) test = standard.space_type_apply_internal_loads(space_type, true, true, true, true, true, true) if test.nil? OpenStudio.logFree(OpenStudio::Warn, 'openstudio.standards.CreateTypical', "Could not add loads for #{space_type.name}. Not expected for #{template} #{lookup_building_type}") end # the last bool test it to make thermostat schedules. They are added to the model but not assigned standard.space_type_apply_internal_load_schedules(space_type, true, true, true, true, true, true, true) # assign colors standard.space_type_apply_rendering_color(space_type) # exend space type name to include the template. Consider this as well for load defs space_type.setName("#{space_type.name} - #{template}") OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Added space type named #{space_type.name}") end end # add construction sets bldg_def_const_set = nil if create_construction_set # Make the default construction set for the building is_residential = 'No' # default is nonresidential for building level bldg_def_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, is_residential) if bldg_def_const_set.is_initialized bldg_def_const_set = bldg_def_const_set.get OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Added default construction set named #{bldg_def_const_set.name}") else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', 'Could not create default construction set for the building.') return false end # make residential construction set as unused resource if ['SmallHotel', 'LargeHotel', 'MidriseApartment', 'HighriseApartment'].include?(building_type) res_const_set = standard.model_add_construction_set(model, climate_zone, lookup_building_type, nil, 'Yes') if res_const_set.is_initialized res_const_set = res_const_set.get res_const_set.setName("#{bldg_def_const_set.name} - Residential ") OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Added residential construction set named #{res_const_set.name}") else OpenStudio.logFree(OpenStudio::Error, 'openstudio.standards.CreateTypical', 'Could not create residential construction set for the building.') return false end end end # set_building_defaults if set_building_defaults # identify default space type default_space_type = nil space_types_new.each do |space_type| standards_building_type = space_type.standardsBuildingType.is_initialized ? space_type.standardsBuildingType.get : nil standards_space_type = space_type.standardsSpaceType.is_initialized ? space_type.standardsSpaceType.get : nil if default_space_type_name == standards_space_type default_space_type = space_type end end # set default space type building = model.getBuilding if !default_space_type.nil? building.setSpaceType(default_space_type) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Setting default Space Type for building to #{building.spaceType.get.name}") end # default construction if !bldg_def_const_set.nil? building.setDefaultConstructionSet(bldg_def_const_set) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Setting default Construction Set for building to #{building.defaultConstructionSet.get.name}") end # set climate zone os_climate_zone = climate_zone.gsub('ASHRAE 169-2013-', '') # trim off letter from climate zone 7 or 8 if (os_climate_zone[0] == '7') || (os_climate_zone[0] == '8') os_climate_zone = os_climate_zone[0] end climate_zone = model.getClimateZones.setClimateZone('ASHRAE', os_climate_zone) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Setting #{climate_zone.institution} Climate Zone to #{climate_zone.value}") # set building type # use lookup_building_type so spaces like MediumOffice will map to Office (Supports baseline automation) building.setStandardsBuildingType(lookup_building_type) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Setting Standards Building Type to #{building.standardsBuildingType}") # rename building if it is named "Building 1" if model.getBuilding.name.to_s == 'Building 1' model.getBuilding.setName("#{building_type} #{template} #{os_climate_zone}") OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "Renaming building to #{model.getBuilding.name}") end end # reporting final condition of model finishing_space_types = model.getSpaceTypes.sort finishing_construction_sets = model.getDefaultConstructionSets.sort OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.CreateTypical', "The building finished with #{finishing_space_types.size} space types and #{finishing_construction_sets.size} construction sets.") return true end end end